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

Add support for SEP-0007 payment requests #1179

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3ce61a1
Add '@stellarguard/stellar-uri' dependency
ebma Nov 23, 2020
9dd12c4
Save settings to localStorage in web build
ebma Nov 23, 2020
1116cbb
Add transactionRequest context
ebma Nov 25, 2020
9e31652
Add TransactionRequestHandler
ebma Nov 25, 2020
3d83dc0
Add VerifyTrustedServiceDialog
ebma Nov 25, 2020
fac8b02
Add PaymentAccountSelectionDialog
ebma Nov 25, 2020
09c84b1
Add transaction-request.json to i18n translations
ebma Nov 25, 2020
fcd49b7
Support filling in payment form from search params
ebma Nov 30, 2020
daf8479
Go to payment form on payment account selection
ebma Nov 30, 2020
776cffb
Only show accounts which have trustline for asset
ebma Nov 30, 2020
c262505
Add proper backnavigation from payment dialog
ebma Dec 2, 2020
c5a0579
Enable trusted services by default
ebma Dec 2, 2020
714943d
Disable form fields if preselected params exist
ebma Dec 2, 2020
66bf4e5
Show error when no account is selectable
ebma Dec 2, 2020
77d5a45
Remove colons and change warning text
ebma Dec 2, 2020
ebf347e
Some small-ish UI changes affecting the tx request review dialog
andywer Dec 3, 2020
1adf3f2
Change wording of warning message
ebma Dec 7, 2020
b106196
Show dismiss button if onCancel prop present
ebma Jan 20, 2021
90f3e3c
Add 'selectedAccount' prop to AccountSelectionList
ebma Jan 20, 2021
2c107d0
Add NoAccountsDialog
ebma Jan 20, 2021
d6dff65
Move components
ebma Jan 20, 2021
f6b3c67
Refactor PaymentAccountSelectionDialog
ebma Jan 20, 2021
abc70ab
Change key of i18n strings
ebma Jan 20, 2021
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
2 changes: 2 additions & 0 deletions i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Generic from "./locales/en/generic.json"
import Operations from "./locales/en/operations.json"
import Payment from "./locales/en/payment.json"
import Trading from "./locales/en/trading.json"
import TransactionRequest from "./locales/en/transaction-request.json"
import TransferService from "./locales/en/transfer-service.json"

const translations = {
Expand All @@ -19,6 +20,7 @@ const translations = {
operations: Operations,
payment: Payment,
trading: Trading,
"transaction-request": TransactionRequest,
"transfer-service": TransferService
} as const

Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/en/generic.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@
"request-failed-error": "Request to {{target}} failed with status {{status}}: {{message}}",
"stellar-address-not-found-error": "Stellar address not found: {{address}}",
"stellar-address-request-failed-error": "Stellar address resolution of {{address}} failed.",
"stellar-uri-verification-error": "Stellar URI's signature could not be verified.",
"submission-failed-error": "Submitting transaction to {{endpoint}} failed with status {{status}}: {{message}}",
"testnet-endpoint-not-available-error": "{{service}} does not provide a testnet endpoint.",
"timeout-error": "Request timed out",
"unexpected-action-error": "Unexpected action: {{action}}",
"unexpected-state-error": "Encountered unexpected state: {{state}}",
"unexpected-response-type-error": "Unexpected response type: {{type}} / ${dataType}",
"unexpected-stellar-uri-type-error": "Incoming uri {{incomingURI}} does not match any expected type.",
"unknown-error": "An unknown error occured.",
"update-already-running-error": "Update is already running!",
"wrong-password-error": "Wrong password.",
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/en/payment.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"actions": {
"dismiss": "Dismiss",
"submit": "Send now"
},
"memo-metadata": {
Expand Down
44 changes: 44 additions & 0 deletions i18n/locales/en/transaction-request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"no-accounts": {
"action": {
"dismiss": "Dismiss"
},
"info": {
"1": "No accounts found for the specified network.",
"2": "You must import an account before you can sign transaction requests."
},
"title": "Transaction proposed"
},
"payment": {
"account-selector": "Select the account to use",
"action": {
"dismiss": "Dismiss",
"select": "Select"
},
"error": {
"no-activated-accounts": "No activated accounts found.",
"no-accounts-with-trustline": "No accounts with matching trustline found."
},
"header": {
"origin-domain": "The following transaction has been proposed by <1>{{originDomain}}<3>",
"no-origin-domain": "You opened the following payment request from an unknown origin."
},
"uri-content": {
"message": "Message"
},
"title": "Transaction proposed",
"warning": "The origin of this request cannot be verified! Decline when in doubt."
},
"verify-trusted-service": {
"action": {
"trust": "Trust",
"cancel": "Cancel"
},
"title": "Verify Trusted Service",
"info": {
"1": "You opened a Stellar URI originating from an unknown origin",
"2": "If you trust this domain you can add this service to your list of trusted services.",
"3": "You can view and edit your list of trusted services anytime in the application settings."
}
}
}
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
}
},
"dependencies": {
"@stellarguard/stellar-uri": "^2.0.0",
"electron-context-menu": "^0.15.0",
"electron-debug": "^3.0.1",
"electron-is-dev": "^1.1.0",
Expand Down
2 changes: 1 addition & 1 deletion shared/types/platform.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
interface TrustedService {
domain: string
signingKey: string
signingKey?: string
}

declare namespace Platform {
Expand Down
3 changes: 2 additions & 1 deletion src/Account/components/AccountSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const isMobileDevice = process.env.PLATFORM === "android" || process.env.PLATFOR
interface AccountSelectionListProps {
accounts: Account[]
disabled?: boolean
selectedAccount?: Account
testnet: boolean
onChange?: (account: Account) => void
}
Expand All @@ -38,7 +39,7 @@ function AccountSelectionList(props: AccountSelectionListProps) {
index={index}
key={account.id}
onClick={handleListItemClick}
selected={index === selectedIndex}
selected={props.selectedAccount ? account === props.selectedAccount : index === selectedIndex}
/>
))}
{props.accounts.length === 0 ? (
Expand Down
2 changes: 2 additions & 0 deletions src/App/bootstrap/app-stage2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { VerticalLayout } from "~Layout/components/Box"
import { appIsLoaded } from "~SplashScreen/splash-screen"
import ConnectionErrorListener from "~Toasts/components/ConnectionErrorListener"
import NotificationContainer from "~Toasts/components/NotificationContainer"
import TransactionRequestHandler from "~TransactionRequest/components/TransactionRequestHandler"
import AllAccountsPage from "../components/AccountListView"
import AndroidBackButton from "../components/AndroidBackButton"
import DesktopNotifications from "../components/DesktopNotifications"
Expand Down Expand Up @@ -72,6 +73,7 @@ function Stage2() {
<React.Suspense fallback={null}>
{/* Notifications need to come after the -webkit-overflow-scrolling element on iOS */}
<DesktopNotifications />
<TransactionRequestHandler />
</React.Suspense>
{process.env.PLATFORM === "android" ? <AndroidBackButton /> : null}
{process.env.PLATFORM === "android" || process.env.PLATFORM === "ios" ? <LinkHandler /> : null}
Expand Down
13 changes: 8 additions & 5 deletions src/App/bootstrap/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import { NotificationsProvider } from "../contexts/notifications"
import { SettingsProvider } from "../contexts/settings"
import { SignatureDelegationProvider } from "../contexts/signatureDelegation"
import { StellarProvider } from "../contexts/stellar"
import { TransactionRequestProvider } from "../contexts/transactionRequest"

export function ContextProviders(props: { children: React.ReactNode }) {
return (
<StellarProvider>
<AccountsProvider>
<SettingsProvider>
<CachingProviders>
<NotificationsProvider>
<SignatureDelegationProvider>{props.children}</SignatureDelegationProvider>
</NotificationsProvider>
</CachingProviders>
<TransactionRequestProvider>
<CachingProviders>
<NotificationsProvider>
<SignatureDelegationProvider>{props.children}</SignatureDelegationProvider>
</NotificationsProvider>
</CachingProviders>
</TransactionRequestProvider>
</SettingsProvider>
</AccountsProvider>
</StellarProvider>
Expand Down
67 changes: 67 additions & 0 deletions src/App/contexts/transactionRequest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from "react"
import { StellarUri, StellarUriType } from "@stellarguard/stellar-uri"
import { CustomError } from "~Generic/lib/errors"
import { subscribeToDeepLinkURLs } from "~Platform/protocol-handler"
import { verifyTransactionRequest } from "~Transaction/lib/stellar-uri"
import { trackError } from "./notifications"

const allowUnsafeTestnetURIs = Boolean(process.env.ALLOW_UNSAFE_TESTNET_URIS)

interface Props {
children: React.ReactNode
}

interface ContextType {
uri: StellarUri | null
clearURI: () => void
}

const initialValues: ContextType = {
uri: null,
clearURI: () => undefined
}

const TransactionRequestContext = React.createContext<ContextType>(initialValues)

export function TransactionRequestProvider(props: Props) {
const [uri, setURI] = React.useState<StellarUri | null>(null)

const clearURI = React.useCallback(() => setURI(null), [])

const verifyStellarURI = React.useCallback(async (incomingURI: string) => {
try {
const parsedURI = await verifyTransactionRequest(incomingURI, { allowUnsafeTestnetURIs })
setURI(parsedURI)
} catch (error) {
trackError(error)
}
}, [])

React.useEffect(() => {
const unsubscribe = subscribeToDeepLinkURLs(async incomingURI => {
const url = new URL(incomingURI)
switch (url.pathname) {
case StellarUriType.Transaction:
case StellarUriType.Pay:
verifyStellarURI(incomingURI)
break
default:
trackError(
CustomError(
"UnexpectedStellarUriTypeError",
`Incoming uri ${incomingURI} does not match any expected type.`,
{ incomingURI }
)
)
break
}
})
return unsubscribe
}, [verifyStellarURI])

return (
<TransactionRequestContext.Provider value={{ uri, clearURI }}>{props.children}</TransactionRequestContext.Provider>
)
}

export { ContextType as TransactionRequestContextType, TransactionRequestContext }
3 changes: 1 addition & 2 deletions src/AppSettings/components/AppSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ function AppSettings() {

const { accounts } = React.useContext(AccountsContext)
const settings = React.useContext(SettingsContext)
const trustedServicesEnabled = process.env.TRUSTED_SERVICES && process.env.TRUSTED_SERVICES === "enabled"

const getEffectiveLanguage = <L extends string | undefined, F extends any>(lang: L, fallback: F) => {
return availableLanguages.indexOf(lang as any) > -1 ? lang : fallback
Expand Down Expand Up @@ -73,7 +72,7 @@ function AppSettings() {
/>
<HideMemoSetting onToggle={settings.toggleHideMemos} value={settings.hideMemos} />
<MultiSigSetting onToggle={settings.toggleMultiSignature} value={settings.multiSignature} />
{trustedServicesEnabled ? <TrustedServicesSetting onClick={navigateToTrustedServices} /> : undefined}
<TrustedServicesSetting onClick={navigateToTrustedServices} />
</List>
<SettingsDialogs />
</Carousel>
Expand Down
2 changes: 2 additions & 0 deletions src/Generic/components/AssetSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ interface AssetSelectorProps {
assets: Array<Asset | Horizon.BalanceLine>
children?: React.ReactNode
className?: string
disabled?: boolean
disabledAssets?: Asset[]
disableUnderline?: boolean
helperText?: TextFieldProps["helperText"]
Expand Down Expand Up @@ -123,6 +124,7 @@ function AssetSelector(props: AssetSelectorProps) {
<TextField
autoFocus={props.autoFocus}
className={props.className}
disabled={props.disabled}
error={Boolean(props.inputError)}
helperText={props.helperText}
label={props.inputError ? props.inputError : props.label}
Expand Down
19 changes: 15 additions & 4 deletions src/Payment/components/PaymentDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react"
import { useTranslation } from "react-i18next"
import { Asset, Server, Transaction } from "stellar-sdk"
import { Asset, MemoType, Server, Transaction } from "stellar-sdk"
import { Account } from "~App/contexts/accounts"
import { trackError } from "~App/contexts/notifications"
import { useLiveAccountData, useLiveAccountOffers } from "~Generic/hooks/stellar-subscriptions"
Expand All @@ -15,12 +15,21 @@ import MainTitle from "~Generic/components/MainTitle"
import TransactionSender from "~Transaction/components/TransactionSender"
import PaymentForm from "./PaymentForm"

export interface PaymentParams {
amount?: string
asset?: Asset
destination?: string
memo?: string
memoType?: MemoType
}

interface Props {
account: Account
accountData: AccountData
horizon: Server
onClose: () => void
openOrdersCount: number
paymentParams?: PaymentParams
sendTransaction: (transaction: Transaction) => Promise<any>
}

Expand Down Expand Up @@ -72,9 +81,9 @@ function PaymentDialog(props: Props) {
<PaymentForm
accountData={props.accountData}
actionsRef={dialogActionsRef}
onCancel={props.onClose}
onSubmit={handleSubmit}
openOrdersCount={props.openOrdersCount}
preselectedParams={props.paymentParams}
testnet={props.account.testnet}
trustedAssets={trustedAssets}
txCreationPending={txCreationPending}
Expand All @@ -83,12 +92,14 @@ function PaymentDialog(props: Props) {
)
}

function ConnectedPaymentDialog(props: Pick<Props, "account" | "onClose">) {
function ConnectedPaymentDialog(
props: Pick<Props, "account" | "onClose" | "paymentParams"> & { onSubmissionCompleted?: () => void }
) {
const accountData = useLiveAccountData(props.account.publicKey, props.account.testnet)
const { offers: openOrders } = useLiveAccountOffers(props.account.publicKey, props.account.testnet)

return (
<TransactionSender account={props.account} onSubmissionCompleted={props.onClose}>
<TransactionSender account={props.account} onSubmissionCompleted={props.onSubmissionCompleted || props.onClose}>
{({ horizon, sendTransaction }) => (
<PaymentDialog
{...props}
Expand Down
Loading