Skip to content
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

feat: sell and fund flow [LIVE-784] #91

Merged
merged 9 commits into from
Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -23,6 +23,7 @@ import {
renderInWrongAppForAccount,
renderError,
renderBootloaderStep,
renderExchange,
renderConfirmSwap,
renderConfirmSell,
LoadingAppInstall,
Expand Down Expand Up @@ -81,6 +82,9 @@ export default function DeviceAction<R, H, P>({
initSwapResult,
signMessageRequested,
allowOpeningGranted,
completeExchangeStarted,
completeExchangeResult,
completeExchangeError,
initSellRequested,
initSellResult,
initSellError,
Expand Down Expand Up @@ -170,6 +174,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, device: selectedDevice, colors, theme });
}
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 @@ -473,6 +473,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 @@ -38,6 +38,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 @@ -341,6 +342,11 @@ export default function BaseNavigator() {
}
options={{ headerShown: false }}
/>
<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