From 7c7fe2db9762c1227f619bf8385db49201e1e822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JOSE=20MOISES=20NU=C3=91EZ=20IGLESIAS?= Date: Tue, 20 May 2025 14:20:28 -0600 Subject: [PATCH 1/9] feat(ui): redesign lnurl payment success screen --- .../send-bitcoin-completed-screen.spec.tsx | 104 ++++++--- .../success-action/field-with-copy.props.tsx | 5 - .../success-action/field-with-copy.tsx | 67 ------ .../success-action/field-with-icon.props.tsx | 7 + .../success-action/field-with-icon.tsx | 139 ++++++++++++ .../success-action/success-action.props.tsx | 8 +- .../success-action/success-action.tsx | 126 ++--------- app/graphql/generated.gql | 30 +++ app/graphql/generated.ts | 41 +++- app/i18n/en/index.ts | 5 + app/i18n/i18n-types.ts | 40 ++++ app/i18n/raw-i18n/source/en.json | 7 +- app/navigation/root-navigator.tsx | 2 +- app/navigation/stack-param-lists.ts | 5 + .../payment-details/index.types.ts | 2 + .../payment-details/intraledger.ts | 2 + .../payment-details/lightning.ts | 3 + .../payment-details/onchain.ts | 4 + .../send-bitcoin-completed-screen.tsx | 204 +++++++++++++----- .../send-bitcoin-confirmation-screen.tsx | 24 ++- .../send-bitcoin-screen/use-send-payment.ts | 29 ++- app/utils/date.ts | 11 + app/utils/helper.ts | 21 ++ 23 files changed, 602 insertions(+), 284 deletions(-) delete mode 100644 app/components/success-action/field-with-copy.props.tsx delete mode 100644 app/components/success-action/field-with-copy.tsx create mode 100644 app/components/success-action/field-with-icon.props.tsx create mode 100644 app/components/success-action/field-with-icon.tsx diff --git a/__tests__/screens/send-bitcoin-completed-screen.spec.tsx b/__tests__/screens/send-bitcoin-completed-screen.spec.tsx index 99b9d65a70..51d6dcc118 100644 --- a/__tests__/screens/send-bitcoin-completed-screen.spec.tsx +++ b/__tests__/screens/send-bitcoin-completed-screen.spec.tsx @@ -1,5 +1,5 @@ import React from "react" -import { fireEvent, render, screen, waitFor } from "@testing-library/react-native" +import { fireEvent, render, screen, waitFor, within } from "@testing-library/react-native" import { loadLocale } from "@app/i18n/i18n-util.sync" import { i18nObject } from "@app/i18n/i18n-util" import { @@ -32,9 +32,7 @@ describe("SendBitcoinCompletedScreen", () => { ) const successTextElement = await waitFor(() => screen.findByTestId("Success Text")) - expect(successTextElement.props.children).toContain( - "Payment has been sent successfully", - ) + expect(within(successTextElement).getByTestId("SUCCESS")).toBeTruthy() }) it("renders the Queued state correctly", async () => { @@ -45,9 +43,7 @@ describe("SendBitcoinCompletedScreen", () => { ) const queuedTextElement = await waitFor(() => screen.findByTestId("Success Text")) - expect(queuedTextElement.props.children).toEqual( - expect.stringContaining("Your transaction is queued"), - ) + expect(within(queuedTextElement).getByTestId("QUEUED")).toBeTruthy() }) it("renders the Pending state correctly", async () => { @@ -58,9 +54,7 @@ describe("SendBitcoinCompletedScreen", () => { ) const pendingTextElement = await waitFor(() => screen.findByTestId("Success Text")) - expect(pendingTextElement.props.children).toEqual( - expect.stringContaining("The payment has been sent"), - ) + expect(within(pendingTextElement).getByTestId("PENDING")).toBeTruthy() }) it("render successAction - LUD 09 - message", async () => { @@ -78,6 +72,11 @@ describe("SendBitcoinCompletedScreen", () => { iv: null, decipher: () => null, }, + formatAmount: "$0.03 (25 SAT)", + feeDisplayText: "$0.00 (0 SAT)", + destination: "moises", + paymentType: "lightning", + createdAt: 1747691078, }, } as const @@ -88,7 +87,15 @@ describe("SendBitcoinCompletedScreen", () => { ) expect(screen.getByText(lud09MessageRoute.params.successAction.message)).toBeTruthy() - expect(screen.getByText(LL.SendBitcoinScreen.note())).toBeTruthy() + expect(screen.getByText(lud09MessageRoute.params.formatAmount)).toBeTruthy() + expect( + screen.getByText( + `${lud09MessageRoute.params.feeDisplayText} | ${lud09MessageRoute.params.paymentType}`, + ), + ).toBeTruthy() + expect(screen.getByText(lud09MessageRoute.params.destination)).toBeTruthy() + expect(screen.getByText(LL.common.share())).toBeTruthy() + expect(screen.getByText(LL.common.close())).toBeTruthy() }) it("render successAction - LUD 09 - URL", async () => { @@ -106,6 +113,11 @@ describe("SendBitcoinCompletedScreen", () => { iv: null, decipher: () => null, }, + formatAmount: "$0.03 (25 SAT)", + feeDisplayText: "$0.00 (0 SAT)", + destination: "moises", + paymentType: "lightning", + createdAt: 1747691078, }, } as const @@ -115,13 +127,23 @@ describe("SendBitcoinCompletedScreen", () => { , ) - const button = screen.getByText(LL.ScanningQRCodeScreen.openLinkTitle()) - + const button = await waitFor(() => + screen.findByTestId(LL.ScanningQRCodeScreen.openLinkTitle()), + ) expect(button).toBeTruthy() - fireEvent.press(button) - expect(Linking.openURL).toHaveBeenCalledWith(lud09URLRoute.params.successAction.url) + + expect(screen.getByText(lud09URLRoute.params.successAction.url)).toBeTruthy() + expect(screen.getByText(lud09URLRoute.params.formatAmount)).toBeTruthy() + expect( + screen.getByText( + `${lud09URLRoute.params.feeDisplayText} | ${lud09URLRoute.params.paymentType}`, + ), + ).toBeTruthy() + expect(screen.getByText(lud09URLRoute.params.destination)).toBeTruthy() + expect(screen.getByText(LL.common.share())).toBeTruthy() + expect(screen.getByText(LL.common.close())).toBeTruthy() }) it("render successAction - LUD 09 - URL with description", async () => { @@ -139,6 +161,11 @@ describe("SendBitcoinCompletedScreen", () => { iv: null, decipher: () => null, }, + formatAmount: "$0.03 (25 SAT)", + feeDisplayText: "$0.00 (0 SAT)", + destination: "moises", + paymentType: "lightning", + createdAt: 1747691078, }, } as const @@ -148,19 +175,28 @@ describe("SendBitcoinCompletedScreen", () => { , ) - expect( - screen.getByText(lud09URLWithDescRoute.params.successAction.description), - ).toBeTruthy() - - const button = screen.getByText(LL.ScanningQRCodeScreen.openLinkTitle()) - + const button = await waitFor(() => + screen.findByTestId(LL.ScanningQRCodeScreen.openLinkTitle()), + ) expect(button).toBeTruthy() - fireEvent.press(button) - expect(Linking.openURL).toHaveBeenCalledWith( lud09URLWithDescRoute.params.successAction.url, ) + + expect( + screen.getByText(lud09URLWithDescRoute.params.successAction.description), + ).toBeTruthy() + expect(screen.getByText(lud09URLWithDescRoute.params.successAction.url)).toBeTruthy() + expect(screen.getByText(lud09URLWithDescRoute.params.formatAmount)).toBeTruthy() + expect( + screen.getByText( + `${lud09URLWithDescRoute.params.feeDisplayText} | ${lud09URLWithDescRoute.params.paymentType}`, + ), + ).toBeTruthy() + expect(screen.getByText(lud09URLWithDescRoute.params.destination)).toBeTruthy() + expect(screen.getByText(LL.common.share())).toBeTruthy() + expect(screen.getByText(LL.common.close())).toBeTruthy() }) it("render successAction - LUD 10 - message", async () => { @@ -180,6 +216,11 @@ describe("SendBitcoinCompletedScreen", () => { decipher: () => null, }, preimage: "25004cd52960a3bac983e3f95c432341a7052cef37b9253b0b0b1256d754559b", + formatAmount: "$0.03 (25 SAT)", + feeDisplayText: "$0.00 (0 SAT)", + destination: "moises", + paymentType: "lightning", + createdAt: 1747691078, }, } as const @@ -189,8 +230,19 @@ describe("SendBitcoinCompletedScreen", () => { , ) - expect(screen.getByText(lud10AESRoute.params.successAction.description)).toBeTruthy() - expect(screen.getByText(encryptedMessage)).toBeTruthy() - expect(screen.getByText(LL.SendBitcoinScreen.note())).toBeTruthy() + expect( + screen.getByText( + `${lud10AESRoute.params.successAction.description} ${encryptedMessage}`, + ), + ).toBeTruthy() + expect(screen.getByText(lud10AESRoute.params.formatAmount)).toBeTruthy() + expect( + screen.getByText( + `${lud10AESRoute.params.feeDisplayText} | ${lud10AESRoute.params.paymentType}`, + ), + ).toBeTruthy() + expect(screen.getByText(lud10AESRoute.params.destination)).toBeTruthy() + expect(screen.getByText(LL.common.share())).toBeTruthy() + expect(screen.getByText(LL.common.close())).toBeTruthy() }) }) diff --git a/app/components/success-action/field-with-copy.props.tsx b/app/components/success-action/field-with-copy.props.tsx deleted file mode 100644 index 4f9800c7a8..0000000000 --- a/app/components/success-action/field-with-copy.props.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export type FielWithCopyProps = { - text: string - copiedMessage: string - accessibilityLabel: string -} diff --git a/app/components/success-action/field-with-copy.tsx b/app/components/success-action/field-with-copy.tsx deleted file mode 100644 index ad4115da96..0000000000 --- a/app/components/success-action/field-with-copy.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from "react" -import { View, Text, TouchableOpacity } from "react-native" -import Clipboard from "@react-native-clipboard/clipboard" -import { GaloyIcon } from "@app/components/atomic/galoy-icon" -import { makeStyles, useTheme } from "@rneui/themed" -import { toastShow } from "@app/utils/toast" -import { useI18nContext } from "@app/i18n/i18n-react" -import { FielWithCopyProps } from "./field-with-copy.props" - -export const FieldWithCopy = ({ - text, - copiedMessage, - accessibilityLabel, -}: FielWithCopyProps) => { - const styles = useStyles() - const { - theme: { colors }, - } = useTheme() - const { LL } = useI18nContext() - - const copyToClipboard = (text: string, message: string) => { - Clipboard.setString(text) - toastShow({ type: "success", message, LL }) - } - - return ( - - {text} - copyToClipboard(text, copiedMessage)} - accessibilityLabel={accessibilityLabel} - hitSlop={30} - > - - - - ) -} - -const useStyles = makeStyles(({ colors }) => ({ - successActionFieldContainer: { - flexDirection: "row", - overflow: "hidden", - backgroundColor: colors.grey5, - borderRadius: 10, - alignItems: "center", - padding: 14, - minHeight: 60, - marginBottom: 12, - }, - disabledFieldBackground: { - flex: 1, - opacity: 0.5, - flexDirection: "row", - alignItems: "center", - }, - iconContainer: { - justifyContent: "center", - alignItems: "flex-start", - paddingLeft: 20, - }, - truncatedText: { - fontSize: 14, - color: colors.grey1, - }, -})) diff --git a/app/components/success-action/field-with-icon.props.tsx b/app/components/success-action/field-with-icon.props.tsx new file mode 100644 index 0000000000..0f8068512e --- /dev/null +++ b/app/components/success-action/field-with-icon.props.tsx @@ -0,0 +1,7 @@ +import { IconNamesType } from "@app/components/atomic/galoy-icon" + +export type FieldWithIconProps = { + title?: string + value: string + iconName?: IconNamesType +} diff --git a/app/components/success-action/field-with-icon.tsx b/app/components/success-action/field-with-icon.tsx new file mode 100644 index 0000000000..0ffb2725fc --- /dev/null +++ b/app/components/success-action/field-with-icon.tsx @@ -0,0 +1,139 @@ +import React from "react" +import { View, Text, TouchableOpacity, TextInput, Linking } from "react-native" +import { GaloyIcon } from "@app/components/atomic/galoy-icon" +import { makeStyles, useTheme } from "@rneui/themed" +import { FieldWithIconProps } from "./field-with-icon.props" +import Clipboard from "@react-native-clipboard/clipboard" +import { toastShow } from "@app/utils/toast" +import { useI18nContext } from "@app/i18n/i18n-react" +import { testProps } from "@app/utils/testProps" + +type TTextWithUrl = { + text: string + url?: string +} +export const FieldWithIconEvent = ({ title, value, iconName }: FieldWithIconProps) => { + const styles = useStyles() + const { LL } = useI18nContext() + + const { + theme: { colors }, + } = useTheme() + const [inputText, setInputText] = React.useState(() => value) + const [isEditing, setIsEditing] = React.useState(false) + + const handleTextWithUrl = (text: string): TTextWithUrl => { + const regex = /(https?:\/\/[^\s]+)/i + const match = text.match(regex) + + if (match) { + const url = match[0] + const textoSinURL = text.replace(url, "").trim() + return { text: textoSinURL, url } + } + return { text } + } + + const copyToClipboard = (text: string) => { + Clipboard.setString(text) + toastShow({ + type: "success", + message: LL.SendBitcoinScreen.copiedSuccessMessage(), + LL, + }) + } + + const handleEvent = () => { + if (iconName === "copy-paste") { + let value = null + if (handleTextWithUrl(inputText)?.url) { + value = `${handleTextWithUrl(inputText).text} ${handleTextWithUrl(inputText).url}` + copyToClipboard(value) + return + } + copyToClipboard(handleTextWithUrl(inputText).text) + return + } + if (iconName === "pencil") { + setIsEditing((prev) => !prev) + } + } + return ( + + {title && {title}} + + {isEditing ? ( + setIsEditing(false)} + style={styles.editingInput} + autoFocus + /> + ) : ( + + {handleTextWithUrl(inputText).text && ( + {handleTextWithUrl(inputText).text} + )} + {handleTextWithUrl(inputText).url && ( + Linking.openURL(handleTextWithUrl(inputText).url!)} + hitSlop={23} + > + {handleTextWithUrl(inputText).url} + + )} + + )} + + {iconName && ( + + + + )} + + ) +} + +const useStyles = makeStyles(({ colors }) => ({ + successActionFieldContainer: { + flexDirection: "row", + overflow: "hidden", + backgroundColor: colors.grey5, + borderRadius: 10, + alignItems: "center", + padding: 14, + minHeight: 60, + marginBottom: 12, + }, + titleFieldBackground: { + fontSize: 14, + color: colors.black, + width: 80, + }, + fieldBackground: { + flex: 1, + flexDirection: "row", + alignItems: "center", + fontSize: 14, + color: colors.black, + }, + editingInput: { + padding: 0, + margin: 0, + }, + inputStyle: { + fontSize: 14, + color: colors.black, + }, + inputUlr: { + fontSize: 14, + color: colors.primary, + }, + iconContainer: { + justifyContent: "center", + alignItems: "flex-start", + paddingLeft: 20, + }, +})) diff --git a/app/components/success-action/success-action.props.tsx b/app/components/success-action/success-action.props.tsx index 4bafbe2286..3afb4034e5 100644 --- a/app/components/success-action/success-action.props.tsx +++ b/app/components/success-action/success-action.props.tsx @@ -1,8 +1,8 @@ -import { LNURLPaySuccessAction } from "lnurl-pay/dist/types/types" - export type SuccessActionComponentProps = { - successAction?: LNURLPaySuccessAction - preimage?: string + visible?: boolean + icon?: "copy-paste" | "pencil" + title?: string + text?: string | null } export enum SuccessActionTag { diff --git a/app/components/success-action/success-action.tsx b/app/components/success-action/success-action.tsx index 57a126bf58..1b0c435b3e 100644 --- a/app/components/success-action/success-action.tsx +++ b/app/components/success-action/success-action.tsx @@ -1,126 +1,28 @@ import React from "react" -import { utils } from "lnurl-pay" -import { View, Text, TouchableOpacity, Linking } from "react-native" +import { View } from "react-native" +import { SuccessActionComponentProps } from "./success-action.props" +import { FieldWithIconEvent } from "./field-with-icon" import { makeStyles } from "@rneui/themed" -import { useI18nContext } from "@app/i18n/i18n-react" -import { SuccessActionComponentProps, SuccessActionTag } from "./success-action.props" -import { FieldWithCopy } from "./field-with-copy" export const SuccessActionComponent: React.FC = ({ - successAction, - preimage, + visible, + icon, + title, + text, }) => { const styles = useStyles() - const { LL } = useI18nContext() - - if (!successAction) return null - - const { tag, message, description, url } = successAction - const decryptedMessage = - tag === SuccessActionTag.AES && preimage - ? utils.decipherAES({ successAction, preimage }) - : null + if (!visible) { + return <> + } return ( - - {tag === SuccessActionTag.MESSAGE && message && ( - <> - {LL.SendBitcoinScreen.note()} - - - )} - - {tag === SuccessActionTag.URL && ( - <> - {description && ( - <> - {LL.SendBitcoinScreen.note()} - - - )} - Linking.openURL(url!)} - accessibilityLabel={LL.SendBitcoinScreen.openSuccessUrl()} - hitSlop={styles.hitSlopIcon} - > - - {LL.ScanningQRCodeScreen.openLinkTitle()} - - - - )} - - {tag === SuccessActionTag.AES && ( - <> - {LL.SendBitcoinScreen.note()} - {description && ( - - )} - {decryptedMessage ? ( - - ) : ( - - {LL.SendBitcoinScreen.pendingDecryptionMessage()} - - )} - - )} + + ) } - -const useStyles = makeStyles(({ colors }) => ({ - successActionContainer: { +const useStyles = makeStyles(() => ({ + fieldContainer: { minWidth: "100%", - paddingHorizontal: 40, - marginTop: 30, - }, - fieldTitleText: { - fontWeight: "bold", - marginBottom: 4, - color: colors.grey1, - }, - encryptionPendingText: { - fontSize: 14, - color: colors.grey3, - textAlign: "center", - marginTop: 10, - }, - hitSlopIcon: { - top: 10, - bottom: 10, - left: 10, - right: 10, - }, - copyUrlButton: { - backgroundColor: colors.grey5, - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - paddingVertical: 10, - paddingHorizontal: 15, - borderRadius: 8, - }, - copyUrlButtonText: { - fontSize: 16, - textAlign: "center", - color: colors.grey1, }, })) diff --git a/app/graphql/generated.gql b/app/graphql/generated.gql index c32bfbe041..9327301d67 100644 --- a/app/graphql/generated.gql +++ b/app/graphql/generated.gql @@ -271,6 +271,10 @@ mutation intraLedgerPaymentSend($input: IntraLedgerPaymentSendInput!) { __typename } status + transaction { + createdAt + __typename + } __typename } } @@ -282,6 +286,10 @@ mutation intraLedgerUsdPaymentSend($input: IntraLedgerUsdPaymentSendInput!) { __typename } status + transaction { + createdAt + __typename + } __typename } } @@ -324,6 +332,7 @@ mutation lnInvoicePaymentSend($input: LnInvoicePaymentInput!) { } status transaction { + createdAt settlementVia { ... on SettlementViaLn { preImage @@ -377,6 +386,10 @@ mutation lnNoAmountInvoicePaymentSend($input: LnNoAmountInvoicePaymentInput!) { __typename } status + transaction { + createdAt + __typename + } __typename } } @@ -399,6 +412,10 @@ mutation lnNoAmountUsdInvoicePaymentSend($input: LnNoAmountUsdInvoicePaymentInpu __typename } status + transaction { + createdAt + __typename + } __typename } } @@ -447,6 +464,7 @@ mutation onChainAddressCurrent($input: OnChainAddressCurrentInput!) { mutation onChainPaymentSend($input: OnChainPaymentSendInput!) { onChainPaymentSend(input: $input) { transaction { + createdAt settlementVia { ... on SettlementViaOnChain { arrivalInMempoolEstimatedAt @@ -472,6 +490,10 @@ mutation onChainPaymentSendAll($input: OnChainPaymentSendAllInput!) { __typename } status + transaction { + createdAt + __typename + } __typename } } @@ -483,6 +505,10 @@ mutation onChainUsdPaymentSend($input: OnChainUsdPaymentSendInput!) { __typename } status + transaction { + createdAt + __typename + } __typename } } @@ -494,6 +520,10 @@ mutation onChainUsdPaymentSendAsBtcDenominated($input: OnChainUsdPaymentSendAsBt __typename } status + transaction { + createdAt + __typename + } __typename } } diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index 0a057537ab..36d488ca8e 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -2939,63 +2939,63 @@ export type IntraLedgerPaymentSendMutationVariables = Exact<{ }>; -export type IntraLedgerPaymentSendMutation = { readonly __typename: 'Mutation', readonly intraLedgerPaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }> } }; +export type IntraLedgerPaymentSendMutation = { readonly __typename: 'Mutation', readonly intraLedgerPaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly transaction?: { readonly __typename: 'Transaction', readonly createdAt: number } | null } }; export type IntraLedgerUsdPaymentSendMutationVariables = Exact<{ input: IntraLedgerUsdPaymentSendInput; }>; -export type IntraLedgerUsdPaymentSendMutation = { readonly __typename: 'Mutation', readonly intraLedgerUsdPaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }> } }; +export type IntraLedgerUsdPaymentSendMutation = { readonly __typename: 'Mutation', readonly intraLedgerUsdPaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly transaction?: { readonly __typename: 'Transaction', readonly createdAt: number } | null } }; export type LnNoAmountInvoicePaymentSendMutationVariables = Exact<{ input: LnNoAmountInvoicePaymentInput; }>; -export type LnNoAmountInvoicePaymentSendMutation = { readonly __typename: 'Mutation', readonly lnNoAmountInvoicePaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }> } }; +export type LnNoAmountInvoicePaymentSendMutation = { readonly __typename: 'Mutation', readonly lnNoAmountInvoicePaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly transaction?: { readonly __typename: 'Transaction', readonly createdAt: number } | null } }; export type LnInvoicePaymentSendMutationVariables = Exact<{ input: LnInvoicePaymentInput; }>; -export type LnInvoicePaymentSendMutation = { readonly __typename: 'Mutation', readonly lnInvoicePaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly transaction?: { readonly __typename: 'Transaction', readonly settlementVia: { readonly __typename: 'SettlementViaIntraLedger', readonly preImage?: string | null } | { readonly __typename: 'SettlementViaLn', readonly preImage?: string | null } | { readonly __typename: 'SettlementViaOnChain' } } | null } }; +export type LnInvoicePaymentSendMutation = { readonly __typename: 'Mutation', readonly lnInvoicePaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly transaction?: { readonly __typename: 'Transaction', readonly createdAt: number, readonly settlementVia: { readonly __typename: 'SettlementViaIntraLedger', readonly preImage?: string | null } | { readonly __typename: 'SettlementViaLn', readonly preImage?: string | null } | { readonly __typename: 'SettlementViaOnChain' } } | null } }; export type LnNoAmountUsdInvoicePaymentSendMutationVariables = Exact<{ input: LnNoAmountUsdInvoicePaymentInput; }>; -export type LnNoAmountUsdInvoicePaymentSendMutation = { readonly __typename: 'Mutation', readonly lnNoAmountUsdInvoicePaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }> } }; +export type LnNoAmountUsdInvoicePaymentSendMutation = { readonly __typename: 'Mutation', readonly lnNoAmountUsdInvoicePaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly transaction?: { readonly __typename: 'Transaction', readonly createdAt: number } | null } }; export type OnChainPaymentSendMutationVariables = Exact<{ input: OnChainPaymentSendInput; }>; -export type OnChainPaymentSendMutation = { readonly __typename: 'Mutation', readonly onChainPaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly transaction?: { readonly __typename: 'Transaction', readonly settlementVia: { readonly __typename: 'SettlementViaIntraLedger' } | { readonly __typename: 'SettlementViaLn' } | { readonly __typename: 'SettlementViaOnChain', readonly arrivalInMempoolEstimatedAt?: number | null } } | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }> } }; +export type OnChainPaymentSendMutation = { readonly __typename: 'Mutation', readonly onChainPaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly transaction?: { readonly __typename: 'Transaction', readonly createdAt: number, readonly settlementVia: { readonly __typename: 'SettlementViaIntraLedger' } | { readonly __typename: 'SettlementViaLn' } | { readonly __typename: 'SettlementViaOnChain', readonly arrivalInMempoolEstimatedAt?: number | null } } | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }> } }; export type OnChainPaymentSendAllMutationVariables = Exact<{ input: OnChainPaymentSendAllInput; }>; -export type OnChainPaymentSendAllMutation = { readonly __typename: 'Mutation', readonly onChainPaymentSendAll: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }> } }; +export type OnChainPaymentSendAllMutation = { readonly __typename: 'Mutation', readonly onChainPaymentSendAll: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly transaction?: { readonly __typename: 'Transaction', readonly createdAt: number } | null } }; export type OnChainUsdPaymentSendMutationVariables = Exact<{ input: OnChainUsdPaymentSendInput; }>; -export type OnChainUsdPaymentSendMutation = { readonly __typename: 'Mutation', readonly onChainUsdPaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }> } }; +export type OnChainUsdPaymentSendMutation = { readonly __typename: 'Mutation', readonly onChainUsdPaymentSend: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly transaction?: { readonly __typename: 'Transaction', readonly createdAt: number } | null } }; export type OnChainUsdPaymentSendAsBtcDenominatedMutationVariables = Exact<{ input: OnChainUsdPaymentSendAsBtcDenominatedInput; }>; -export type OnChainUsdPaymentSendAsBtcDenominatedMutation = { readonly __typename: 'Mutation', readonly onChainUsdPaymentSendAsBtcDenominated: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }> } }; +export type OnChainUsdPaymentSendAsBtcDenominatedMutation = { readonly __typename: 'Mutation', readonly onChainUsdPaymentSendAsBtcDenominated: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly transaction?: { readonly __typename: 'Transaction', readonly createdAt: number } | null } }; export type AccountDeleteMutationVariables = Exact<{ [key: string]: never; }>; @@ -6478,6 +6478,9 @@ export const IntraLedgerPaymentSendDocument = gql` message } status + transaction { + createdAt + } } } `; @@ -6514,6 +6517,9 @@ export const IntraLedgerUsdPaymentSendDocument = gql` message } status + transaction { + createdAt + } } } `; @@ -6550,6 +6556,9 @@ export const LnNoAmountInvoicePaymentSendDocument = gql` message } status + transaction { + createdAt + } } } `; @@ -6587,6 +6596,7 @@ export const LnInvoicePaymentSendDocument = gql` } status transaction { + createdAt settlementVia { ... on SettlementViaLn { preImage @@ -6632,6 +6642,9 @@ export const LnNoAmountUsdInvoicePaymentSendDocument = gql` message } status + transaction { + createdAt + } } } `; @@ -6665,6 +6678,7 @@ export const OnChainPaymentSendDocument = gql` mutation onChainPaymentSend($input: OnChainPaymentSendInput!) { onChainPaymentSend(input: $input) { transaction { + createdAt settlementVia { ... on SettlementViaOnChain { arrivalInMempoolEstimatedAt @@ -6711,6 +6725,9 @@ export const OnChainPaymentSendAllDocument = gql` message } status + transaction { + createdAt + } } } `; @@ -6747,6 +6764,9 @@ export const OnChainUsdPaymentSendDocument = gql` message } status + transaction { + createdAt + } } } `; @@ -6783,6 +6803,9 @@ export const OnChainUsdPaymentSendAsBtcDenominatedDocument = gql` message } status + transaction { + createdAt + } } } `; diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index 350775e5a1..185b9c005c 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -2312,6 +2312,11 @@ const en: BaseTranslation = { copiedSuccessMessage: "Message copied successfully", copiedSecretMessage: "Secret message copied successfully", pendingDecryptionMessage: "Encrypted message. Waiting for payment confirmation.", + feeLabel: "Fee", + noteLabel: "Note", + sender: "Sender", + recipient: "Recipient", + time: "Time", }, SettingsScreen: { staticQr: "Printable Static QR Code", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index ca05c75365..87da63ea82 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -7219,6 +7219,26 @@ type RootTranslation = { * E​n​c​r​y​p​t​e​d​ ​m​e​s​s​a​g​e​.​ ​W​a​i​t​i​n​g​ ​f​o​r​ ​p​a​y​m​e​n​t​ ​c​o​n​f​i​r​m​a​t​i​o​n​. */ pendingDecryptionMessage: string + /** + * F​e​e + */ + feeLabel: string + /** + * N​o​t​e + */ + noteLabel: string + /** + * S​e​n​d​e​r + */ + sender: string + /** + * R​e​c​i​p​i​e​n​t + */ + recipient: string + /** + * T​i​m​e + */ + time: string } SettingsScreen: { /** @@ -16330,6 +16350,26 @@ export type TranslationFunctions = { * Encrypted message. Waiting for payment confirmation. */ pendingDecryptionMessage: () => LocalizedString + /** + * Fee + */ + feeLabel: () => LocalizedString + /** + * Note + */ + noteLabel: () => LocalizedString + /** + * Sender + */ + sender: () => LocalizedString + /** + * Recipient + */ + recipient: () => LocalizedString + /** + * Time + */ + time: () => LocalizedString } SettingsScreen: { /** diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 6e7c06db19..3005427f77 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -2241,7 +2241,12 @@ "copySecretMessage": "Copy secret message to clipboard", "copiedSuccessMessage": "Message copied successfully", "copiedSecretMessage": "Secret message copied successfully", - "pendingDecryptionMessage": "Encrypted message. Waiting for payment confirmation." + "pendingDecryptionMessage": "Encrypted message. Waiting for payment confirmation.", + "feeLabel": "Fee", + "noteLabel": "Note", + "sender": "Sender", + "recipient": "Recipient", + "time": "Time" }, "SettingsScreen": { "staticQr": "Printable Static QR Code", diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index 6d0a296fd7..769f6f1b17 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -164,7 +164,7 @@ export const RootStack = () => { Promise<{ status: PaymentSendResult | null | undefined + transaction?: Partial | null | undefined errors?: readonly GraphQlApplicationError[] extraInfo?: PaymentSendExtraInfo }> diff --git a/app/screens/send-bitcoin-screen/payment-details/intraledger.ts b/app/screens/send-bitcoin-screen/payment-details/intraledger.ts index 5fcdaf814d..5c8a0e8766 100644 --- a/app/screens/send-bitcoin-screen/payment-details/intraledger.ts +++ b/app/screens/send-bitcoin-screen/payment-details/intraledger.ts @@ -71,6 +71,7 @@ export const createIntraledgerPaymentDetails = ( return { status: data?.intraLedgerPaymentSend.status, errors: data?.intraLedgerPaymentSend.errors, + transaction: data?.intraLedgerPaymentSend?.transaction, } } @@ -99,6 +100,7 @@ export const createIntraledgerPaymentDetails = ( return { status: data?.intraLedgerUsdPaymentSend.status, errors: data?.intraLedgerUsdPaymentSend.errors, + transaction: data?.intraLedgerUsdPaymentSend?.transaction, } } diff --git a/app/screens/send-bitcoin-screen/payment-details/lightning.ts b/app/screens/send-bitcoin-screen/payment-details/lightning.ts index 37083e6634..583032ada5 100644 --- a/app/screens/send-bitcoin-screen/payment-details/lightning.ts +++ b/app/screens/send-bitcoin-screen/payment-details/lightning.ts @@ -109,6 +109,7 @@ export const createNoAmountLightningPaymentDetails = ( return { status: data?.lnNoAmountInvoicePaymentSend.status, errors: data?.lnNoAmountInvoicePaymentSend.errors, + transaction: data?.lnNoAmountInvoicePaymentSend.transaction, } } @@ -163,6 +164,7 @@ export const createNoAmountLightningPaymentDetails = ( return { status: data?.lnNoAmountUsdInvoicePaymentSend.status, errors: data?.lnNoAmountUsdInvoicePaymentSend.errors, + transaction: data?.lnNoAmountUsdInvoicePaymentSend.transaction, } } @@ -259,6 +261,7 @@ export const createAmountLightningPaymentDetails = ( return { status: data?.lnInvoicePaymentSend.status, errors: data?.lnInvoicePaymentSend.errors, + transaction: data?.lnInvoicePaymentSend.transaction, extraInfo: { preimage: settlementVia?.__typename === "SettlementViaLn" || diff --git a/app/screens/send-bitcoin-screen/payment-details/onchain.ts b/app/screens/send-bitcoin-screen/payment-details/onchain.ts index 59e4952d2b..bcaea22e20 100644 --- a/app/screens/send-bitcoin-screen/payment-details/onchain.ts +++ b/app/screens/send-bitcoin-screen/payment-details/onchain.ts @@ -64,6 +64,7 @@ export const createNoAmountOnchainPaymentDetails = ( return { status: data?.onChainPaymentSendAll.status, errors: data?.onChainPaymentSendAll.errors, + transaction: data?.onChainPaymentSendAll.transaction, } } @@ -140,6 +141,7 @@ export const createNoAmountOnchainPaymentDetails = ( return { status: data?.onChainPaymentSend.status, errors: data?.onChainPaymentSend.errors, + transaction: data?.onChainPaymentSend.transaction, extraInfo: { arrivalAtMempoolEstimate: data?.onChainPaymentSend.transaction?.settlementVia.__typename === @@ -203,6 +205,7 @@ export const createNoAmountOnchainPaymentDetails = ( return { status: data?.onChainUsdPaymentSend.status, errors: data?.onChainUsdPaymentSend.errors, + transaction: data?.onChainUsdPaymentSend.transaction, } } @@ -243,6 +246,7 @@ export const createNoAmountOnchainPaymentDetails = ( return { status: data?.onChainUsdPaymentSendAsBtcDenominated.status, errors: data?.onChainUsdPaymentSendAsBtcDenominated.errors, + transaction: data?.onChainUsdPaymentSendAsBtcDenominated.transaction, } } diff --git a/app/screens/send-bitcoin-screen/send-bitcoin-completed-screen.tsx b/app/screens/send-bitcoin-screen/send-bitcoin-completed-screen.tsx index e18df06c42..a91185295d 100644 --- a/app/screens/send-bitcoin-screen/send-bitcoin-completed-screen.tsx +++ b/app/screens/send-bitcoin-screen/send-bitcoin-completed-screen.tsx @@ -1,32 +1,35 @@ import React, { useCallback, useEffect } from "react" -import { View, Alert } from "react-native" +import { View, Alert, TouchableHighlight, ScrollView } from "react-native" import InAppReview from "react-native-in-app-review" import { useApolloClient } from "@apollo/client" import { GaloyIcon } from "@app/components/atomic/galoy-icon" import { Screen } from "@app/components/screen" -import { - SuccessIconAnimation, - CompletedTextAnimation, -} from "@app/components/success-animation" +import { SuccessIconAnimation } from "@app/components/success-animation" import { SuccessActionComponent } from "@app/components/success-action" import { setFeedbackModalShown } from "@app/graphql/client-only-query" -import { useFeedbackModalShownQuery } from "@app/graphql/generated" +import { + useFeedbackModalShownQuery, + useSettingsScreenQuery, +} from "@app/graphql/generated" import { useAppConfig } from "@app/hooks" import { useI18nContext } from "@app/i18n/i18n-react" import { RootStackParamList } from "@app/navigation/stack-param-lists" import { logAppFeedback } from "@app/utils/analytics" import { RouteProp, useNavigation } from "@react-navigation/native" import { StackNavigationProp } from "@react-navigation/stack" -import { makeStyles, Text, useTheme } from "@rneui/themed" +import { Button, makeStyles, useTheme } from "@rneui/themed" import { testProps } from "../../utils/testProps" -import { - formatTimeToMempool, - timeToMempool, -} from "../transaction-detail-screen/format-time" + import { SuggestionModal } from "./suggestion-modal" import { PaymentSendCompletedStatus } from "./use-send-payment" +import LogoLightMode from "@app/assets/logo/blink-logo-light.svg" +import LogoDarkMode from "@app/assets/logo/app-logo-dark.svg" +import { GaloyPrimaryButton } from "@app/components/atomic/galoy-primary-button" +import { SuccessActionTag } from "@app/components/success-action/success-action.props" +import { utils } from "lnurl-pay" +import { formatUnixTimestampYMDHM } from "@app/utils/date" type Props = { route: RouteProp @@ -42,10 +45,15 @@ const SendBitcoinCompletedScreen: React.FC = ({ route }) => { status: statusRaw, successAction, preimage, + formatAmount, + feeDisplayText, + destination, + paymentType, + createdAt, } = route.params const styles = useStyles() const { - theme: { colors }, + theme: { mode, colors }, } = useTheme() const status = processStatus({ arrivalAtMempoolEstimate, status: statusRaw }) @@ -57,7 +65,10 @@ const SendBitcoinCompletedScreen: React.FC = ({ route }) => { const client = useApolloClient() const feedbackShownData = useFeedbackModalShownQuery() const feedbackModalShown = feedbackShownData?.data?.feedbackModalShown - const { LL, locale } = useI18nContext() + const { data } = useSettingsScreenQuery({ fetchPolicy: "cache-first" }) + + const { LL } = useI18nContext() + const usernameTitle = data?.me?.username || LL.common.blinkUser() const iDontEnjoyTheApp = () => { logAppFeedback({ @@ -104,7 +115,7 @@ const SendBitcoinCompletedScreen: React.FC = ({ route }) => { }, [LL, client, appConfig]) const FEEDBACK_DELAY = 3000 - const CALLBACK_DELAY = 3000 + // const CALLBACK_DELAY = 3000 useEffect(() => { if (!feedbackModalShown) { const feedbackTimeout = setTimeout(() => { @@ -114,10 +125,10 @@ const SendBitcoinCompletedScreen: React.FC = ({ route }) => { clearTimeout(feedbackTimeout) } } - if (!successAction?.tag && !showSuggestionModal) { - const navigateToHomeTimeout = setTimeout(navigation.popToTop, CALLBACK_DELAY) - return () => clearTimeout(navigateToHomeTimeout) - } + // if (!successAction?.tag && !showSuggestionModal) { + // const navigateToHomeTimeout = setTimeout(navigation.popToTop, CALLBACK_DELAY) + // return () => clearTimeout(navigateToHomeTimeout) + // } }, [ client, feedbackModalShown, @@ -131,47 +142,113 @@ const SendBitcoinCompletedScreen: React.FC = ({ route }) => { const MainIcon = () => { switch (status) { case "SUCCESS": - return + return case "QUEUED": - return + return case "PENDING": - return + return } } - const SuccessText = () => { - switch (status) { - case "SUCCESS": - return LL.SendBitcoinScreen.success() - case "QUEUED": - return LL.TransactionDetailScreen.txNotBroadcast({ - countdown: formatTimeToMempool( - timeToMempool(arrivalAtMempoolEstimate as number), - LL, - locale, - ), - }) - case "PENDING": - return LL.SendBitcoinScreen.pendingPayment() + const Logo = mode === "dark" ? LogoDarkMode : LogoLightMode + + const noteMessage = (): string => { + if (!successAction) return "" + + const { tag, message, description, url } = successAction + const decryptedMessage = + tag === SuccessActionTag.AES && preimage + ? utils.decipherAES({ successAction, preimage }) + : null + + const parts = [] + + if (message) { + parts.push(message) } + if (url) { + parts.push(url) + } + if (description) { + parts.push(description) + } + if (decryptedMessage) { + parts.push(decryptedMessage) + } + + return parts.join(" ") } return ( - - - {MainIcon()} - - - {SuccessText()} - - - + + + + {MainIcon()} + + + + + + + + + + + + + + {}} + title={LL.common.share()} + /> +