Skip to content

Commit

Permalink
Merge pull request #91 from LedgerHQ/feat/LL-8098
Browse files Browse the repository at this point in the history
feat: sell and fund flow [LIVE-784]
  • Loading branch information
Justkant committed Jun 15, 2022
2 parents 2d26dcd + bf12e0f commit 25b9e29
Show file tree
Hide file tree
Showing 16 changed files with 530 additions and 21 deletions.
6 changes: 6 additions & 0 deletions .changeset/large-snakes-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"live-mobile": minor
"@ledgerhq/live-common": patch
---

feat: sell and fund flow [LIVE-784]
18 changes: 18 additions & 0 deletions apps/ledger-live-mobile/src/components/DeviceAction/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
renderInWrongAppForAccount,
renderError,
renderBootloaderStep,
renderExchange,
renderConfirmSwap,
renderConfirmSell,
LoadingAppInstall,
Expand Down Expand Up @@ -86,6 +87,9 @@ export default function DeviceAction<R, H, P>({
initSwapResult,
signMessageRequested,
allowOpeningGranted,
completeExchangeStarted,
completeExchangeResult,
completeExchangeError,
initSellRequested,
initSellResult,
initSellError,
Expand Down Expand Up @@ -188,6 +192,20 @@ export default function DeviceAction<R, H, P>({
});
}

if (
completeExchangeStarted &&
!completeExchangeResult &&
!completeExchangeError
) {
return renderExchange({
// $FlowFixMe
exchangeType: request?.exchangeType,
t,
device,
theme,
});
}

if (initSwapRequested && !initSwapResult && !initSwapError) {
return renderConfirmSwap({
t,
Expand Down
49 changes: 49 additions & 0 deletions apps/ledger-live-mobile/src/components/DeviceAction/rendering.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,55 @@ export function renderLoading({
);
}

export function renderExchange({
exchangeType,
t,
device,
theme,
}: RawProps & {
exchangeType: number;
device: Device;
}) {
switch (exchangeType) {
case 0x00: // swap
return <div>{"Confirm swap on your device"}</div>;
case 0x01: // sell
case 0x02: // fund
return renderSecureTransferDeviceConfirmation({
exchangeTypeName: exchangeType === 0x01 ? "confirmSell" : "confirmFund",
t,
device,
theme,
});
default:
return <CenteredText>{"Confirm exchange on your device"}</CenteredText>;
}
}

export function renderSecureTransferDeviceConfirmation({
t,
exchangeTypeName,
device,
theme,
}: RawProps & {
exchangeTypeName: string;
device: Device;
}) {
return (
<Wrapper>
<AnimationContainer>
<Animation
source={getDeviceAnimation({ device, key: "validate", theme })}
/>
</AnimationContainer>
<TitleText>{t(`DeviceAction.${exchangeTypeName}.title`)}</TitleText>
<Alert type="primary" learnMoreUrl={urls.swap.learnMore}>
{t(`DeviceAction.${exchangeTypeName}.alert`)}
</Alert>
</Wrapper>
);
}

export function LoadingAppInstall({
analyticsPropertyFlow = "unknown",
request,
Expand Down
26 changes: 14 additions & 12 deletions apps/ledger-live-mobile/src/components/DeviceActionModal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { useState, useCallback } from "react";
import { Device } from "@ledgerhq/live-common/lib/hw/actions/types";
import { SyncSkipUnderPriority } from "@ledgerhq/live-common/lib/bridge/react";
import styled from "styled-components/native";
import { Device } from "@ledgerhq/live-common/lib/hw/actions/types";
import { Alert, Flex } from "@ledgerhq/native-ui";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import DeviceAction from "./DeviceAction";
import styled from "styled-components/native";
import BottomModal from "./BottomModal";
import DeviceAction from "./DeviceAction";

const DeviceActionContainer = styled(Flex).attrs({
flexDirection: "row",
Expand Down Expand Up @@ -40,18 +40,20 @@ export default function DeviceActionModal({
const showAlert = !device?.wired;
const [result, setResult] = useState<any | null>(null);

const handleModalHide = useCallback(() => {
if (onModalHide) onModalHide();
if (onResult && result) {
onResult(result);
setResult(null);
}
}, [onModalHide, onResult, result]);

return (
<BottomModal
id="DeviceActionModal"
isOpened={result ? false : !!device}
onClose={onClose}
onModalHide={() => {
if (onModalHide) onModalHide();
if (onResult && result) {
onResult(result);
setResult(null);
}
}}
onClose={result ? undefined : onClose}
onModalHide={handleModalHide}
>
{onResult && result
? null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import AddAccountsNavigator from "./AddAccountsNavigator";
import ExchangeBuyFlowNavigator from "./ExchangeBuyFlowNavigator";
import ExchangeSellFlowNavigator from "./ExchangeSellFlowNavigator";
import ExchangeNavigator from "./ExchangeNavigator";
import PlatformExchangeNavigator from "./PlatformExchangeNavigator";
import FirmwareUpdateNavigator from "./FirmwareUpdateNavigator";
import AccountSettingsNavigator from "./AccountSettingsNavigator";
import ImportAccountsNavigator from "./ImportAccountsNavigator";
Expand Down Expand Up @@ -346,6 +347,11 @@ export default function BaseNavigator() {
options={{ headerShown: false }}
{...noNanoBuyNanoWallScreenOptions}
/>
<Stack.Screen
name={NavigatorName.PlatformExchange}
component={PlatformExchangeNavigator}
options={{ headerShown: false }}
/>
<Stack.Screen
name={ScreenName.OperationDetails}
component={OperationDetails}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useMemo } from "react";
import { useTheme } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import { useTranslation } from "react-i18next";
import { getStackNavigatorConfig } from "../../navigation/navigatorConfig";
import styles from "../../navigation/styles";
import { ScreenName } from "../../const";
import PlatformStartExchange from "../../screens/Platform/exchange/StartExchange";
import PlatformCompleteExchange from "../../screens/Platform/exchange/CompleteExchange";

export default function PlatformExchangeNavigator() {
const { t } = useTranslation();
const { colors } = useTheme();
const stackNavigationConfig = useMemo(
() => getStackNavigatorConfig(colors, true),
[colors],
);

return (
<Stack.Navigator
screenOptions={{ ...stackNavigationConfig, headerShown: false }}
>
<Stack.Screen
name={ScreenName.PlatformStartExchange}
component={PlatformStartExchange}
options={{
headerStyle: styles.headerNoShadow,
title: t("transfer.swap.landing.header"),
}}
/>
<Stack.Screen
name={ScreenName.PlatformCompleteExchange}
component={PlatformCompleteExchange}
options={{
headerStyle: styles.headerNoShadow,
title: t("transfer.swap.landing.header"),
}}
/>
</Stack.Navigator>
);
}

const Stack = createStackNavigator();
150 changes: 149 additions & 1 deletion apps/ledger-live-mobile/src/components/WebPlatformPlayer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ import type {

import { getEnv } from "@ledgerhq/live-common/lib/env";
import { getAccountBridge } from "@ledgerhq/live-common/lib/bridge";
import { getMainAccount } from "@ledgerhq/live-common/lib/account";
import type { Device } from "@ledgerhq/live-common/lib/hw/actions/types";
import {
listCryptoCurrencies,
findCryptoCurrencyById,
} from "@ledgerhq/live-common/lib/currencies/index";
} from "@ledgerhq/live-common/lib/currencies";
import type { AppManifest } from "@ledgerhq/live-common/lib/platform/types";

import { useJSONRPCServer } from "@ledgerhq/live-common/lib/platform/JSONRPCServer";
Expand Down Expand Up @@ -113,6 +115,8 @@ const WebPlatformPlayer = ({ manifest, inputs }: Props) => {
const [widgetLoaded, setWidgetLoaded] = useState(false);
const [isInfoPanelOpened, setIsInfoPanelOpened] = useState(false);

const [device, setDevice] = useState();

const uri = useMemo(() => {
const url = new URL(manifest.url);

Expand Down Expand Up @@ -398,6 +402,146 @@ const WebPlatformPlayer = ({ manifest, inputs }: Props) => {
[manifest, accounts],
);

const startExchange = useCallback(
({ exchangeType }: { exchangeType: number }) => {
tracking.platformStartExchangeRequested(manifest);

return new Promise((resolve, reject) => {
navigation.navigate(NavigatorName.PlatformExchange, {
screen: ScreenName.PlatformStartExchange,
params: {
request: {
exchangeType,
},
onResult: (result: {
startExchangeResult?: number,
startExchangeError?: Error,
device: Device,
}) => {
if (result.startExchangeError) {
tracking.platformStartExchangeFail(manifest);
reject(result.startExchangeError);
}

if (result.startExchangeResult) {
tracking.platformStartExchangeSuccess(manifest);
setDevice(result.device);
resolve(result.startExchangeResult);
}

const n = navigation.getParent() || navigation;
n.pop();
},
},
});
});
},
[manifest, navigation],
);

const completeExchange = useCallback(
({
provider,
fromAccountId,
toAccountId,
transaction,
binaryPayload,
signature,
feesStrategy,
exchangeType,
}: {
provider: string,
fromAccountId: string,
toAccountId: string,
transaction: RawPlatformTransaction,
binaryPayload: string,
signature: string,
feesStrategy: string,
exchangeType: number,
}) => {
// Nb get a hold of the actual accounts, and parent accounts
const fromAccount = accounts.find(a => a.id === fromAccountId);
let fromParentAccount;

const toAccount = accounts.find(a => a.id === toAccountId);
let toParentAccount;

if (!fromAccount) {
return null;
}

if (exchangeType === 0x00 && !toAccount) {
// if we do a swap, a destination account must be provided
return null;
}

if (fromAccount.type === "TokenAccount") {
fromParentAccount = accounts.find(a => a.id === fromAccount.parentId);
}
if (toAccount && toAccount.type === "TokenAccount") {
toParentAccount = accounts.find(a => a.id === toAccount.parentId);
}

const accountBridge = getAccountBridge(fromAccount, fromParentAccount);
const mainFromAccount = getMainAccount(fromAccount, fromParentAccount);

// eslint-disable-next-line no-param-reassign
transaction.family = mainFromAccount.currency.family;

const platformTransaction = deserializePlatformTransaction(transaction);

platformTransaction.feesStrategy = feesStrategy;

let processedTransaction = accountBridge.createTransaction(
mainFromAccount,
);
processedTransaction = accountBridge.updateTransaction(
processedTransaction,
platformTransaction,
);

tracking.platformCompleteExchangeRequested(manifest);
return new Promise((resolve, reject) => {
navigation.navigate(NavigatorName.PlatformExchange, {
screen: ScreenName.PlatformCompleteExchange,
params: {
request: {
exchangeType,
provider,
exchange: {
fromAccount,
fromParentAccount,
toAccount,
toParentAccount,
},
transaction: processedTransaction,
binaryPayload,
signature,
feesStrategy,
},
device,
onResult: (result: { operation?: Operation, error?: Error }) => {
if (result.error) {
tracking.platformStartExchangeFail(manifest);
reject(result.error);
}

if (result.operation) {
tracking.platformStartExchangeSuccess(manifest);
resolve(result.operation);
}

setDevice();
const n = navigation.getParent() || navigation;
n.pop();
},
},
});
});
},
[accounts, manifest, navigation, device],
);

const handlers = useMemo(
() => ({
"account.list": listAccounts,
Expand All @@ -406,6 +550,8 @@ const WebPlatformPlayer = ({ manifest, inputs }: Props) => {
"account.receive": receiveOnAccount,
"transaction.sign": signTransaction,
"transaction.broadcast": broadcastTransaction,
"exchange.start": startExchange,
"exchange.complete": completeExchange,
}),
[
listAccounts,
Expand All @@ -414,6 +560,8 @@ const WebPlatformPlayer = ({ manifest, inputs }: Props) => {
receiveOnAccount,
signTransaction,
broadcastTransaction,
startExchange,
completeExchange,
],
);

Expand Down
Loading

0 comments on commit 25b9e29

Please sign in to comment.