Skip to content
Open
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
3 changes: 3 additions & 0 deletions app/_locales/en/messages.json

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

2 changes: 2 additions & 0 deletions shared/lib/deep-links/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { notifications } from './notifications';
import { notificationsSettings } from './notifications-settings';
import { onboarding } from './onboarding';
import { money } from './money';
import { moneyAccountDeposit } from './money-account-deposit';
import { musd } from './musd';
import { perps } from './perps';
import { perpsAsset } from './perps-asset';
Expand Down Expand Up @@ -61,6 +62,7 @@ addRoute(notificationsSettings);
addRoute(onboarding);
addRoute(swap);
addRoute(money);
addRoute(moneyAccountDeposit);
addRoute(musd);
addRoute(perps);
addRoute(perpsAsset);
Expand Down
33 changes: 33 additions & 0 deletions shared/lib/deep-links/routes/money-account-deposit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { MONEY_DEPOSIT_ROUTE } from './route';
import { moneyAccountDeposit } from './money-account-deposit';

describe('money account deposit deep link route', () => {
it('has the same pathname as the mobile deeplink', () => {
expect(moneyAccountDeposit.pathname).toBe('/money-account-deposit');
});

it('returns the correct title key', () => {
expect(moneyAccountDeposit.getTitle(new URLSearchParams())).toBe(
'deepLink_theMoneyDepositPage',
);
});

it('navigates to the Money deposit bridge', () => {
const result = moneyAccountDeposit.handler(new URLSearchParams());

expect(result).toStrictEqual({
path: MONEY_DEPOSIT_ROUTE,
query: new URLSearchParams(),
});
});

it('ignores incoming query params', () => {
const params = new URLSearchParams({ ref: 'extension', foo: 'bar' });
const result = moneyAccountDeposit.handler(params);

expect(result).toStrictEqual({
path: MONEY_DEPOSIT_ROUTE,
query: new URLSearchParams(),
});
});
});
20 changes: 20 additions & 0 deletions shared/lib/deep-links/routes/money-account-deposit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { MONEY_DEPOSIT_ROUTE, Route } from './route';

/**
* Deeplink that starts a Money Account deposit.
*
* It navigates to the Money deposit bridge, which checks the Money account is
* available and replaces itself with the deposit confirmation. Users who are
* ineligible for a Money account are redirected to the wallet home.
* - https://link.metamask.io/money-account-deposit
*/
export const moneyAccountDeposit = new Route({
pathname: '/money-account-deposit',
getTitle: (_: URLSearchParams) => 'deepLink_theMoneyDepositPage',
handler: function handler(_params: URLSearchParams) {
return {
path: MONEY_DEPOSIT_ROUTE,
query: new URLSearchParams(),
};
},
});
1 change: 1 addition & 0 deletions shared/lib/deep-links/routes/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export {
DEFAULT_ROUTE,
DEEP_LINK_ROUTE,
DISCOVER_SEARCH_ROUTE,
MONEY_DEPOSIT_ROUTE,
MONEY_HOME_ROUTE,
NOTIFICATIONS_ROUTE,
PERPS_MARKET_DETAIL_ROUTE,
Expand Down
1 change: 1 addition & 0 deletions shared/lib/deep-links/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const routesProtectedByInterstitial = [
'/card-onboarding',
'/swap',
'/money',
'/money-account-deposit',
'/earn-musd',
'/perps',
'/perps-markets',
Expand Down
1 change: 1 addition & 0 deletions ui/helpers/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export const MONEY_HOW_IT_WORKS_ROUTE = '/money-home/how-it-works';
export const MONEY_TRANSACTION_DETAILS_ROUTE =
'/money-home/activity/:transactionId';
export const MONEY_EARN_ROUTE = '/money-home/earn';
export const MONEY_DEPOSIT_ROUTE = '/money-home/deposit';

/**
* Builds the Money transaction details path for a given activity item id.
Expand Down
61 changes: 56 additions & 5 deletions ui/hooks/money/useMoneyAccountDeposit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
import { KeyringTypes } from '@metamask/keyring-controller';
import { AccountGroupType, AccountWalletType } from '@metamask/account-api';
import { renderHookWithProvider } from '../../../test/lib/render-helpers-navigate';
import { MONEY_HOME_ROUTE } from '../../helpers/constants/routes';
import {
MONEY_DEPOSIT_ROUTE,
MONEY_HOME_ROUTE,
} from '../../helpers/constants/routes';
import { getMoneyAccountDepositIntent } from '../../helpers/money/deposit-intent';
import {
ConfirmationLoader,
Expand Down Expand Up @@ -254,6 +257,54 @@ describe('useMoneyAccountDeposit', () => {
});
});

it('resolves true once the confirmation is opened', async () => {
const { result } = renderHookWithProvider(
() => useMoneyAccountDeposit(),
EVM_ACCOUNT_STATE,
);

await act(async () => {
await expect(result.current.initiateDeposit()).resolves.toBe(true);
});
});

it('resolves false for an initiation ignored while one is in flight', async () => {
const { result } = renderHookWithProvider(
() => useMoneyAccountDeposit(),
EVM_ACCOUNT_STATE,
);

await act(async () => {
const [first, second] = await Promise.all([
result.current.initiateDeposit(),
result.current.initiateDeposit(),
]);
expect(first).toBe(true);
expect(second).toBe(false);
});
});

it('replaces the current page and uses an explicit goBackTo when requested', async () => {
const { result } = renderHookWithProvider(
() => useMoneyAccountDeposit(),
EVM_ACCOUNT_STATE,
MONEY_DEPOSIT_ROUTE,
);

await act(async () => {
await result.current.initiateDeposit({
replaceConfirmation: true,
goBackTo: MONEY_HOME_ROUTE,
});
});

expect(navigateToTransactionMock).toHaveBeenCalledWith(TRANSACTION_ID, {
loader: ConfirmationLoader.CustomAmount,
goBackTo: MONEY_HOME_ROUTE,
replace: true,
});
});

it('forwards the preferred payment token to the confirmation', async () => {
const { result } = renderHookWithProvider(
() => useMoneyAccountDeposit(),
Expand Down Expand Up @@ -327,7 +378,7 @@ describe('useMoneyAccountDeposit', () => {
await act(async () => {
await expect(
result.current.initiateDeposit({ intent: 'card' }),
).resolves.toBeUndefined();
).resolves.toBe(false);
});

expect(getMoneyAccountDepositIntent(failedBatchId)).toBeUndefined();
Expand Down Expand Up @@ -414,7 +465,7 @@ describe('useMoneyAccountDeposit', () => {
);

await act(async () => {
await expect(result.current.initiateDeposit()).resolves.toBeUndefined();
await expect(result.current.initiateDeposit()).resolves.toBe(false);
});

expect(createDepositTransactionMock).not.toHaveBeenCalled();
Expand All @@ -436,7 +487,7 @@ describe('useMoneyAccountDeposit', () => {
});

await act(async () => {
await expect(result.current.initiateDeposit()).resolves.toBeUndefined();
await expect(result.current.initiateDeposit()).resolves.toBe(false);
});

expect(createDepositTransactionMock).not.toHaveBeenCalled();
Expand Down Expand Up @@ -556,7 +607,7 @@ describe('useMoneyAccountDeposit', () => {
);

await act(async () => {
await expect(result.current.initiateDeposit()).resolves.toBeUndefined();
await expect(result.current.initiateDeposit()).resolves.toBe(false);
});

expect(createDepositTransactionMock).not.toHaveBeenCalled();
Expand Down
28 changes: 21 additions & 7 deletions ui/hooks/money/useMoneyAccountDeposit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ export type InitiateDepositOptions = {
* Token to pre-select as the source of funds.
*/
preferredPaymentToken?: SetPayTokenRequest;
/**
* Replace the current page with the confirmation instead of pushing it,
* for entry points that should not be returned to (e.g. the deeplink
* bridge).
*/
replaceConfirmation?: boolean;
/**
* Route to show when the confirmation closes. Defaults to the current
* location.
*/
goBackTo?: string;
};

const DEPOSIT_FAILED_TOAST_COPY = {
Expand Down Expand Up @@ -92,13 +103,13 @@ let isDepositInitiationInFlight = false;
* the confirmation defaults the From row — and quotes — to that account instead of
* the money account that executes the batch.
*
* The current location is passed as `goBackTo` so closing the confirmation
* returns the user to the surface they started from (e.g. the Money home)
* rather than the global wallet home.
* The current location is passed as `goBackTo` (unless overridden) so
* closing the confirmation returns the user to the surface they started from
* (e.g. the Money home) rather than the global wallet home.
*
* Setup failures are reported to Sentry and shown as a toast inside this
* hook, matching mobile. The promise resolves after that so callers do not
* each need a `.catch`. There is no navigation to roll back on failure
* each need a `.catch`; it resolves to whether the confirmation was opened. There is no navigation to roll back on failure
* (the extension navigates after creation) and no user-rejection path at
* initiation (rejection happens later, inside the confirmation).
*
Expand All @@ -117,9 +128,9 @@ export function useMoneyAccountDeposit() {
const [isLoading, setIsLoading] = useState(false);

const initiateDeposit = useCallback(
async (options?: InitiateDepositOptions) => {
async (options?: InitiateDepositOptions): Promise<boolean> => {
if (isDepositInitiationInFlight) {
return;
return false;
}

const batchId = bytesToHex(new Uint8Array(uuidParse(uuidv4())));
Expand All @@ -144,9 +155,11 @@ export function useMoneyAccountDeposit() {

navigateToTransaction(transactionId, {
loader: ConfirmationLoader.CustomAmount,
goBackTo: location.pathname + location.search,
goBackTo: options?.goBackTo ?? location.pathname + location.search,
preferredPaymentToken: options?.preferredPaymentToken,
replace: options?.replaceConfirmation,
});
return true;
} catch (error) {
clearMoneyAccountDepositIntent(batchId);
const toastCopy = getDepositFailedToastCopy(options?.intent);
Expand All @@ -160,6 +173,7 @@ export function useMoneyAccountDeposit() {
...(options?.intent ? { intent: options.intent } : {}),
},
});
return false;
} finally {
isDepositInitiationInFlight = false;
setIsLoading(false);
Expand Down
18 changes: 18 additions & 0 deletions ui/pages/confirmations/hooks/useConfirmationNavigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,24 @@ describe('useConfirmationNavigation', () => {
});
});

it('replaces the current entry and returns to goBackTo when replace is set', () => {
const result = renderHook(ApprovalType.Transaction);

result.navigateToTransaction('tx-300', {
goBackTo: '/money-home',
replace: true,
});

expect(mockUseNavigate).toHaveBeenCalledTimes(1);
expect(mockUseNavigate).toHaveBeenCalledWith(
{
pathname: `${CONFIRM_TRANSACTION_ROUTE}/tx-300`,
search: 'goBackTo=%2Fmoney-home',
},
{ replace: true },
);
});

it('navigates with both loader and goBackTo params', () => {
const result = renderHook(ApprovalType.Transaction);

Expand Down
21 changes: 15 additions & 6 deletions ui/pages/confirmations/hooks/useConfirmationNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ export type ConfirmationNavigationOptions = {
* Token the confirmation should select as the source of funds.
*/
preferredPaymentToken?: SetPayTokenRequest;
/**
* Replace the current history entry instead of pushing. Back then replaces
* the confirmation with `goBackTo` rather than popping to the current page.
*/
replace?: boolean;
};

/**
Expand Down Expand Up @@ -237,14 +242,18 @@ export function useConfirmationNavigation() {
);
}

// This helper always pushes. Back must pop, or `goBackTo` is written
// on top of the page that is already the previous entry.
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${transactionId}`;

if (options.replace) {
navigate({ pathname, search: params.toString() }, { replace: true });
return;
}

// A pushed confirmation must pop on back, or `goBackTo` is written on
// top of the page that is already the previous entry.
params.set('goBackAction', ConfirmationGoBackAction.Pop);

navigate({
pathname: `${CONFIRM_TRANSACTION_ROUTE}/${transactionId}`,
search: params.toString(),
});
navigate({ pathname, search: params.toString() });
},
[navigate],
);
Expand Down
Loading
Loading