From 7b47198cb0d42168aeca5c14fc256d6ea9f7d2b5 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:12:25 +0530 Subject: [PATCH 1/8] WIP: add boilerplate; with back-button support --- src/popup/Popup.tsx | 5 +++ src/popup/components/OutOfFunds.tsx | 45 +++++++++++++++++++++---- src/popup/components/layout/header.tsx | 8 +++++ src/popup/pages/OutOfFunds.tsx | 11 +++--- src/popup/pages/OutOfFunds_AddFunds.tsx | 28 +++++++++++++++ 5 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 src/popup/pages/OutOfFunds_AddFunds.tsx diff --git a/src/popup/Popup.tsx b/src/popup/Popup.tsx index da6cf5e4..19897e9d 100644 --- a/src/popup/Popup.tsx +++ b/src/popup/Popup.tsx @@ -19,6 +19,7 @@ export const ROUTES_PATH = { SETTINGS: '/settings', MISSING_HOST_PERMISSION: '/missing-host-permission', OUT_OF_FUNDS: '/out-of-funds', + OUT_OF_FUNDS_ADD_FUNDS: '/out-of-funds/s/add-funds', ERROR_KEY_REVOKED: '/error/key-revoked' } as const @@ -49,6 +50,10 @@ export const routes = [ path: ROUTES_PATH.OUT_OF_FUNDS, lazy: () => import('./pages/OutOfFunds') }, + { + path: ROUTES_PATH.OUT_OF_FUNDS_ADD_FUNDS, + lazy: () => import('./pages/OutOfFunds_AddFunds') + }, { path: ROUTES_PATH.SETTINGS, lazy: () => import('./pages/Settings') diff --git a/src/popup/components/OutOfFunds.tsx b/src/popup/components/OutOfFunds.tsx index b14fd67c..97abeb0e 100644 --- a/src/popup/components/OutOfFunds.tsx +++ b/src/popup/components/OutOfFunds.tsx @@ -10,19 +10,49 @@ interface OutOfFundsProps { info: Pick grantRecurring?: RecurringGrant['amount'] grantOneTime?: OneTimeGrant['amount'] - requestAddFunds: (details: AddFundsPayload) => Promise + onChooseOption: (recurring: boolean) => void } export const OutOfFunds = ({ info, grantOneTime, grantRecurring, - requestAddFunds + onChooseOption }: OutOfFundsProps) => { if (!grantOneTime && !grantRecurring) { throw new Error('Provide at least one of grantOneTime and grantRecurring') } + return ( +
+

Out of funds

+ + + + +
+ ) +} + +interface AddFundsProps { + info: Pick + recurring: boolean + grantRecurring?: RecurringGrant['amount'] + grantOneTime?: OneTimeGrant['amount'] + requestAddFunds: (details: AddFundsPayload) => Promise +} + +export function AddFunds({ + info, + grantOneTime, + grantRecurring, + recurring, + requestAddFunds +}: AddFundsProps) { + if (!grantOneTime && !grantRecurring) { + throw new Error('Provide at least one of grantOneTime and grantRecurring') + } + const currencySymbol = getCurrencySymbol(info.assetCode) const amount = transformBalance( grantRecurring?.value || grantOneTime!.value, @@ -39,14 +69,15 @@ export const OutOfFunds = ({ return (
-

Out of funds

- +

Add funds

Top-up: {currencySymbol + amount}

- - - + {recurring ? ( + + ) : ( + + )}
) } diff --git a/src/popup/components/layout/header.tsx b/src/popup/components/layout/header.tsx index dfb237ab..eb6e1645 100644 --- a/src/popup/components/layout/header.tsx +++ b/src/popup/components/layout/header.tsx @@ -12,6 +12,14 @@ const NavigationButton = () => { return useMemo(() => { if (!connected) return null + if (location.pathname.includes('/s/')) { + return ( + + + + ) + } + return location.pathname === `${ROUTES_PATH.SETTINGS}` ? ( diff --git a/src/popup/pages/OutOfFunds.tsx b/src/popup/pages/OutOfFunds.tsx index 8c4d1d3a..174537a0 100644 --- a/src/popup/pages/OutOfFunds.tsx +++ b/src/popup/pages/OutOfFunds.tsx @@ -1,21 +1,24 @@ import React from 'react' +import { useNavigate } from 'react-router-dom' import { OutOfFunds } from '@/popup/components/OutOfFunds' import { usePopupState } from '@/popup/lib/context' -import { addFunds } from '@/popup/lib/messages' +import { ROUTES_PATH } from '@/popup/Popup' +import type { State } from '@/popup/pages/OutOfFunds_AddFunds' export const Component = () => { const { state: { grants, walletAddress } } = usePopupState() + const navigate = useNavigate() return ( { - const res = await addFunds(data) - return res + onChooseOption={(recurring) => { + const state: State = { recurring } + navigate(ROUTES_PATH.OUT_OF_FUNDS_ADD_FUNDS, { state }) }} /> ) diff --git a/src/popup/pages/OutOfFunds_AddFunds.tsx b/src/popup/pages/OutOfFunds_AddFunds.tsx new file mode 100644 index 00000000..e0297de9 --- /dev/null +++ b/src/popup/pages/OutOfFunds_AddFunds.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import { useLocation } from 'react-router-dom' +import { AddFunds } from '@/popup/components/OutOfFunds' +import { usePopupState } from '@/popup/lib/context' +import { addFunds } from '@/popup/lib/messages' + +export type State = { recurring: boolean } + +export const Component = () => { + const { + state: { grants, walletAddress } + } = usePopupState() + const location = useLocation() + const state = location.state as State + + return ( + { + const res = await addFunds(data) + return res + }} + /> + ) +} From 510e74285e5dee45742f91ee133da72978f37547 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:54:54 +0530 Subject: [PATCH 2/8] ui first pass --- src/popup/components/OutOfFunds.tsx | 158 ++++++++++++++++++------ src/popup/pages/OutOfFunds_AddFunds.tsx | 9 +- 2 files changed, 122 insertions(+), 45 deletions(-) diff --git a/src/popup/components/OutOfFunds.tsx b/src/popup/components/OutOfFunds.tsx index 97abeb0e..4eda8b8d 100644 --- a/src/popup/components/OutOfFunds.tsx +++ b/src/popup/components/OutOfFunds.tsx @@ -1,10 +1,18 @@ import React from 'react' import { Button } from './ui/Button' -import type { RecurringGrant, OneTimeGrant } from '@/shared/types' +import type { RecurringGrant, OneTimeGrant, AmountValue } from '@/shared/types' import type { AddFundsPayload, Response } from '@/shared/messages' import type { WalletAddress } from '@interledger/open-payments' -import { getCurrencySymbol, transformBalance } from '../lib/utils' +import { + charIsNumber, + formatNumber, + getCurrencySymbol, + transformBalance +} from '../lib/utils' import { getNextOccurrence } from '@/shared/helpers' +import { ErrorMessage } from './ErrorMessage' +import { Input } from './ui/Input' +import { useForm } from 'react-hook-form' interface OutOfFundsProps { info: Pick @@ -24,10 +32,23 @@ export const OutOfFunds = ({ } return ( -
-

Out of funds

+
+ +
+

Funds have been depleted. You can no longer make payments.

+

Please add funds.

+ {grantRecurring?.value && ( +

+ +

+ )} +
+ +
-
@@ -36,49 +57,104 @@ export const OutOfFunds = ({ interface AddFundsProps { info: Pick - recurring: boolean - grantRecurring?: RecurringGrant['amount'] - grantOneTime?: OneTimeGrant['amount'] + recurring: false | 'P1M' + defaultAmount: AmountValue requestAddFunds: (details: AddFundsPayload) => Promise } export function AddFunds({ info, - grantOneTime, - grantRecurring, + defaultAmount, recurring, requestAddFunds }: AddFundsProps) { - if (!grantOneTime && !grantRecurring) { - throw new Error('Provide at least one of grantOneTime and grantRecurring') - } + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + setError, + setValue + } = useForm({ + criteriaMode: 'firstError', + mode: 'onSubmit', + reValidateMode: 'onBlur', + defaultValues: { + amount: transformBalance(defaultAmount, info.assetScale) + } + }) const currencySymbol = getCurrencySymbol(info.assetCode) - const amount = transformBalance( - grantRecurring?.value || grantOneTime!.value, - info.assetScale - ) - const requestTopUpOneTime = () => { - return requestAddFunds({ amount: amount, recurring: false }) - } + return ( +
{ + const response = await requestAddFunds({ + amount: data.amount, + recurring: !!recurring + }) + if (!response.success) { + setError('root', { message: response.message }) + } + })} + > + - const requestTopUpRecurring = () => { - return requestAddFunds({ amount: amount, recurring: true }) - } + { + if ( + !charIsNumber(e.key) && + e.key !== 'Backspace' && + e.key !== 'ArrowLeft' && + e.key !== 'ArrowRight' && + e.key !== 'Delete' && + e.key !== 'Tab' + ) { + e.preventDefault() + } + }} + errorMessage={errors.amount?.message} + {...register('amount', { + required: { value: true, message: 'Amount is required.' }, + valueAsNumber: false, + onBlur: (e: React.FocusEvent) => { + setValue( + 'amount', + formatNumber(+e.currentTarget.value, info.assetScale) + ) + } + })} + /> - return ( -
-

Add funds

-

- Top-up: {currencySymbol + amount} -

- {recurring ? ( - - ) : ( - + {(errors.root || errors.amount) && ( + )} -
+ + + ) } @@ -93,15 +169,15 @@ function RecurringAutoRenewInfo({ const renewDate = getNextOccurrence(grantRecurring.interval, new Date()) return ( -

- If you do nothing, you will have {currencySymbol} - {amount} available on{' '} -

+ ) } diff --git a/src/popup/pages/OutOfFunds_AddFunds.tsx b/src/popup/pages/OutOfFunds_AddFunds.tsx index e0297de9..742905ea 100644 --- a/src/popup/pages/OutOfFunds_AddFunds.tsx +++ b/src/popup/pages/OutOfFunds_AddFunds.tsx @@ -11,14 +11,15 @@ export const Component = () => { state: { grants, walletAddress } } = usePopupState() const location = useLocation() - const state = location.state as State + + const state: State = { recurring: false, ...location.state } + const defaultAmount = grants.recurring?.value ?? grants.oneTime!.value return ( { const res = await addFunds(data) return res From 187483d8358aad9b4a917f77d88f00a3657e19fb Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:29:13 +0530 Subject: [PATCH 3/8] use i18n on text --- src/_locales/en/messages.json | 52 +++++++++++++++++++ src/popup/components/OutOfFunds.tsx | 80 +++++++++++++++++------------ 2 files changed, 98 insertions(+), 34 deletions(-) diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 18edf452..068fd47d 100755 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -37,6 +37,58 @@ "pay_error_notEnoughFunds": { "message": "Not enough funds to facilitate payment." }, + "outOfFunds_error_title": { + "message": "Out of funds" + }, + "outOfFunds_error_text": { + "message": "Funds have been depleted. You can no longer make payments." + }, + "outOfFunds_error_textHint": { + "message": "Please add funds." + }, + "outOfFunds_error_textDoNothing": { + "message": "If you do nothing, $AMOUNT$ will automatically be available on $AUTO_RENEW_DATE$", + "placeholders": { + "AMOUNT": { + "content": "$1", + "example": "$2.05" + }, + "AUTO_RENEW_DATE": { + "content": "$2", + "example": "Aug 22, 2024, 6:05 PM" + } + } + }, + "outOfFunds_action_optionOneTime": { + "message": "Let me add a one off payment" + }, + "outOfFunds_action_optionRecurring": { + "message": "Let me add a monthly grant" + }, + "outOfFundsAddFunds_label_walletAddress": { + "message": "Connected wallet address" + }, + "outOfFundsAddFunds_label_amount": { + "message": "Amount" + }, + "outOfFundsAddFunds_label_amountDescriptionOneTime": { + "message": "Enter the amount to add from the wallet." + }, + "outOfFundsAddFunds_label_amountDescriptionRecurring": { + "message": "Enter the amount to add from the wallet. This amount will renew automatically every month (next: $NEXT_RENEW_DATE$}).", + "placeholders": { + "NEXT_RENEW_DATE": { + "content": "$1", + "example": "Aug 22, 2024" + } + } + }, + "outOfFundsAddFunds_action_addOneTime": { + "message": "Add funds" + }, + "outOfFundsAddFunds_action_addRecurring": { + "message": "Add funds" + }, "connectWallet_error_invalidClient": { "message": "Failed to connect. Please make sure you have added the public key to the correct wallet address." } diff --git a/src/popup/components/OutOfFunds.tsx b/src/popup/components/OutOfFunds.tsx index 4eda8b8d..4413580e 100644 --- a/src/popup/components/OutOfFunds.tsx +++ b/src/popup/components/OutOfFunds.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Button } from './ui/Button' +import { useForm } from 'react-hook-form' import type { RecurringGrant, OneTimeGrant, AmountValue } from '@/shared/types' import type { AddFundsPayload, Response } from '@/shared/messages' import type { WalletAddress } from '@interledger/open-payments' @@ -8,11 +8,12 @@ import { formatNumber, getCurrencySymbol, transformBalance -} from '../lib/utils' +} from '@/popup/lib/utils' +import { useTranslation } from '@/popup/lib/context' import { getNextOccurrence } from '@/shared/helpers' -import { ErrorMessage } from './ErrorMessage' -import { Input } from './ui/Input' -import { useForm } from 'react-hook-form' +import { ErrorMessage } from '@/popup/components/ErrorMessage' +import { Button } from '@/popup/components/ui/Button' +import { Input } from '@/popup/components/ui/Input' interface OutOfFundsProps { info: Pick @@ -30,13 +31,17 @@ export const OutOfFunds = ({ if (!grantOneTime && !grantRecurring) { throw new Error('Provide at least one of grantOneTime and grantRecurring') } + const t = useTranslation() return (
- +
-

Funds have been depleted. You can no longer make payments.

-

Please add funds.

+

{t('outOfFunds_error_text')}

+

{t('outOfFunds_error_textHint')}

{grantRecurring?.value && (

- - + +

) } @@ -68,6 +77,7 @@ export function AddFunds({ recurring, requestAddFunds }: AddFundsProps) { + const t = useTranslation() const { register, handleSubmit, @@ -100,7 +110,7 @@ export function AddFunds({ > { @@ -136,10 +146,7 @@ export function AddFunds({ required: { value: true, message: 'Amount is required.' }, valueAsNumber: false, onBlur: (e: React.FocusEvent) => { - setValue( - 'amount', - formatNumber(+e.currentTarget.value, info.assetScale) - ) + setValue('amount', formatNumber(+e.currentTarget.value, 2)) } })} /> @@ -152,7 +159,9 @@ export function AddFunds({ )} ) @@ -162,22 +171,25 @@ function RecurringAutoRenewInfo({ grantRecurring, info }: Pick) { + const t = useTranslation() + if (!grantRecurring) return null const currencySymbol = getCurrencySymbol(info.assetCode) const amount = transformBalance(grantRecurring.value, info.assetScale) const renewDate = getNextOccurrence(grantRecurring.interval, new Date()) + const renewDateLocalized = renewDate.toLocaleString(undefined, { + dateStyle: 'medium', + timeStyle: 'short' + }) - return ( - <> - If you do nothing, {currencySymbol} - {amount} will automatically be available on{' '} - - - ) + return t('outOfFunds_error_textDoNothing', [ + `${currencySymbol}${amount}`, + renewDateLocalized + ]) +} + +function getNextOccurrenceDate(period: 'P1M', baseDate = new Date()) { + const date = getNextOccurrence(`R/${baseDate.toISOString()}/${period}`) + return date.toLocaleDateString(undefined, { dateStyle: 'medium' }) } From 80ae614f962dd1b2fe415b369e3adda1dc1391f0 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:53:43 +0530 Subject: [PATCH 4/8] nits & cleanup --- src/_locales/en/messages.json | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 068fd47d..7116f7b1 100755 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -47,16 +47,10 @@ "message": "Please add funds." }, "outOfFunds_error_textDoNothing": { - "message": "If you do nothing, $AMOUNT$ will automatically be available on $AUTO_RENEW_DATE$", + "message": "If you do nothing, $AMOUNT$ will automatically be available on $AUTO_RENEW_DATE$", "placeholders": { - "AMOUNT": { - "content": "$1", - "example": "$2.05" - }, - "AUTO_RENEW_DATE": { - "content": "$2", - "example": "Aug 22, 2024, 6:05 PM" - } + "AMOUNT": { "content": "$1", "example": "$2.05" }, + "AUTO_RENEW_DATE": { "content": "$2", "example": "Aug 22, 2024, 6:05 PM" } } }, "outOfFunds_action_optionOneTime": { @@ -77,10 +71,7 @@ "outOfFundsAddFunds_label_amountDescriptionRecurring": { "message": "Enter the amount to add from the wallet. This amount will renew automatically every month (next: $NEXT_RENEW_DATE$}).", "placeholders": { - "NEXT_RENEW_DATE": { - "content": "$1", - "example": "Aug 22, 2024" - } + "NEXT_RENEW_DATE": { "content": "$1", "example": "Aug 22, 2024" } } }, "outOfFundsAddFunds_action_addOneTime": { From 0b6f85f36d047c2b8a412465065bf1b07fe89de3 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:59:19 +0530 Subject: [PATCH 5/8] Update src/popup/components/OutOfFunds.tsx --- src/popup/components/OutOfFunds.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/popup/components/OutOfFunds.tsx b/src/popup/components/OutOfFunds.tsx index 4413580e..6968a9d9 100644 --- a/src/popup/components/OutOfFunds.tsx +++ b/src/popup/components/OutOfFunds.tsx @@ -190,6 +190,6 @@ function RecurringAutoRenewInfo({ } function getNextOccurrenceDate(period: 'P1M', baseDate = new Date()) { - const date = getNextOccurrence(`R/${baseDate.toISOString()}/${period}`) + const date = getNextOccurrence(`R/${baseDate.toISOString()}/${period}`, baseDate) return date.toLocaleDateString(undefined, { dateStyle: 'medium' }) } From 00fc94e82855df5a55bdad6ea20842ba312a6cc3 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 22 Jul 2024 20:02:54 +0530 Subject: [PATCH 6/8] lint fix --- src/popup/components/OutOfFunds.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/popup/components/OutOfFunds.tsx b/src/popup/components/OutOfFunds.tsx index 6968a9d9..d3d92f01 100644 --- a/src/popup/components/OutOfFunds.tsx +++ b/src/popup/components/OutOfFunds.tsx @@ -190,6 +190,9 @@ function RecurringAutoRenewInfo({ } function getNextOccurrenceDate(period: 'P1M', baseDate = new Date()) { - const date = getNextOccurrence(`R/${baseDate.toISOString()}/${period}`, baseDate) + const date = getNextOccurrence( + `R/${baseDate.toISOString()}/${period}`, + baseDate + ) return date.toLocaleDateString(undefined, { dateStyle: 'medium' }) } From 9e28318aa892b2db25deca7df9b9bd20e1ec952f Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:02:44 +0530 Subject: [PATCH 7/8] update copy a bit --- src/_locales/en/messages.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 7116f7b1..6f09cb1f 100755 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -47,17 +47,17 @@ "message": "Please add funds." }, "outOfFunds_error_textDoNothing": { - "message": "If you do nothing, $AMOUNT$ will automatically be available on $AUTO_RENEW_DATE$", + "message": "If you do nothing, $AMOUNT$ will automatically be renewed and available on $AUTO_RENEW_DATE$", "placeholders": { "AMOUNT": { "content": "$1", "example": "$2.05" }, "AUTO_RENEW_DATE": { "content": "$2", "example": "Aug 22, 2024, 6:05 PM" } } }, "outOfFunds_action_optionOneTime": { - "message": "Let me add a one off payment" + "message": "Let me top-up funds one time" }, "outOfFunds_action_optionRecurring": { - "message": "Let me add a monthly grant" + "message": "Let me add funds & auto-renew monthly" }, "outOfFundsAddFunds_label_walletAddress": { "message": "Connected wallet address" @@ -69,7 +69,7 @@ "message": "Enter the amount to add from the wallet." }, "outOfFundsAddFunds_label_amountDescriptionRecurring": { - "message": "Enter the amount to add from the wallet. This amount will renew automatically every month (next: $NEXT_RENEW_DATE$}).", + "message": "Enter the amount to add from the wallet. This amount will renew automatically every month (next: $NEXT_RENEW_DATE$).", "placeholders": { "NEXT_RENEW_DATE": { "content": "$1", "example": "Aug 22, 2024" } } From 803fe673cce343fccc310344e2c9d401ad41d4a2 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Tue, 23 Jul 2024 21:07:08 +0530 Subject: [PATCH 8/8] make `state.recurring` boolean only --- src/popup/components/OutOfFunds.tsx | 2 +- src/popup/pages/OutOfFunds_AddFunds.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/popup/components/OutOfFunds.tsx b/src/popup/components/OutOfFunds.tsx index d3d92f01..ca7367bd 100644 --- a/src/popup/components/OutOfFunds.tsx +++ b/src/popup/components/OutOfFunds.tsx @@ -66,7 +66,7 @@ export const OutOfFunds = ({ interface AddFundsProps { info: Pick - recurring: false | 'P1M' + recurring: boolean defaultAmount: AmountValue requestAddFunds: (details: AddFundsPayload) => Promise } diff --git a/src/popup/pages/OutOfFunds_AddFunds.tsx b/src/popup/pages/OutOfFunds_AddFunds.tsx index 742905ea..02d6c435 100644 --- a/src/popup/pages/OutOfFunds_AddFunds.tsx +++ b/src/popup/pages/OutOfFunds_AddFunds.tsx @@ -19,7 +19,7 @@ export const Component = () => { { const res = await addFunds(data) return res