diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts
index 6ed34ec..c624182 100644
--- a/src/constants/dialogs.ts
+++ b/src/constants/dialogs.ts
@@ -11,6 +11,7 @@ export enum DialogTypes {
ExternalNavKeplr = 'ExternalNavKeplr',
MnemonicExport = 'MnemonicExport',
MobileSignIn = 'MobileSignIn',
+ MobileDownload = 'MobileDownload',
Onboarding = 'Onboarding',
OrderDetails = 'OrderDetails',
Preferences = 'Preferences',
diff --git a/src/layout/DialogManager.tsx b/src/layout/DialogManager.tsx
index 4c5ec7e..cef213e 100644
--- a/src/layout/DialogManager.tsx
+++ b/src/layout/DialogManager.tsx
@@ -31,6 +31,7 @@ import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDial
import { NewMarketMessageDetailsDialog } from '@/views/dialogs/NewMarketMessageDetailsDialog';
import { NewMarketAgreementDialog } from '@/views/dialogs/NewMarketAgreementDialog';
import { ExternalNavStrideDialog } from '@/views/dialogs/ExternalNavStrideDialog';
+import { MobileDownloadDialog } from '@/views/dialogs/MobileDownloadDialog';
export const DialogManager = () => {
const dispatch = useDispatch();
@@ -63,6 +64,7 @@ export const DialogManager = () => {
[DialogTypes.ExternalNavStride]: ,
[DialogTypes.MnemonicExport]: ,
[DialogTypes.MobileSignIn]: ,
+ [DialogTypes.MobileDownload]: ,
[DialogTypes.Onboarding]: ,
[DialogTypes.OrderDetails]: ,
[DialogTypes.Preferences]: ,
diff --git a/src/views/dialogs/MobileDownloadDialog.tsx b/src/views/dialogs/MobileDownloadDialog.tsx
new file mode 100644
index 0000000..6a4aff1
--- /dev/null
+++ b/src/views/dialogs/MobileDownloadDialog.tsx
@@ -0,0 +1,140 @@
+import styled, { AnyStyledComponent, css } from 'styled-components';
+
+import { layoutMixins } from '@/styles/layoutMixins';
+
+import { Dialog } from '@/components/Dialog';
+import { QrCode } from '@/components/QrCode';
+import { useStringGetter } from '@/hooks';
+import { STRING_KEYS } from '@/constants/localization';
+
+type ElementProps = {
+ setIsOpen: (open: boolean) => void;
+};
+
+/*
+ When/if deployer deploys the web app with smartbanner, "smartbanner:button-url-apple" and/or
+ "smartbanner:button-url-google" are set.
+ This implementation assumes "smartbanner:button-url-apple" and "smartbanner:button-url-google"
+ are set to the same value with onelink or other redirect URL.
+ Since there is no way for the desktop web app to know what mobile device the user is using,
+ we should give a onelink URL which redirects to either iOS or Android app store depending on
+ the mobile device used to scan the link.
+*/
+
+// for testing only
+// export const mobileAppUrl = "http://example.com";
+
+let mobileAppUrl: string | undefined | null = undefined;
+
+export const getMobileAppUrl = () => {
+ if (!mobileAppUrl) {
+ mobileAppUrl =
+ // for testing to verify is retrieved by name, QR code should show "@dYdX" as value
+ // document.querySelector('meta[name="twitter:creator"]')?.getAttribute('content') ??
+ document.querySelector('meta[name="smartbanner:button-url-apple"]')?.getAttribute('content') ??
+ document.querySelector('meta[name="smartbanner:button-url-google"]')?.getAttribute('content');
+ }
+ return mobileAppUrl;
+}
+
+const MobileQrCode = ({
+ url,
+}: {
+ url: string;
+}) => {
+ return (
+
+
+
+ );
+};
+
+/*
+MobileDownloadDialog should only been shown on desktop when mobileAppUrl has value. That's controlled by AccountMenu.tsx.
+*/
+
+export const MobileDownloadDialog = ({ setIsOpen }: ElementProps) => {
+ const stringGetter = useStringGetter();
+ const content = (
+
+ );
+
+ return (
+
+ );
+};
+
+const Styled: Record = {};
+
+Styled.Content = styled.div`
+ ${layoutMixins.column}
+ gap: 1rem;
+
+ strong {
+ font-weight: 900;
+ color: var(--color-text-2);
+ }
+
+ footer {
+ ${layoutMixins.row}
+ justify-content: space-between;
+
+ svg {
+ width: auto;
+ }
+ }
+`;
+
+Styled.WaitingSpan = styled.span`
+ strong {
+ color: var(--color-warning);
+ }
+`;
+
+Styled.QrCodeContainer = styled.figure<{ isShowing: boolean }>`
+ ${layoutMixins.stack}
+
+ overflow: hidden;
+ border-radius: 0.75em;
+
+ cursor: pointer;
+
+ transition: 0.2s;
+
+ &:hover {
+ filter: brightness(var(--hover-filter-base));
+ }
+
+ > * {
+ position: relative;
+ transition: 0.16s;
+ }
+
+ > :first-child {
+ pointer-events: none;
+
+ ${({ isShowing }) =>
+ !isShowing &&
+ css`
+ filter: blur(1rem) brightness(1.4);
+ will-change: filter;
+ `}
+ }
+
+ > span {
+ place-self: center;
+
+ font-size: 1.4em;
+ color: var(--color-text-2);
+
+ ${({ isShowing }) =>
+ isShowing &&
+ css`
+ opacity: 0;
+ `}
+ }
+`;
diff --git a/src/views/menus/AccountMenu.tsx b/src/views/menus/AccountMenu.tsx
index f40b2b4..5dd5732 100644
--- a/src/views/menus/AccountMenu.tsx
+++ b/src/views/menus/AccountMenu.tsx
@@ -40,6 +40,7 @@ import { getAppTheme } from '@/state/configsSelectors';
import { isTruthy } from '@/lib/isTruthy';
import { truncateAddress } from '@/lib/wallet';
import { MustBigNumber } from '@/lib/numbers';
+import { getMobileAppUrl } from '../dialogs/MobileDownloadDialog';
export const AccountMenu = () => {
const stringGetter = useStringGetter();
@@ -189,6 +190,18 @@ export const AccountMenu = () => {
label: stringGetter({ key: STRING_KEYS.DISPLAY_SETTINGS }),
onSelect: () => dispatch(openDialog({ type: DialogTypes.DisplaySettings })),
},
+ ...(getMobileAppUrl()
+ ? [
+ {
+ value: 'MobileDownload',
+ icon: ,
+ label: stringGetter({ key: STRING_KEYS.DOWNLOAD_MOBILE_APP }),
+ onSelect: () => {
+ dispatch(openDialog({ type: DialogTypes.MobileDownload }));
+ },
+ },
+ ]
+ : []),
...(onboardingState === OnboardingState.AccountConnected && hdKey
? [
{