Skip to content

Commit

Permalink
feat: add onCancel support to requests
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed May 25, 2021
1 parent 0f2ce87 commit a616e7f
Show file tree
Hide file tree
Showing 19 changed files with 1,313 additions and 1,782 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-files-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@stacks/wallet-web': minor
---

This adds firing an event when a user cancels an auth or transaction popup which triggers calling an onCancel callback function.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ yarn-error.log
stacks-wallet-chromium.zip
packages
web-ext-artifacts/
.yalc/
yalc.lock
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@
"@reach/visually-hidden": "0.15.0",
"@rehooks/document-title": "1.0.2",
"@stacks/auth": "1.5.0-alpha.0",
"@stacks/connect": "5.1.0",
"@stacks/connect-react": "6.0.0",
"@stacks/connect-ui": "5.1.1",
"@stacks/connect": "5.4.0",
"@stacks/connect-react": "9.0.0",
"@stacks/connect-ui": "5.1.2",
"@stacks/network": "1.5.0-alpha.0",
"@stacks/rpc-client": "1.0.3",
"@stacks/transactions": "1.4.1",
Expand Down
26 changes: 25 additions & 1 deletion src/common/hooks/use-tx-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@store/transaction';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { currentNetworkStore } from '@store/networks';
import { finishTransaction } from '@common/transaction-utils';
import { cancelTransaction, finishTransaction } from '@common/transaction-utils';
import { useLoadable } from '@common/hooks/use-loadable';
import { finalizeTxSignature } from '@common/utils';

Expand All @@ -25,6 +25,29 @@ export const useTxState = () => {
const signedTransaction = useLoadable(signedTransactionStore);
const isUnauthorizedTransaction = useRecoilValue(isUnauthorizedTransactionStore);

const handleCancelTransaction = useRecoilCallback(
({ snapshot, set }) =>
async () => {
const pendingTransaction = await snapshot.getPromise(pendingTransactionStore);
const requestPayload = await snapshot.getPromise(requestTokenStore);
if (!pendingTransaction || !requestPayload) {
set(transactionBroadcastErrorStore, 'No pending transaction found.');
return;
}
const tx = await snapshot.getPromise(signedTransactionStore);
if (!tx) return;
try {
const result = await cancelTransaction({
tx,
});
finalizeTxSignature(requestPayload, result);
} catch (error) {
set(transactionBroadcastErrorStore, error.message);
}
},
[]
);

const doSubmitPendingTransaction = useRecoilCallback(
({ snapshot, set }) =>
async () => {
Expand Down Expand Up @@ -58,6 +81,7 @@ export const useTxState = () => {
contractSource,
contractInterface,
pendingTransactionFunction,
handleCancelTransaction,
doSubmitPendingTransaction,
broadcastError,
isUnauthorizedTransaction,
Expand Down
10 changes: 10 additions & 0 deletions src/common/hooks/use-wallet.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useCallback } from 'react';
import {
createWalletGaiaConfig,
getOrCreateWalletConfig,
Expand Down Expand Up @@ -75,6 +76,14 @@ export const useWallet = () => {
[]
);

const handleCancelAuthentication = useCallback(() => {
if (!decodedAuthRequest || !authRequest) {
return;
}
const authResponse = 'cancel';
finalizeAuthResponse({ decodedAuthRequest, authRequest, authResponse });
}, [decodedAuthRequest, authRequest]);

const doFinishSignIn = useRecoilCallback(
({ set, snapshot }) =>
async (accountIndex: number) => {
Expand Down Expand Up @@ -136,6 +145,7 @@ export const useWallet = () => {
doFinishSignIn,
doSetLatestNonce,
setWallet,
handleCancelAuthentication,
...vaultMessenger,
};
};
12 changes: 12 additions & 0 deletions src/common/transaction-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ export const generateTransaction = async ({
return tx;
};

export const cancelTransaction = async ({ tx }: { tx: StacksTransaction }): Promise<TxResult> => {
const serialized = tx.serialize();
const txRaw = `0x${serialized.toString('hex')}`;

return {
txRaw,
cancel: true,
};
};

export const finishTransaction = async ({
tx,
pendingTransaction,
Expand All @@ -168,6 +178,7 @@ export const finishTransaction = async ({
if (tx.auth.authType === AuthType.Sponsored) {
return {
txRaw,
cancel: false,
};
}

Expand All @@ -181,6 +192,7 @@ export const finishTransaction = async ({
return {
txId: response,
txRaw,
cancel: false,
};
} else {
const error = `${response.error} - ${response.reason}`;
Expand Down
3 changes: 1 addition & 2 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ export const finalizeAuthResponse = ({
};

export const finalizeTxSignature = (requestPayload: string, data: TxResult) => {
console.log(requestPayload, data);
try {
const tabId = getTab(StorageKey.transactionRequests, requestPayload);
const responseMessage: TransactionResponseMessage = {
Expand All @@ -84,7 +83,7 @@ export const finalizeTxSignature = (requestPayload: string, data: TxResult) => {
};
chrome.tabs.sendMessage(tabId, responseMessage);
deleteTabForRequest(StorageKey.transactionRequests, requestPayload);
window.close();
if (!data.cancel) window.close();
} catch (error) {
console.debug('Failed to get Tab ID for transaction request:', requestPayload);
throw new Error(
Expand Down
87 changes: 57 additions & 30 deletions src/components/popup/container.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,72 @@
import React, { memo } from 'react';
import React, { memo, useCallback, useEffect } from 'react';
import { Flex, color } from '@stacks/ui';
import { SettingsPopover } from './settings-popover';
import { useRecoilValue } from 'recoil';
import { hasRehydratedVaultStore } from '@store/wallet';
import { useTxState } from '@common/hooks/use-tx-state';
import { useWallet } from '@common/hooks/use-wallet';

interface PopupHomeProps {
header?: any;
requestType?: string;
}

export const PopupContainer: React.FC<PopupHomeProps> = memo(({ children, header }) => {
const hasRehydratedVault = useRecoilValue(hasRehydratedVaultStore);
if (!hasRehydratedVault) {
console.error('No hasRehydratedVault, rendered null');
}
return (
<Flex
flexDirection="column"
flexGrow={1}
width="100%"
background={color('bg')}
data-test="container-outer"
minHeight="100vh"
maxHeight="100vh"
position="relative"
overflow="auto"
>
{header && header}
<SettingsPopover />
export const PopupContainer: React.FC<PopupHomeProps> = memo(
({ children, header, requestType }) => {
const hasRehydratedVault = useRecoilValue(hasRehydratedVaultStore);
const { handleCancelTransaction } = useTxState();
const { handleCancelAuthentication } = useWallet();

if (!hasRehydratedVault) {
console.error('No hasRehydratedVault, rendered null');
}

/*
* When the popup is closed, this checks the requestType and forces
* the request promise to fail; triggering an onCancel callback function.
*/
const handleUnmount = useCallback(async () => {
if (requestType === 'auth') {
handleCancelAuthentication();
}
if (requestType === 'transaction') {
await handleCancelTransaction();
}
}, [requestType, handleCancelAuthentication, handleCancelTransaction]);

useEffect(() => {
window.addEventListener('beforeunload', handleUnmount);
return () => window.removeEventListener('beforeunload', handleUnmount);
}, [handleUnmount]);

return (
<Flex
flexDirection="column"
flexGrow={1}
className="main-content"
as="main"
position="relative"
width="100%"
px="loose"
pb="loose"
background={color('bg')}
data-test="container-outer"
minHeight="100vh"
maxHeight="100vh"
position="relative"
overflow="auto"
>
{/*TODO: this seems like a bug, I think it could cause a blank screen some time*/}
{hasRehydratedVault ? children : null}
{header && header}
<SettingsPopover />
<Flex
flexDirection="column"
flexGrow={1}
className="main-content"
as="main"
position="relative"
width="100%"
px="loose"
pb="loose"
>
{/*TODO: this seems like a bug, I think it could cause a blank screen some time*/}
{hasRehydratedVault ? children : null}
</Flex>
</Flex>
</Flex>
);
});
);
}
);
2 changes: 1 addition & 1 deletion src/components/unlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const Unlock: React.FC = () => {
}, [doUnlockWallet, password]);

return (
<PopupContainer header={<Header />}>
<PopupContainer header={<Header />} requestType="auth">
<Box width="100%" mt="loose">
<Text textStyle="body.large" display="block">
Enter your password you used on this device to unlock your wallet.
Expand Down
12 changes: 10 additions & 2 deletions src/extension/inpage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,15 @@ const provider: StacksProvider = {
}
);
document.dispatchEvent(event);
return new Promise(resolve => {
return new Promise((resolve, reject) => {
const handleMessage = (event: MessageEvent<AuthenticationResponseMessage>) => {
if (!isValidEvent(event, Methods.authenticationResponse)) return;
if (event.data.payload?.authenticationRequest !== authenticationRequest) return;
window.removeEventListener('message', handleMessage);
if (event.data.payload.authenticationResponse === 'cancel') {
reject(event.data.payload.authenticationResponse);
return;
}
resolve(event.data.payload.authenticationResponse);
};
window.addEventListener('message', handleMessage);
Expand All @@ -79,11 +83,15 @@ const provider: StacksProvider = {
detail: { transactionRequest },
});
document.dispatchEvent(event);
return new Promise(resolve => {
return new Promise((resolve, reject) => {
const handleMessage = (event: MessageEvent<TransactionResponseMessage>) => {
if (!isValidEvent(event, Methods.transactionResponse)) return;
if (event.data.payload?.transactionRequest !== transactionRequest) return;
window.removeEventListener('message', handleMessage);
if (event.data.payload.transactionResponse.cancel) {
reject(event.data.payload.transactionResponse);
return;
}
resolve(event.data.payload.transactionResponse);
};
window.addEventListener('message', handleMessage);
Expand Down
12 changes: 11 additions & 1 deletion src/pages/connect/choose-account.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback, useEffect } from 'react';
import { Screen, ScreenBody } from '@screen';
import { Title } from '@components/typography';
import { Box } from '@stacks/ui';
Expand Down Expand Up @@ -31,11 +31,21 @@ export const ChooseAccount: React.FC<ChooseAccountProps> = ({ next }) => {
const [reusedApps, setReusedApps] = React.useState<ConfigApp[]>([]);
const { decodedAuthRequest: authRequest } = useOnboardingState();
const [accountIndex, setAccountIndex] = React.useState<number | undefined>();
const { handleCancelAuthentication } = useWallet();

if (!wallet) {
return <Navigate to={{ pathname: '/', hash: 'sign-up' }} screenPath={ScreenPaths.GENERATION} />;
}

const handleUnmount = useCallback(async () => {
handleCancelAuthentication();
}, [handleCancelAuthentication]);

useEffect(() => {
window.addEventListener('beforeunload', handleUnmount);
return () => window.removeEventListener('beforeunload', handleUnmount);
}, [handleUnmount]);

// TODO: refactor into util, create unit tests
const didSelectAccount = ({ accountIndex }: { accountIndex: number }) => {
setAccountIndex(accountIndex);
Expand Down
2 changes: 1 addition & 1 deletion src/pages/install/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const Actions: React.FC<StackProps> = props => {
};

export const Installed: React.FC = memo(() => (
<PopupContainer header={<Header hideActions />}>
<PopupContainer header={<Header hideActions />} requestType="auth">
<Stack spacing="extra-loose" flexGrow={1} justifyContent="center">
<Stack width="100%" spacing="loose" textAlign="center" alignItems="center">
<Title as="h1" fontWeight={500}>
Expand Down
1 change: 1 addition & 0 deletions src/pages/install/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const InstalledSignIn: React.FC = () => {
<PopupContainer
header={<Header title="Continue with your Secret Key" onClose={onBack} hideActions />}
key="sign-in"
requestType="auth"
>
<Stack spacing="loose">
<Caption className="onboarding-text">
Expand Down
2 changes: 1 addition & 1 deletion src/pages/popup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ const PageTop: React.FC<StackProps> = memo(props => (
));

export const PopupHome: React.FC = memo(() => (
<PopupContainer header={<Header />}>
<PopupContainer header={<Header />} requestType="auth">
<Stack spacing="loose">
<PageTop />
<AccountInfo />
Expand Down
2 changes: 1 addition & 1 deletion src/pages/set-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const SetPasswordPage: React.FC<SetPasswordProps> = ({
>
{formik => (
<Form>
<PopupContainer header={<Header hideActions title="Set a password" />}>
<PopupContainer header={<Header hideActions title="Set a password" />} requestType="auth">
<Body className="onboarding-text">
This password is for this device only. To access your account on a new device you will
use your Secret Key.
Expand Down
2 changes: 1 addition & 1 deletion src/pages/transaction/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const TransactionPage: React.FC = () => {
if (!currentAccount || !pendingTransaction) throw new Error('Invalid code path.');

return (
<PopupContainer header={<PopupHeader />}>
<PopupContainer header={<PopupHeader />} requestType="transaction">
<Stack pt="extra-loose" spacing="base">
<Title fontWeight="bold" as="h1">
{pageTitle}
Expand Down
2 changes: 1 addition & 1 deletion test-app/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { StacksTestnet } from '@stacks/network';

dayjs.extend(relativeTime);

let coreApiUrl = 'https://stacks-node-api.stacks.co';
let coreApiUrl = 'https://stacks-node-api.testnet.stacks.co';

export const getRPCClient = () => {
return new RPCClient(coreApiUrl);
Expand Down

0 comments on commit a616e7f

Please sign in to comment.