-
Notifications
You must be signed in to change notification settings - Fork 38
Dismissable App Promo Banner #2394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release/5.37.0
Are you sure you want to change the base?
Changes from all commits
45323d4
b61ec65
91a49a5
cc130fc
c50cbc1
89b00b0
47203df
3da9b6b
9218e3f
077117e
45f8bd6
1779c51
5199754
cdafa95
85014fb
5e591ce
6268562
4f2b5c6
9a0dd5e
6817e58
0aed2a4
1e8117a
bd6034d
c559337
1f3710d
192ce88
924d488
1e98b16
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { DataStorageAccess } from "background/helpers/dataStorageAccess"; | ||
|
|
||
| const STORAGE_KEY = "mobileAppBannerDismissed"; | ||
|
|
||
| export const dismissMobileAppBanner = async ({ | ||
| localStore, | ||
| }: { | ||
| localStore: DataStorageAccess; | ||
| }): Promise<void> => { | ||
| await localStore.setItem(STORAGE_KEY, true); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it'd be nice to have this function return |
||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { DataStorageAccess } from "background/helpers/dataStorageAccess"; | ||
|
|
||
| const STORAGE_KEY = "mobileAppBannerDismissed"; | ||
|
|
||
| export const getMobileAppBannerDismissed = async ({ | ||
| localStore, | ||
| }: { | ||
| localStore: DataStorageAccess; | ||
| }): Promise<{ isDismissed: boolean }> => { | ||
| const dismissed = await localStore.getItem(STORAGE_KEY); | ||
| return { isDismissed: !!dismissed }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import React, { useState, useEffect } from "react"; | ||
| import { Icon, Text } from "@stellar/design-system"; | ||
| import { useTranslation } from "react-i18next"; | ||
| import browser from "webextension-polyfill"; | ||
|
|
||
| import { | ||
| getMobileAppBannerDismissed, | ||
| dismissMobileAppBanner, | ||
| } from "@shared/api/internal"; | ||
|
|
||
| import { openTab } from "popup/helpers/navigate"; | ||
| import FreighterLogo from "popup/assets/logo-freighter-shadow.png"; | ||
|
|
||
| import "./styles.scss"; | ||
|
|
||
| // Check if browser storage is available (extension context, not fullscreen) | ||
| const isStorageAvailable = (): boolean => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this check is a holdover from the last version of this PR, right? You shouldn't need to check this anymore. Whether you're in extension mode or in fullscreen mode, you should have access to the background storage now. You should be able to safely remove this |
||
| return !!browser?.storage?.local; | ||
| }; | ||
|
|
||
| export const MobileAppBanner = () => { | ||
| const { t } = useTranslation(); | ||
| const [isDismissed, setIsDismissed] = useState<boolean | null>(null); | ||
| const [isLoading, setIsLoading] = useState(true); | ||
|
|
||
| useEffect(() => { | ||
| const checkDismissedStatus = async () => { | ||
| try { | ||
| const dismissed = await getMobileAppBannerDismissed(); | ||
| setIsDismissed(dismissed); | ||
| } catch (error) { | ||
| console.error("Error checking banner dismissal status:", error); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's use |
||
| setIsDismissed(false); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| if (isStorageAvailable()) { | ||
| checkDismissedStatus(); | ||
| } else { | ||
| setIsLoading(false); | ||
| } | ||
| }, []); | ||
|
|
||
| const handleDismiss = async (e: React.MouseEvent) => { | ||
| e.stopPropagation(); | ||
| try { | ||
| await dismissMobileAppBanner(); | ||
| setIsDismissed(true); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Related to this comment: https://github.com/stellar/freighter/pull/2394/files#r2571903580
|
||
| } catch (error) { | ||
| console.error("Error dismissing banner:", error); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. similar to above comment - let's use |
||
| } | ||
| }; | ||
|
|
||
| const handleBannerClick = () => { | ||
| openTab("https://www.freighter.app/#download"); | ||
| }; | ||
|
|
||
| // Don't show banner if storage is not available (e.g., fullscreen mode) or if dismissed | ||
| if (!isStorageAvailable() || isLoading || isDismissed) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| className="MobileAppBanner" | ||
| data-testid="mobile-app-banner" | ||
| onClick={handleBannerClick} | ||
| > | ||
| <div className="MobileAppBanner__content"> | ||
| <div className="MobileAppBanner__text"> | ||
| <Text | ||
| as="div" | ||
| size="sm" | ||
| weight="medium" | ||
| className="MobileAppBanner__title" | ||
| > | ||
| {t("Introducing Freighter Mobile")} | ||
| </Text> | ||
| <Text | ||
| as="div" | ||
| size="xs" | ||
| weight="medium" | ||
| className="MobileAppBanner__subtitle" | ||
| > | ||
| {t("Download on iOS or Android today")} | ||
| </Text> | ||
| </div> | ||
| <div className="MobileAppBanner__logo"> | ||
| <img src={FreighterLogo} alt="Freighter logo" /> | ||
| </div> | ||
| </div> | ||
| <button | ||
| type="button" | ||
| className="MobileAppBanner__dismiss" | ||
| onClick={handleDismiss} | ||
| aria-label="Dismiss banner" | ||
| data-testid="mobile-app-banner-dismiss" | ||
| > | ||
| <Icon.X /> | ||
| </button> | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| @use "../../../styles/utils.scss" as *; | ||
|
|
||
| .MobileAppBanner { | ||
| position: relative; | ||
| background: var(--sds-clr-lilac-03); | ||
| border-radius: pxToRem(12px); | ||
| padding: pxToRem(16px); | ||
| margin: pxToRem(12px) 0 pxToRem(0) pxToRem(-16px); | ||
| width: calc(100% + pxToRem(32px)); | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: space-between; | ||
| gap: pxToRem(12px); | ||
| cursor: pointer; | ||
| transition: opacity 0.2s ease; | ||
|
|
||
| &:hover { | ||
| opacity: 0.9; | ||
| } | ||
|
|
||
| &__content { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: space-between; | ||
| width: 100%; | ||
| gap: pxToRem(16px); | ||
| max-width: 100%; | ||
| margin: 0 auto; | ||
| } | ||
|
|
||
| &__text { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: pxToRem(8px); | ||
| flex: 1; | ||
| } | ||
|
|
||
| &__title { | ||
| color: var(--sds-clr-base-01); | ||
| margin: 0; | ||
| font-size: pxToRem(14px); | ||
| } | ||
|
|
||
| &__subtitle { | ||
| color: var(--sds-clr-gray-11); | ||
| margin: 0; | ||
| font-size: pxToRem(12px); | ||
| } | ||
|
|
||
| &__logo { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| flex-shrink: 0; | ||
|
|
||
| img { | ||
| width: pxToRem(40px); | ||
| height: pxToRem(40px); | ||
| object-fit: contain; | ||
| } | ||
| } | ||
|
|
||
| &__dismiss { | ||
| position: absolute; | ||
| top: pxToRem(4px); | ||
| right: pxToRem(4px); | ||
| background: none; | ||
| border: none; | ||
| cursor: pointer; | ||
| padding: pxToRem(4px); | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| color: var(--sds-clr-base-01); | ||
| border-radius: pxToRem(16px); | ||
| z-index: 10; | ||
| pointer-events: auto; | ||
|
|
||
| &:hover { | ||
| opacity: 1; | ||
| background: rgba(255, 255, 255, 0.1); | ||
| } | ||
|
|
||
| svg { | ||
| width: pxToRem(16px); | ||
| height: pxToRem(16px); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can actually store this in
/extension/src/constants/localStorageTypes. Then, you can reuse this const ingetMobileAppBannerDismissed, as well