Skip to content

Commit

Permalink
feat: add desktop download URL and instructions (#1233)
Browse files Browse the repository at this point in the history
* feat: add desktop download URL and instructions

- add a downloadUrls.desktop property
- add a wallet.desktop.instructions property
- set these on the Ledger connector to present desktop and mobile
  app download buttons for Ledger Live

* fix: getWalletConnectUri in qrCode getUri

* feat: platform detection util

* feat: platforms util type enum

* feat: platform specific desktop download urls

* feat: ledger connector download urls and learn mores

* chore: amend changeset

* feat: connect instruction step icon

* feat: desktop flow learn more button

* revert: mobile action label

* feat: platform label in desktop download details

* feat: desktop connect details platform icons

* fix: rename it Ledger

* fix: linting

* fix: dependency

* fix: i18n string for mobile and desktop wallet

* fix: i18n connector strings

* chore: update changeset

* chore: update macos icon to sonoma

* fix: missing i18n strings

---------

Co-authored-by: Daniel Sinclair <d@niel.nyc>
  • Loading branch information
hlopes-ledger and DanielSinclair committed Oct 30, 2023
1 parent 9b567f9 commit ef64a22
Show file tree
Hide file tree
Showing 17 changed files with 457 additions and 36 deletions.
43 changes: 43 additions & 0 deletions .changeset/wise-doors-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
'@rainbow-me/rainbowkit': minor
---

**Improved desktop wallet download support**

RainbowKit wallet connectors now support desktop download links and desktop
wallet instructions.

Dapps that utilize the Custom Wallets API can reference the updated docs [here](https://www.rainbowkit.com/docs/custom-wallets).

```ts
{
downloadUrls: {
windows: 'https://my-wallet/windows-app',
macos: 'https://my-wallet/macos-app',
linux: 'https://my-wallet/linux-app',
desktop: 'https://my-wallet/desktop-app',
}
}
```

We've also introduced a new 'connect' `InstructionStepName` type in the `instructions` API to provide wallet connection instructions.

```ts
return {
connector,
desktop: {
getUri,
instructions: {
learnMoreUrl: 'https://my-wallet/learn-more',
steps: [
// ...
{
description: 'A prompt will appear for you to approve the connection to My Wallet.'
step: 'connect',
title: 'Connect',
}
]
},
},
}
```
6 changes: 4 additions & 2 deletions packages/rainbowkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
"vitest": "^0.33.0",
"wagmi": "~1.4.3",
"@wagmi/core": "~1.4.3",
"@types/i18n-js": "^3.8.5"
"@types/i18n-js": "^3.8.5",
"@types/ua-parser-js": "^0.7.36"
},
"dependencies": {
"@vanilla-extract/css": "1.9.1",
Expand All @@ -72,7 +73,8 @@
"clsx": "1.1.1",
"qrcode": "1.5.0",
"react-remove-scroll": "2.5.4",
"i18n-js": "^4.3.2"
"i18n-js": "^4.3.2",
"ua-parser-js": "^1.0.35"
},
"repository": {
"type": "git",
Expand Down
153 changes: 150 additions & 3 deletions packages/rainbowkit/src/components/ConnectOptions/ConnectDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { touchableStyles } from '../../css/touchableStyles';
import { useWindowSize } from '../../hooks/useWindowSize';
import { BrowserType, getBrowser, isSafari } from '../../utils/browsers';
import { getGradientRGBAs } from '../../utils/colors';
import { PlatformType, getPlatform } from '../../utils/platforms';
import { InstructionStepName } from '../../wallets/Wallet';
import {
WalletConnector,
Expand All @@ -12,6 +13,7 @@ import { AsyncImage } from '../AsyncImage/AsyncImage';
import { loadImages } from '../AsyncImage/useAsyncImage';
import { Box, BoxProps } from '../Box/Box';
import { ActionButton } from '../Button/ActionButton';
import { ConnectIcon, preloadConnectIcon } from '../Icons/Connect';
import { CreateIcon, preloadCreateIcon } from '../Icons/Create';
import { RefreshIcon, preloadRefreshIcon } from '../Icons/Refresh';
import { ScanIcon, preloadScanIcon } from '../Icons/Scan';
Expand Down Expand Up @@ -46,6 +48,22 @@ const getBrowserSrc: () => Promise<string> = async () => {

const preloadBrowserIcon = () => loadImages(getBrowserSrc);

const getPlatformSrc: () => Promise<string> = async () => {
const platform = getPlatform();
switch (platform) {
case PlatformType.Windows:
return (await import('../Icons/Windows.svg')).default;
case PlatformType.MacOS:
return (await import('../Icons/Macos.svg')).default;
case PlatformType.Linux:
return (await import('../Icons/Linux.svg')).default;
default:
return (await import('../Icons/Linux.svg')).default;
}
};

const preloadPlatformIcon = () => loadImages(getPlatformSrc);

export function GetDetail({
getWalletDownload,
compactModeEnabled,
Expand Down Expand Up @@ -79,6 +97,7 @@ export function GetDetail({
?.filter(
(wallet) =>
wallet.extensionDownloadUrl ||
wallet.desktopDownloadUrl ||
(wallet.qrCode && wallet.downloadUrls?.qrCode),
)
.map((wallet) => {
Expand All @@ -87,6 +106,8 @@ export function GetDetail({
const hasMobileCompanionApp = downloadUrls?.qrCode && qrCode;
const hasExtension = !!wallet.extensionDownloadUrl;
const hasMobileAndExtension = downloadUrls?.qrCode && hasExtension;
const hasMobileAndDesktop =
downloadUrls?.qrCode && !!wallet.desktopDownloadUrl;

return (
<Box
Expand Down Expand Up @@ -118,6 +139,8 @@ export function GetDetail({
<Text color="modalTextSecondary" size="14" weight="medium">
{hasMobileAndExtension
? i18n.t('get.mobile_and_extension.description')
: hasMobileAndDesktop
? i18n.t('get.mobile_and_desktop.description')
: hasMobileCompanionApp
? i18n.t('get.mobile.description')
: hasExtension
Expand Down Expand Up @@ -195,6 +218,8 @@ export function ConnectDetail({

const hasExtension = !!wallet.extensionDownloadUrl;
const hasQrCodeAndExtension = downloadUrls?.qrCode && hasExtension;
const hasQrCodeAndDesktop =
downloadUrls?.qrCode && !!wallet.desktopDownloadUrl;
const hasQrCode = qrCode && qrCodeUri;

const secondaryAction: {
Expand All @@ -221,7 +246,7 @@ export function ConnectDetail({
label: i18n.t('connect.secondary_action.get.label'),
onClick: () =>
changeWalletStep(
hasQrCodeAndExtension
hasQrCodeAndExtension || hasQrCodeAndDesktop
? WalletStep.DownloadOptions
: WalletStep.Download,
),
Expand All @@ -234,6 +259,7 @@ export function ConnectDetail({
useEffect(() => {
// Preload icon used on next screen
preloadBrowserIcon();
preloadPlatformIcon();
}, []);

return (
Expand Down Expand Up @@ -404,7 +430,7 @@ const DownloadOptionsBox = ({
isCompact: boolean;
iconUrl: string | (() => Promise<string>);
iconBackground?: string;
variant: 'browser' | 'app';
variant: 'browser' | 'app' | 'desktop';
}) => {
const isBrowserCard = variant === 'browser';
const gradientRgbas =
Expand Down Expand Up @@ -588,9 +614,16 @@ export function DownloadOptionsDetail({
wallet: WalletConnector;
}) {
const browser = getBrowser();
const platform = getPlatform();
const modalSize = useContext(ModalSizeContext);
const isCompact = modalSize === 'compact';
const { extension, extensionDownloadUrl, mobileDownloadUrl } = wallet;
const {
desktop,
desktopDownloadUrl,
extension,
extensionDownloadUrl,
mobileDownloadUrl,
} = wallet;

const i18n = useContext(I18nContext);

Expand All @@ -599,6 +632,7 @@ export function DownloadOptionsDetail({
preloadCreateIcon();
preloadScanIcon();
preloadRefreshIcon();
preloadConnectIcon();
}, []);

return (
Expand Down Expand Up @@ -644,6 +678,29 @@ export function DownloadOptionsDetail({
variant="browser"
/>
)}
{desktopDownloadUrl && (
<DownloadOptionsBox
actionLabel={i18n.t('get_options.desktop.download.label', {
platform,
})}
description={i18n.t('get_options.desktop.description')}
iconUrl={getPlatformSrc}
isCompact={isCompact}
onAction={() =>
changeWalletStep(
desktop?.instructions
? WalletStep.InstructionsDesktop
: WalletStep.Connect,
)
}
title={i18n.t('get_options.desktop.title', {
wallet: wallet.name,
platform,
})}
url={desktopDownloadUrl}
variant="desktop"
/>
)}
{mobileDownloadUrl && (
<DownloadOptionsBox
actionLabel={i18n.t('get_options.mobile.download.label', {
Expand Down Expand Up @@ -733,6 +790,7 @@ const stepIcons: Record<
InstructionStepName,
(wallet: WalletConnector) => ReactNode
> = {
connect: () => <ConnectIcon />,
create: () => <CreateIcon />,
install: (wallet) => (
<AsyncImage
Expand Down Expand Up @@ -923,3 +981,92 @@ export function InstructionExtensionDetail({
</Box>
);
}

export function InstructionDesktopDetail({
connectWallet,
wallet,
}: {
connectWallet: (wallet: WalletConnector) => void;
wallet: WalletConnector;
}) {
const i18n = useContext(I18nContext);

return (
<Box
alignItems="center"
display="flex"
flexDirection="column"
height="full"
width="full"
>
<Box
display="flex"
flexDirection="column"
gap="28"
height="full"
justifyContent="center"
paddingY="32"
style={{ maxWidth: 320 }}
>
{wallet?.desktop?.instructions?.steps.map((d, idx) => (
<Box
alignItems="center"
display="flex"
flexDirection="row"
gap="16"
key={idx}
>
<Box
borderRadius="10"
height="48"
minWidth="48"
overflow="hidden"
position="relative"
width="48"
>
{stepIcons[d.step]?.(wallet)}
</Box>
<Box display="flex" flexDirection="column" gap="4">
<Text color="modalText" size="14" weight="bold">
{i18n.t(d.title)}
</Text>
<Text color="modalTextSecondary" size="14" weight="medium">
{i18n.t(d.description)}
</Text>
</Box>
</Box>
))}
</Box>

<Box
alignItems="center"
display="flex"
flexDirection="column"
gap="12"
justifyContent="center"
marginBottom="16"
>
<ActionButton
label={i18n.t('get_instructions.desktop.connect.label')}
onClick={() => connectWallet(wallet)}
/>
<Box
as="a"
className={touchableStyles({ active: 'shrink', hover: 'grow' })}
display="block"
href={wallet?.desktop?.instructions?.learnMoreUrl}
paddingX="12"
paddingY="4"
rel="noreferrer"
style={{ willChange: 'transform' }}
target="_blank"
transition="default"
>
<Text color="accentColor" size="14" weight="bold">
{i18n.t('get_instructions.desktop.learn_more.label')}
</Text>
</Box>
</Box>
</Box>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
DownloadDetail,
DownloadOptionsDetail,
GetDetail,
InstructionDesktopDetail,
InstructionExtensionDetail,
InstructionMobileDetail,
} from './ConnectDetails';
Expand All @@ -44,6 +45,7 @@ export enum WalletStep {
DownloadOptions = 'DOWNLOAD_OPTIONS',
Download = 'DOWNLOAD',
InstructionsMobile = 'INSTRUCTIONS_MOBILE',
InstructionsDesktop = 'INSTRUCTIONS_DESKTOP',
InstructionsExtension = 'INSTRUCTIONS_EXTENSION',
}

Expand Down Expand Up @@ -154,12 +156,15 @@ export function DesktopOptions({ onClose }: { onClose: () => void }) {
setSelectedOptionId(id);
const sWallet = wallets.find((w) => id === w.id);
const isMobile = sWallet?.downloadUrls?.qrCode;
const isDesktop = !!sWallet?.desktopDownloadUrl;
const isExtension = !!sWallet?.extensionDownloadUrl;
setSelectedWallet(sWallet);
if (isMobile && isExtension) {
if (isMobile && (isExtension || isDesktop)) {
changeWalletStep(WalletStep.DownloadOptions);
} else if (isMobile) {
changeWalletStep(WalletStep.Download);
} else if (isDesktop) {
changeWalletStep(WalletStep.InstructionsDesktop);
} else {
changeWalletStep(WalletStep.InstructionsExtension);
}
Expand Down Expand Up @@ -312,6 +317,22 @@ export function DesktopOptions({ onClose }: { onClose: () => void }) {
});
headerBackButtonLink = WalletStep.DownloadOptions;
break;
case WalletStep.InstructionsDesktop:
walletContent = selectedWallet && (
<InstructionDesktopDetail
connectWallet={selectWallet}
wallet={selectedWallet}
/>
);
headerLabel =
selectedWallet &&
i18n.t('get_options.title', {
wallet: compactModeEnabled
? selectedWallet.shortName || selectedWallet.name
: selectedWallet.name,
});
headerBackButtonLink = WalletStep.DownloadOptions;
break;
default:
break;
}
Expand Down
18 changes: 18 additions & 0 deletions packages/rainbowkit/src/components/Icons/Connect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { AsyncImage } from '../AsyncImage/AsyncImage';
import { loadImages } from '../AsyncImage/useAsyncImage';

const src = async () => (await import('./connect.svg')).default;

export const preloadConnectIcon = () => loadImages(src);

export const ConnectIcon = () => (
<AsyncImage
background="#515a70"
borderColor="generalBorder"
borderRadius="10"
height="48"
src={src}
width="48"
/>
);
1 change: 1 addition & 0 deletions packages/rainbowkit/src/components/Icons/Linux.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/rainbowkit/src/components/Icons/Macos.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/rainbowkit/src/components/Icons/Windows.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

2 comments on commit ef64a22

@vercel
Copy link

@vercel vercel bot commented on ef64a22 Oct 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on ef64a22 Oct 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.