From 7b6ad10b49acb92a04de66c4df888469d5216b45 Mon Sep 17 00:00:00 2001 From: Lionell Briones Date: Tue, 19 Aug 2025 12:26:15 +0800 Subject: [PATCH 01/75] feat: added shield plan UI --- app/images/card-amex.svg | 3 + app/images/card-mc.svg | 6 + app/images/card-visa.svg | 10 + ui/helpers/constants/routes.ts | 7 + ui/pages/pages.scss | 1 + ui/pages/routes/routes.component.tsx | 7 + ui/pages/routes/utils.js | 12 + ui/pages/shield-plan/index.scss | 80 ++++++ ui/pages/shield-plan/index.ts | 1 + .../shield-payment-modal.stories.tsx | 17 ++ ui/pages/shield-plan/shield-payment-modal.tsx | 196 +++++++++++++++ ui/pages/shield-plan/shield-plan.stories.tsx | 13 + ui/pages/shield-plan/shield-plan.tsx | 234 ++++++++++++++++++ ui/pages/shield-plan/types.ts | 7 + 14 files changed, 594 insertions(+) create mode 100644 app/images/card-amex.svg create mode 100644 app/images/card-mc.svg create mode 100644 app/images/card-visa.svg create mode 100644 ui/pages/shield-plan/index.scss create mode 100644 ui/pages/shield-plan/index.ts create mode 100644 ui/pages/shield-plan/shield-payment-modal.stories.tsx create mode 100644 ui/pages/shield-plan/shield-payment-modal.tsx create mode 100644 ui/pages/shield-plan/shield-plan.stories.tsx create mode 100644 ui/pages/shield-plan/shield-plan.tsx create mode 100644 ui/pages/shield-plan/types.ts diff --git a/app/images/card-amex.svg b/app/images/card-amex.svg new file mode 100644 index 000000000000..3710e3336ef3 --- /dev/null +++ b/app/images/card-amex.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/images/card-mc.svg b/app/images/card-mc.svg new file mode 100644 index 000000000000..c13cf1e24dc7 --- /dev/null +++ b/app/images/card-mc.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/images/card-visa.svg b/app/images/card-visa.svg new file mode 100644 index 000000000000..0ed5353ad9cc --- /dev/null +++ b/app/images/card-visa.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index 0963bdabfee5..d38cbda181db 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -126,6 +126,8 @@ export const DEEP_LINK_ROUTE = '/link'; export const WALLET_DETAILS_ROUTE = '/wallet-details/:id'; export const DEFI_ROUTE = '/defi'; +export const SHIELD_PLAN_ROUTE = '/shield-plan'; + export const ROUTES = [ { path: DEFAULT_ROUTE, label: 'Home', trackInAnalytics: true }, { path: '', label: 'Home', trackInAnalytics: true }, // "" is an alias for the Home route @@ -625,6 +627,11 @@ export const ROUTES = [ trackInAnalytics: false, }, ///: END:ONLY_INCLUDE_IF + { + path: SHIELD_PLAN_ROUTE, + label: 'Shield Plan', + trackInAnalytics: false, + }, ] as const satisfies AppRoute[]; export type AppRoutes = (typeof ROUTES)[number]; diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss index d7c2baf1fb59..11bcf2b364c3 100644 --- a/ui/pages/pages.scss +++ b/ui/pages/pages.scss @@ -31,3 +31,4 @@ @import 'multichain-accounts/address-qr-code/address-qr-code'; @import 'multichain-accounts/base-account-details/base-account-details'; @import 'multichain-accounts/multichain-account-details-page/index'; +@import 'shield-plan/index'; diff --git a/ui/pages/routes/routes.component.tsx b/ui/pages/routes/routes.component.tsx index d1b52034dd04..089c18c72b12 100644 --- a/ui/pages/routes/routes.component.tsx +++ b/ui/pages/routes/routes.component.tsx @@ -62,6 +62,7 @@ import { ACCOUNT_LIST_PAGE_ROUTE, MULTICHAIN_ACCOUNT_DETAILS_PAGE_ROUTE, NONEVM_BALANCE_CHECK_ROUTE, + SHIELD_PLAN_ROUTE, } from '../../helpers/constants/routes'; import { getProviderConfig, @@ -302,12 +303,17 @@ const MultichainAccountDetailsPage = mmLazy( '../multichain-accounts/multichain-account-details-page/index.ts' )) as unknown as DynamicImportType, ); + const NonEvmBalanceCheck = mmLazy( (() => import( '../nonevm-balance-check/index.tsx' )) as unknown as DynamicImportType, ); + +const ShieldPlan = mmLazy( + (() => import('../shield-plan/index.ts')) as unknown as DynamicImportType, +); // End Lazy Routes // eslint-disable-next-line @typescript-eslint/naming-convention @@ -617,6 +623,7 @@ export default function Routes() { path={NONEVM_BALANCE_CHECK_ROUTE} component={NonEvmBalanceCheck} /> + diff --git a/ui/pages/routes/utils.js b/ui/pages/routes/utils.js index 76bdb9453460..10978d33f126 100644 --- a/ui/pages/routes/utils.js +++ b/ui/pages/routes/utils.js @@ -27,6 +27,7 @@ import { ACCOUNT_DETAILS_ROUTE, ACCOUNT_DETAILS_QR_CODE_ROUTE, MULTICHAIN_ACCOUNT_DETAILS_PAGE_ROUTE, + SHIELD_PLAN_ROUTE, } from '../../helpers/constants/routes'; export function isConfirmTransactionRoute(pathname) { @@ -273,6 +274,17 @@ export function hideAppHeader(props) { }), ); + const isShieldPlanPage = Boolean( + matchPath(location.pathname, { + path: SHIELD_PLAN_ROUTE, + exact: false, + }), + ); + + if (isShieldPlanPage) { + return true; + } + return ( isHandlingPermissionsRequest || isHandlingAddEthereumChainRequest || diff --git a/ui/pages/shield-plan/index.scss b/ui/pages/shield-plan/index.scss new file mode 100644 index 000000000000..b18d85357844 --- /dev/null +++ b/ui/pages/shield-plan/index.scss @@ -0,0 +1,80 @@ +@use "design-system"; + +.shield-plan-page { + max-width: 600px; + + .shield-plan-page__plans { + grid-template-columns: 1fr 1fr; + } + + .shield-plan-page__plan { + border: 1px solid transparent; + .shield-plan-page__radio { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 50%; + border: 1px solid var(--color-border-default); + } + + .shield-plan-page__radio-label { + flex: 1; + } + } + + .shield-plan-page__plan--selected { + border-color: var(--color-primary-default); + + .shield-plan-page__radio { + border-color: var(--color-primary-default); + border-width: 2px; + + &::after { + content: ''; + width: 12px; + height: 12px; + background-color: var(--color-primary-default); + border-radius: 50%; + } + } + } + + .shield-plan-page__group { + .shield-plan-page__row { + margin-bottom: 1px; + + &:first-of-type { + border-top-left-radius: 8px; + border-top-right-radius: 8px; + } + + &:last-of-type { + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + margin-bottom: 0; + } + } + } +} + +.shield-payment-modal { + .payment-method-item { + position: relative; + + &:not(.payment-method-item--selected) { + &:hover { + background: var(--color-background-default-hover); + } + } + + &__selected-indicator { + width: 4px; + height: calc(100% - 8px); + position: absolute; + top: 4px; + left: 4px; + } + } +} diff --git a/ui/pages/shield-plan/index.ts b/ui/pages/shield-plan/index.ts new file mode 100644 index 000000000000..20ca1e61f054 --- /dev/null +++ b/ui/pages/shield-plan/index.ts @@ -0,0 +1 @@ +export { default } from './shield-plan'; diff --git a/ui/pages/shield-plan/shield-payment-modal.stories.tsx b/ui/pages/shield-plan/shield-payment-modal.stories.tsx new file mode 100644 index 000000000000..018e149b8885 --- /dev/null +++ b/ui/pages/shield-plan/shield-payment-modal.stories.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { ShieldPaymentModal } from './shield-payment-modal'; + +export default { + title: 'Components/UI/ShieldPlan/ShieldPaymentModal', + component: ShieldPaymentModal, +}; + +export const DefaultStory = () => { + return ( +
+ {}} /> +
+ ); +}; + +DefaultStory.storyName = 'Default'; diff --git a/ui/pages/shield-plan/shield-payment-modal.tsx b/ui/pages/shield-plan/shield-payment-modal.tsx new file mode 100644 index 000000000000..dc5f952be489 --- /dev/null +++ b/ui/pages/shield-plan/shield-payment-modal.tsx @@ -0,0 +1,196 @@ +import React, { useState } from 'react'; +import { + AvatarNetwork, + AvatarNetworkSize, + AvatarToken, + BadgeWrapper, + Box, + Icon, + IconName, + IconSize, + Modal, + ModalBody, + ModalContent, + ModalHeader, + ModalOverlay, + Text, +} from '../../components/component-library'; +import { + AlignItems, + BackgroundColor, + BlockSize, + BorderColor, + BorderRadius, + Display, + FlexDirection, + JustifyContent, + TextAlign, + TextColor, + TextVariant, +} from '../../helpers/constants/design-system'; +import classnames from 'classnames'; +import { AssetPickerModal } from '../../components/multichain/asset-picker-amount/asset-picker-modal'; +import { TabName } from '../../components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-tabs'; +import { PAYMENT_METHODS, PaymentMethod } from './types'; + +export const ShieldPaymentModal = ({ + isOpen, + onClose, + selectedPaymentMethod, + setSelectedPaymentMethod, +}: { + isOpen: boolean; + onClose: () => void; + selectedPaymentMethod: PaymentMethod; + setSelectedPaymentMethod: (method: PaymentMethod) => void; +}) => { + const [showAssetPickerModal, setShowAssetPickerModal] = useState(false); + + return ( + + + + Change payment method + + { + setSelectedPaymentMethod(PAYMENT_METHODS.TOKEN); + setShowAssetPickerModal(true); + }} + > + {selectedPaymentMethod === PAYMENT_METHODS.TOKEN && ( + + )} + + + + } + > + + + + Pay with USDT + + Balance: 123.43 USDT + + + + + + + + setSelectedPaymentMethod(PAYMENT_METHODS.CARD)} + > + {selectedPaymentMethod === PAYMENT_METHODS.CARD && ( + + )} + + + + + Pay with card + + Mastercard + Visa + American Express + + + + + + + setShowAssetPickerModal(false)} + asset={undefined} + onAssetChange={() => {}} + header="Select a token" + autoFocus={false} + visibleTabs={[TabName.TOKENS]} + /> + + + ); +}; diff --git a/ui/pages/shield-plan/shield-plan.stories.tsx b/ui/pages/shield-plan/shield-plan.stories.tsx new file mode 100644 index 000000000000..8f9ef0ddf877 --- /dev/null +++ b/ui/pages/shield-plan/shield-plan.stories.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import ShieldPlan from './shield-plan'; + +export default { + title: 'Components/UI/ShieldPlan', + component: ShieldPlan, +}; + +export const DefaultStory = () => { + return ; +}; + +DefaultStory.storyName = 'Default'; diff --git a/ui/pages/shield-plan/shield-plan.tsx b/ui/pages/shield-plan/shield-plan.tsx new file mode 100644 index 000000000000..2c299571fbf1 --- /dev/null +++ b/ui/pages/shield-plan/shield-plan.tsx @@ -0,0 +1,234 @@ +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import classnames from 'classnames'; +import { + Content, + Footer, + Header, + Page, +} from '../../components/multichain/pages/page'; +import { + AlignItems, + BackgroundColor, + BlockSize, + BorderColor, + BorderRadius, + Display, + FlexDirection, + IconColor, + JustifyContent, + TextAlign, + TextColor, + TextVariant, +} from '../../helpers/constants/design-system'; +import { + ButtonIconSize, + ButtonIcon, + IconName, + Box, + Text, + BoxProps, + BadgeWrapper, + AvatarNetwork, + AvatarNetworkSize, + AvatarToken, + Icon, + IconSize, + ButtonSize, + ButtonVariant, + Button, +} from '../../components/component-library'; +import { useI18nContext } from '../../hooks/useI18nContext'; +import { PAYMENT_METHODS, PaymentMethod } from './types'; +import { ShieldPaymentModal } from './shield-payment-modal'; + +const PLAN_TYPES = { + ANNUAL: 'annual', + MONTHLY: 'monthly', +} as const; + +type Plan = { + id: (typeof PLAN_TYPES)[keyof typeof PLAN_TYPES]; + label: string; + price: string; +}; + +const ShieldPlan = () => { + const history = useHistory(); + const t = useI18nContext(); + + const [selectedPlan, setSelectedPlan] = useState( + PLAN_TYPES.ANNUAL, + ); + + const handleBack = () => { + history.goBack(); + }; + + const plans: Plan[] = [ + { + id: PLAN_TYPES.ANNUAL, + label: 'Annual', + price: '$80/year', + }, + { + id: PLAN_TYPES.MONTHLY, + label: 'Monthly', + price: '$8/month', + }, + ]; + + const planDetails = [ + 'No charge now, try free for 14 days', + 'Pre-approve membership (default 1 year), with fees charged only on a monthly basis', + 'Secures your assets from risky transactions', + ]; + + const [showPaymentModal, setShowPaymentModal] = useState(false); + + const [selectedPaymentMethod, setSelectedPaymentMethod] = + useState(PAYMENT_METHODS.TOKEN); + + const rowsStyleProps: BoxProps<'div'> = { + display: Display.Flex, + justifyContent: JustifyContent.spaceBetween, + alignItems: AlignItems.center, + backgroundColor: BackgroundColor.backgroundSection, + padding: 4, + }; + + return ( + +
+ } + > + Choose your plan +
+ + + {plans.map((plan) => ( + setSelectedPlan(plan.id)} + > +
+ + {plan.label} + {plan.price} + + + ))} + + + setShowPaymentModal(true)} + width={BlockSize.Full} + > + Pay with + + + {selectedPaymentMethod === PAYMENT_METHODS.TOKEN ? ( + + } + > + + + ) : ( + + )} + + {selectedPaymentMethod === PAYMENT_METHODS.TOKEN + ? 'ETH' + : 'Card'} + + + + + + + + + Plan details + + + {planDetails.map((detail, index) => ( + + + {detail} + + ))} + + + + setShowPaymentModal(false)} + selectedPaymentMethod={selectedPaymentMethod} + setSelectedPaymentMethod={setSelectedPaymentMethod} + /> + +
+ + + Auto renews for $8/month until canceled + +
+ + ); +}; + +export default ShieldPlan; diff --git a/ui/pages/shield-plan/types.ts b/ui/pages/shield-plan/types.ts new file mode 100644 index 000000000000..de5dfb4963c4 --- /dev/null +++ b/ui/pages/shield-plan/types.ts @@ -0,0 +1,7 @@ +export const PAYMENT_METHODS = { + TOKEN: 'token', + CARD: 'card', +} as const; + +export type PaymentMethod = + (typeof PAYMENT_METHODS)[keyof typeof PAYMENT_METHODS]; From af2361a37a61e3dfce0174ccf1973337647cf7e7 Mon Sep 17 00:00:00 2001 From: Lionell Briones Date: Tue, 19 Aug 2025 18:09:41 +0800 Subject: [PATCH 02/75] feat: updated font size --- ui/pages/shield-plan/shield-plan.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ui/pages/shield-plan/shield-plan.tsx b/ui/pages/shield-plan/shield-plan.tsx index 2c299571fbf1..4cbd47d31787 100644 --- a/ui/pages/shield-plan/shield-plan.tsx +++ b/ui/pages/shield-plan/shield-plan.tsx @@ -154,7 +154,7 @@ const ShieldPlan = () => { onClick={() => setShowPaymentModal(true)} width={BlockSize.Full} > - Pay with + Pay with {selectedPaymentMethod === PAYMENT_METHODS.TOKEN ? ( @@ -177,7 +177,7 @@ const ShieldPlan = () => { ) : ( )} - + {selectedPaymentMethod === PAYMENT_METHODS.TOKEN ? 'ETH' : 'Card'} @@ -192,13 +192,18 @@ const ShieldPlan = () => { {...rowsStyleProps} display={Display.Block} > - + Plan details - + {planDetails.map((detail, index) => ( From 1e12eb803723b26444f279f4e4dabc9e696fe900 Mon Sep 17 00:00:00 2001 From: Lionell Briones Date: Thu, 21 Aug 2025 11:28:55 +0800 Subject: [PATCH 03/75] feat: add save badge, custom token list and confirmation style UI --- ui/pages/shield-plan/index.scss | 14 +++++++++ ui/pages/shield-plan/shield-payment-modal.tsx | 29 ++++++++++++++++--- ui/pages/shield-plan/shield-plan.tsx | 24 ++++++++++++++- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/ui/pages/shield-plan/index.scss b/ui/pages/shield-plan/index.scss index b18d85357844..1df8dbecc8ab 100644 --- a/ui/pages/shield-plan/index.scss +++ b/ui/pages/shield-plan/index.scss @@ -9,6 +9,8 @@ .shield-plan-page__plan { border: 1px solid transparent; + position: relative; + .shield-plan-page__radio { display: flex; align-items: center; @@ -22,6 +24,14 @@ .shield-plan-page__radio-label { flex: 1; } + + .shield-plan-page__save-badge { + position: absolute; + right: 10px; + top: -8px; + height: 20px; + background-color: var(--color-primary-default); + } } .shield-plan-page__plan--selected { @@ -57,6 +67,10 @@ } } } + + .shield-plan-page__footer { + border-top: 1px solid var(--color-border-muted); + } } .shield-payment-modal { diff --git a/ui/pages/shield-plan/shield-payment-modal.tsx b/ui/pages/shield-plan/shield-payment-modal.tsx index dc5f952be489..c15144669612 100644 --- a/ui/pages/shield-plan/shield-payment-modal.tsx +++ b/ui/pages/shield-plan/shield-payment-modal.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import classnames from 'classnames'; import { AvatarNetwork, AvatarNetworkSize, @@ -9,7 +10,6 @@ import { IconName, IconSize, Modal, - ModalBody, ModalContent, ModalHeader, ModalOverlay, @@ -28,9 +28,9 @@ import { TextColor, TextVariant, } from '../../helpers/constants/design-system'; -import classnames from 'classnames'; import { AssetPickerModal } from '../../components/multichain/asset-picker-amount/asset-picker-modal'; import { TabName } from '../../components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-tabs'; +import { AssetType } from '../../../shared/constants/transaction'; import { PAYMENT_METHODS, PaymentMethod } from './types'; export const ShieldPaymentModal = ({ @@ -184,11 +184,32 @@ export const ShieldPaymentModal = ({ setShowAssetPickerModal(false)} - asset={undefined} - onAssetChange={() => {}} + onAssetChange={(asset) => { + console.log('onAssetChange', asset); + }} header="Select a token" autoFocus={false} visibleTabs={[TabName.TOKENS]} + customTokenListGenerator={() => { + return [ + { + address: '0x0000000000000000000000000000000000000000', + symbol: 'USDC', + image: + 'https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png?1547042194', + type: AssetType.token, + chainId: '0x1', + }, + { + address: '0x0000000000000000000000000000000000000000', + symbol: 'USDT', + image: + 'https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png?1547042194', + type: AssetType.token, + chainId: '0x1', + }, + ] as unknown as (keyof typeof AssetPickerModal)['customTokenListGenerator']; + }} /> diff --git a/ui/pages/shield-plan/shield-plan.tsx b/ui/pages/shield-plan/shield-plan.tsx index 4cbd47d31787..87edbb8698b4 100644 --- a/ui/pages/shield-plan/shield-plan.tsx +++ b/ui/pages/shield-plan/shield-plan.tsx @@ -143,6 +143,23 @@ const ShieldPlan = () => { {plan.label} {plan.price} + {plan.id === PLAN_TYPES.ANNUAL && ( + + + Save 16% + + + )} ))} @@ -220,7 +237,12 @@ const ShieldPlan = () => { setSelectedPaymentMethod={setSelectedPaymentMethod} /> -