Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
18da350
fix: use subscription pricing for subscription confirmation
tuna1207 Oct 8, 2025
89fd35b
Merge branch 'main' into fix/update-shield-crypto-confirmation
tuna1207 Oct 9, 2025
f4acc67
refactor: make product price from transaction data hook
tuna1207 Oct 9, 2025
c37a8f4
feat: handle subscription after crypto approval
tuna1207 Oct 9, 2025
e3a1a2a
fix: test case
tuna1207 Oct 9, 2025
9672813
feat: add test case
tuna1207 Oct 9, 2025
65fdcaf
fix: useShieldSubscriptionPricingFromTokenApproval return pending state
tuna1207 Oct 9, 2025
9277e6b
fix: useShieldSubscriptionPricingFromTokenApproval compare all plan
tuna1207 Oct 10, 2025
35f1aa0
fix: correct test case
tuna1207 Oct 10, 2025
d9a6112
Merge branch 'main' into fix/update-shield-crypto-confirmation
tuna1207 Oct 10, 2025
03b3f5d
feat: useShieldSubscriptionPricingFromTokenApproval make sure runtime…
tuna1207 Oct 10, 2025
f8f70a6
Merge branch 'fix/update-shield-crypto-confirmation' into feat/handle…
tuna1207 Oct 10, 2025
de6f940
fix: test lint
tuna1207 Oct 10, 2025
bce7fde
Merge branch 'fix/update-shield-crypto-confirmation' into feat/handle…
tuna1207 Oct 10, 2025
c922fc6
feat: refactor handle subscription function
tuna1207 Oct 10, 2025
ace43d4
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 10, 2025
d61a845
fix: test case
tuna1207 Oct 10, 2025
f7d2aeb
refactor: useDecodedTransactionDataValue
tuna1207 Oct 10, 2025
07d1302
fix: test case
tuna1207 Oct 11, 2025
201459b
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 11, 2025
8892a09
fix: test case
tuna1207 Oct 11, 2025
217451a
fix: test case
tuna1207 Oct 11, 2025
b4ecf08
fix: don't refetch pricing api in confirmation
tuna1207 Oct 11, 2025
e2ced52
fix: e2e test mock
tuna1207 Oct 13, 2025
629e7bd
fix: e2e test
tuna1207 Oct 13, 2025
674599a
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 13, 2025
831e7dd
fix: decoded transaction data amount
tuna1207 Oct 15, 2025
e1ca6e8
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 15, 2025
f5cbd15
Merge branch 'main' into feat/handle-subscription-after-confirm
chaitanyapotti Oct 15, 2025
226d8f9
feat: handle shield approve transaction in context
tuna1207 Oct 16, 2025
3da06dd
fix: remove redundant changes
tuna1207 Oct 16, 2025
4e16def
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 16, 2025
11b8e12
feat: make sure no null property access
tuna1207 Oct 16, 2025
7bb8029
fix: test lint
tuna1207 Oct 16, 2025
393cf97
fix: lint
tuna1207 Oct 16, 2025
6bebd9c
fix: lint
tuna1207 Oct 16, 2025
ffcd229
fix: remove redundant config
tuna1207 Oct 16, 2025
71e897c
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 17, 2025
cfd8bab
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 18, 2025
b7c2a4a
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 22, 2025
b9cac30
feat: handle subscription crypto approval in background
tuna1207 Oct 22, 2025
265e6c8
feat: handle status page and loading indicator
tuna1207 Oct 22, 2025
7fb65f3
Revert "feat: handle status page and loading indicator"
tuna1207 Oct 22, 2025
daf4520
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 23, 2025
2914a6f
feat: update shield approval confirm flow
tuna1207 Oct 23, 2025
028aae6
refactor: updateAndApproveTx before and error handler
tuna1207 Oct 23, 2025
defdc63
fix: remove redundant selector
tuna1207 Oct 23, 2025
6e344fd
feat: update timeout
tuna1207 Oct 23, 2025
9de5717
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 24, 2025
225dbbd
fix: wait for subscription creation cancel
tuna1207 Oct 24, 2025
4a784df
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 28, 2025
f555b7c
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 29, 2025
deccc18
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 29, 2025
8f91aa2
fix: test case
tuna1207 Oct 29, 2025
f7d727a
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 30, 2025
e82066a
feat: handle subscription crypto approval using controller
tuna1207 Oct 30, 2025
0f600bf
feat: handle cache last selected payment method
tuna1207 Oct 30, 2025
32ff392
Revert "feat: handle cache last selected payment method"
tuna1207 Oct 30, 2025
7f249b3
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 30, 2025
315e423
fix: test case
tuna1207 Oct 30, 2025
de3751d
feat: move shield navigate functions to useShieldConfirm
tuna1207 Oct 30, 2025
797054e
refactor: not update updateAndApproveTx action
tuna1207 Oct 30, 2025
525790a
fix: test case
tuna1207 Oct 30, 2025
97ecfe5
fix: test lint
tuna1207 Oct 30, 2025
a7462a9
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 31, 2025
784c076
feat: ensure no null access
tuna1207 Oct 31, 2025
23c1e78
feat: catch log err
tuna1207 Oct 31, 2025
83761cd
Merge branch 'main' into feat/handle-subscription-after-confirm
tuna1207 Oct 31, 2025
ddcfa60
feat: use last selected payment detail instead of deducting from appr…
tuna1207 Oct 31, 2025
aa69533
fix: test lint
tuna1207 Nov 1, 2025
55fb0b6
Merge branch 'main' into fix/flicker-subscription-crypto-approval
tuna1207 Nov 4, 2025
0602d99
Merge branch 'main' into fix/flicker-subscription-crypto-approval
tuna1207 Nov 5, 2025
efacdee
Merge branch 'main' into fix/flicker-subscription-crypto-approval
chaitanyapotti Nov 5, 2025
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
102 changes: 0 additions & 102 deletions ui/hooks/subscription/useSubscriptionPricing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,12 @@ import {
ProductType,
PaymentType,
} from '@metamask/subscription-controller';
import {
TransactionMeta,
TransactionStatus,
} from '@metamask/transaction-controller';
import { renderHookWithProvider } from '../../../test/lib/render-helpers';
import baseMockState from '../../../test/data/mock-state.json';
import {
useSubscriptionPricing,
useSubscriptionProductPlans,
useSubscriptionPaymentMethods,
useShieldSubscriptionPricingFromTokenApproval,
} from './useSubscriptionPricing';

const mockSubscriptionPricing: PricingResponse = {
Expand Down Expand Up @@ -186,101 +181,4 @@ describe('useSubscriptionPricing', () => {
expect(result.current).toBeUndefined();
});
});

describe('useShieldSubscriptionPricingFromTokenApproval', () => {
const mockTransactionMeta: TransactionMeta = {
id: 'test-tx-id',
chainId: '0x1',
networkClientId: 'mainnet',
status: TransactionStatus.unapproved,
time: Date.now(),
txParams: {
from: '0x1234567890123456789012345678901234567890',
to: '0x0000000000000000000000000000000000000000',
},
};

it('should return monthly plan when approval amount matches monthly', async () => {
const { result } = renderHookWithProvider(
() =>
useShieldSubscriptionPricingFromTokenApproval({
transactionMeta: mockTransactionMeta,
decodedApprovalAmount: '120000000',
}),
mockState,
);

// Wait for async operation to complete
await new Promise((resolve) => setTimeout(resolve, 100));

expect(result.current.productPrice).toEqual({
interval: RECURRING_INTERVALS.month,
unitAmount: 120000000,
unitDecimals: 6,
currency: 'usd',
trialPeriodDays: 7,
minBillingCycles: 1,
});
expect(result.current.selectedTokenPrice).toEqual(
mockSubscriptionPricing?.paymentMethods?.find(
(paymentMethod) => paymentMethod.type === PAYMENT_TYPES.byCrypto,
)?.chains?.[0]?.tokens?.[0],
);
});

it('should return yearly plan when approval amount matches yearly', async () => {
const { result } = renderHookWithProvider(
() =>
useShieldSubscriptionPricingFromTokenApproval({
transactionMeta: mockTransactionMeta,
decodedApprovalAmount: '100000000',
}),
mockState,
);

// Wait for async operation to complete
await new Promise((resolve) => setTimeout(resolve, 100));

expect(result.current.productPrice).toEqual({
interval: RECURRING_INTERVALS.year,
unitAmount: 100000000,
unitDecimals: 6,
currency: 'usd',
trialPeriodDays: 7,
minBillingCycles: 1,
});
});

it('should return undefined when approval amount does not match any plan', async () => {
const { result } = renderHookWithProvider(
() =>
useShieldSubscriptionPricingFromTokenApproval({
transactionMeta: mockTransactionMeta,
decodedApprovalAmount: '99999999',
}),
mockState,
);

// Wait for async operation to complete
await new Promise((resolve) => setTimeout(resolve, 100));

expect(result.current.productPrice).toBeUndefined();
});

it('should return undefined when transaction meta is not provided', async () => {
const { result } = renderHookWithProvider(
() =>
useShieldSubscriptionPricingFromTokenApproval({
transactionMeta: undefined,
decodedApprovalAmount: '120000000',
}),
mockState,
);

// Wait for async operation to complete
await new Promise((resolve) => setTimeout(resolve, 100));

expect(result.current.productPrice).toBeUndefined();
});
});
});
84 changes: 0 additions & 84 deletions ui/hooks/subscription/useSubscriptionPricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,14 @@ import { useDispatch, useSelector } from 'react-redux';
import log from 'loglevel';
import {
ChainPaymentInfo,
PAYMENT_TYPES,
PaymentType,
PricingPaymentMethod,
PricingResponse,
PRODUCT_TYPES,
ProductPrice,
ProductType,
RECURRING_INTERVALS,
TokenPaymentInfo,
} from '@metamask/subscription-controller';
import { Hex } from '@metamask/utils';
import { TransactionMeta } from '@metamask/transaction-controller';
import { getSubscriptionPricing } from '../../selectors/subscription';
import {
getSubscriptionCryptoApprovalAmount,
Expand Down Expand Up @@ -214,83 +210,3 @@ export const useSubscriptionPaymentMethods = (
[pricing, paymentType],
);
};

/**
* Use this hook to get the shield subscription price derived from transaction data.
*
* @param params - The parameters for the hook.
* @param params.transactionMeta - The transaction meta.
* @param params.decodedApprovalAmount - The decoded approval amount.
* @returns The product price.
*/
export const useShieldSubscriptionPricingFromTokenApproval = ({
transactionMeta,
decodedApprovalAmount,
}: {
transactionMeta?: TransactionMeta;
decodedApprovalAmount?: string;
}) => {
const { subscriptionPricing } = useSubscriptionPricing(); // shouldn't refetch pricing here since we are using the cached pricing from shield plan screen to compare price amount
const pricingPlans = useSubscriptionProductPlans(
PRODUCT_TYPES.SHIELD,
subscriptionPricing,
);
const cryptoPaymentMethod = useSubscriptionPaymentMethods(
PAYMENT_TYPES.byCrypto,
subscriptionPricing,
);
const selectedTokenPrice = useMemo(() => {
return cryptoPaymentMethod?.chains
?.find(
(chain) =>
chain.chainId.toLowerCase() ===
transactionMeta?.chainId.toLowerCase(),
)
?.tokens.find(
(token) =>
token.address.toLowerCase() ===
transactionMeta?.txParams?.to?.toLowerCase(),
);
}, [cryptoPaymentMethod, transactionMeta]);

// need to do async here since `getSubscriptionCryptoApprovalAmount` make call to background script
const { value: productPrice, pending } = useAsyncResult(async (): Promise<
ProductPrice | undefined
> => {
if (selectedTokenPrice) {
const params = {
chainId: transactionMeta?.chainId as Hex,
paymentTokenAddress: selectedTokenPrice.address as Hex,
productType: PRODUCT_TYPES.SHIELD,
};
// Get all intervals from RECURRING_INTERVALS
const intervals = Object.values(RECURRING_INTERVALS);

// Fetch approval amounts for all intervals
const approvalAmounts = await Promise.all(
intervals.map((interval) =>
getSubscriptionCryptoApprovalAmount({
...params,
interval,
}),
),
);

// Find the matching plan by comparing approval amounts
for (let i = 0; i < approvalAmounts.length; i++) {
if (approvalAmounts[i]?.approveAmount === decodedApprovalAmount) {
return pricingPlans?.find((plan) => plan.interval === intervals[i]);
}
}
}

return undefined;
}, [
transactionMeta,
selectedTokenPrice,
decodedApprovalAmount,
pricingPlans,
]);

return { productPrice, pending, selectedTokenPrice };
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { ProductPrice } from '@metamask/subscription-controller';
import {
CachedLastSelectedPaymentMethod,
PAYMENT_TYPES,
PricingResponse,
PRODUCT_TYPES,
RECURRING_INTERVALS,
} from '@metamask/subscription-controller';
import { getMockApproveConfirmState } from '../../../../../../../test/data/confirmations/helper';
import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers';
import { tEn } from '../../../../../../../test/lib/i18n-helpers';
Expand Down Expand Up @@ -63,26 +69,67 @@ jest.mock('../../../../../../store/actions', () => ({
}),
}));

jest.mock('../../../../../../hooks/subscription/useSubscriptionPricing', () => {
const mockProductPrice: ProductPrice = {
interval: 'month',
minBillingCycles: 12,
unitAmount: 8000000,
unitDecimals: 6,
currency: 'usd',
trialPeriodDays: 14,
};
return {
useShieldSubscriptionPricingFromTokenApproval: jest.fn(() => ({
productPrice: mockProductPrice,
pending: false,
})),
};
});
const mockSubscriptionPricing: PricingResponse = {
products: [
{
name: PRODUCT_TYPES.SHIELD,
prices: [
{
interval: RECURRING_INTERVALS.month,
unitAmount: 8_000_000,
unitDecimals: 6,
currency: 'usd',
trialPeriodDays: 14,
minBillingCycles: 12,
},
{
interval: RECURRING_INTERVALS.year,
unitAmount: 100_000_000,
unitDecimals: 6,
currency: 'usd',
trialPeriodDays: 14,
minBillingCycles: 1,
},
],
},
],
paymentMethods: [
{
type: PAYMENT_TYPES.byCrypto,
chains: [
{
chainId: '0x1',
paymentAddress: '0x1234567890123456789012345678901234567890',
tokens: [
{
address: '0x0000000000000000000000000000000000000000',
symbol: 'usdc',
decimals: 6,
conversionRate: { usd: '1' },
},
],
},
],
},
],
};

const mockLastUsedPaymentDetail: CachedLastSelectedPaymentMethod = {
plan: RECURRING_INTERVALS.month,
paymentTokenAddress: '0x1234567890123456789012345678901234567890',
type: PAYMENT_TYPES.byCrypto,
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Mismatched Token Addresses Break Test Token Display

The test data has mismatched token addresses. The paymentTokenAddress in mockLastUsedPaymentDetail does not align with the token address defined in mockSubscriptionPricing. This causes selectedTokenPrice to be undefined, which means the test might not accurately reflect the component's intended behavior or display the correct token symbol.

Fix in Cursor Fix in Web


describe('ShieldSubscriptionApproveInfo', () => {
it('renders correctly', () => {
const state = getMockApproveConfirmState();
// @ts-expect-error - mock state
state.metamask.lastSelectedPaymentMethod = {
[PRODUCT_TYPES.SHIELD]: mockLastUsedPaymentDetail,
};
// @ts-expect-error - mock state
state.metamask.pricing = mockSubscriptionPricing;

const mockStore = configureMockStore([])(state);
const { getByText } = renderWithConfirmContextProvider(
<ShieldSubscriptionApproveInfo />,
Expand Down
Loading
Loading