diff --git a/package.json b/package.json index e24e626546..b0888707f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matters-web", - "version": "4.7.0", + "version": "4.8.0", "description": "codebase of Matters' website", "sideEffects": false, "author": "Matters ", diff --git a/src/common/enums/externalLinks.ts b/src/common/enums/externalLinks.ts index 9a7ad45c07..4a82e7d790 100644 --- a/src/common/enums/externalLinks.ts +++ b/src/common/enums/externalLinks.ts @@ -63,10 +63,10 @@ export const GUIDE_LINKS = { }, PWA: { zh_hant: - 'https://matters.news/zh-Hant/@1ampa55ag3/24439-guidance-%E5%A6%82%E4%BD%95%E8%AE%A9%E4%BD%A0%E7%9A%84matters%E4%B9%8B%E6%97%85%E6%9B%B4%E4%BE%BF%E6%8D%B7-bafyreiayiuxi4qc2a7qpgjp3fe42wmaoppqykckcvtq4hiukl5pgs3dn2m', + 'https://matters.news/@hi176/342215-%E6%8C%87%E5%8D%97-%E6%83%B3%E5%9C%A8%E6%89%8B%E6%A9%9F%E4%B8%8A%E6%96%B9%E4%BE%BF%E5%9C%B0%E4%BD%BF%E7%94%A8-matters-%E9%80%99%E8%A3%A1%E6%9C%89%E4%B8%80%E5%80%8B%E5%BE%88%E5%A5%BD%E7%9A%84%E6%96%B9%E6%B3%95-bafyreiclzb52uisucbf7gch2k2ll7mcc6kiivaxxqdqo7drnx5oj4sqvu4', zh_hans: - 'https://matters.news/@1ampa55ag3/24439-guidance-%E5%A6%82%E4%BD%95%E8%AE%A9%E4%BD%A0%E7%9A%84matters%E4%B9%8B%E6%97%85%E6%9B%B4%E4%BE%BF%E6%8D%B7-bafyreiayiuxi4qc2a7qpgjp3fe42wmaoppqykckcvtq4hiukl5pgs3dn2m', - en: 'https://matters.news/en/@1ampa55ag3/24439-guidance-%E5%A6%82%E4%BD%95%E8%AE%A9%E4%BD%A0%E7%9A%84matters%E4%B9%8B%E6%97%85%E6%9B%B4%E4%BE%BF%E6%8D%B7-bafyreiayiuxi4qc2a7qpgjp3fe42wmaoppqykckcvtq4hiukl5pgs3dn2m', + 'https://matters.news/zh-Hans/@hi176/342215-%E6%8C%87%E5%8D%97-%E6%83%B3%E5%9C%A8%E6%89%8B%E6%A9%9F%E4%B8%8A%E6%96%B9%E4%BE%BF%E5%9C%B0%E4%BD%BF%E7%94%A8-matters-%E9%80%99%E8%A3%A1%E6%9C%89%E4%B8%80%E5%80%8B%E5%BE%88%E5%A5%BD%E7%9A%84%E6%96%B9%E6%B3%95-bafyreiclzb52uisucbf7gch2k2ll7mcc6kiivaxxqdqo7drnx5oj4sqvu4', + en: 'https://matters.news/en/@hi176/342215-%E6%8C%87%E5%8D%97-%E6%83%B3%E5%9C%A8%E6%89%8B%E6%A9%9F%E4%B8%8A%E6%96%B9%E4%BE%BF%E5%9C%B0%E4%BD%BF%E7%94%A8-matters-%E9%80%99%E8%A3%A1%E6%9C%89%E4%B8%80%E5%80%8B%E5%BE%88%E5%A5%BD%E7%9A%84%E6%96%B9%E6%B3%95-bafyreiclzb52uisucbf7gch2k2ll7mcc6kiivaxxqdqo7drnx5oj4sqvu4', }, RSS: { zh_hant: diff --git a/src/common/enums/text.ts b/src/common/enums/text.ts index cbd484f036..3987e198be 100644 --- a/src/common/enums/text.ts +++ b/src/common/enums/text.ts @@ -100,6 +100,7 @@ export const TEXT = { editUserProfile: '編輯資料', email: '電子信箱', emptySearchResults: '不好意思,什麼都沒搜到', + enterCustomAmount: '輸入自訂金額', enterDisplayName: '請輸入姓名', enterEmail: '請輸入電子信箱', enterNewEmail: '請輸入新電子信箱', @@ -138,6 +139,7 @@ export const TEXT = { FORBIDDEN_BY_TARGET_STATE: '你無法對此對象進行該操作', FORBIDDEN: '你尚無權限進行該操作', forgetPassword: '忘記密碼', + forgetPaymentPassword: '忘記交易密碼', frequentSearch: '熱門搜尋', guide: '玩轉 Matters 實用指南', help: '說明', @@ -469,6 +471,7 @@ export const TEXT = { editUserProfile: '编辑资料', email: '邮箱', emptySearchResults: '不好意思,什么都没搜到', + enterCustomAmount: '输入自订金额', enterDisplayName: '请输入姓名', enterEmail: '请输入邮箱', enterNewEmail: '请输入新邮箱', @@ -507,6 +510,7 @@ export const TEXT = { FORBIDDEN_BY_TARGET_STATE: '你无法对此对象进行该操作', FORBIDDEN: '你尚无权限进行该操作', forgetPassword: '忘记密码', + forgetPaymentPassword: '忘记交易密码', frequentSearch: '热门搜索', guide: '玩转 Matters 实用指南', help: '说明', @@ -844,6 +848,7 @@ export const TEXT = { editUserProfile: 'Edit', email: 'Email', emptySearchResults: 'Result not found', + enterCustomAmount: 'Enter a custom amount', enterDisplayName: 'Display Name', enterEmail: 'Email', enterNewEmail: 'New Email', @@ -883,6 +888,7 @@ export const TEXT = { 'You do not have permissionn to perform this operation', FORBIDDEN: 'You do not have permission to perform this operation', forgetPassword: 'Forget Password', + forgetPaymentPassword: 'Forget Payment Password', frequentSearch: 'Search trends', guide: 'Explore Matters', help: 'Help', @@ -908,7 +914,7 @@ export const TEXT = { 'Adding articles to a collection helps readers find your articles.', hintPassword: 'Minimum 8 characters. Uppercase/lowercase letters, numbers and symbols are allowed', - hintPaymentPassword: 'Enter a 6-digit transaction password.', + hintPaymentPassword: 'Enter a 6-digit payment password.', hintPaymentPointer: 'The wallet address starts with "$".', hintTerm: 'We have amended or modified our Terms and Privacy Policy. Agree and accept all Terms to continue using our Services.', diff --git a/src/common/styles/mixins.css b/src/common/styles/mixins.css index af3a505678..1071ab4990 100644 --- a/src/common/styles/mixins.css +++ b/src/common/styles/mixins.css @@ -167,6 +167,43 @@ } } +@define-mixin form-input-round { + @mixin all-transition; + + height: var(--input-height); + padding: var(--spacing-base); + border: 1px solid var(--color-grey-light); + border-radius: var(--spacing-x-tight); + caret-color: var(--color-matters-green); + + &:focus, + &.focus { + background-color: var(--color-green-lighter); + border-color: var(--color-matters-green); + } + &.error { + border-color: var(--color-red); + + &:focus, + &.focus { + background-color: transparent; + border-color: var(--color-red); + } + } + + /* Chrome, Safari, Edge, Opera */ + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + margin: 0; + -webkit-appearance: none; + } + + /* Firefox */ + &[type='number'] { + -moz-appearance: textfield; + } +} + /* Components ========================================================================== */ @define-mixin feed-footer-actions { diff --git a/src/common/utils/form/validate.ts b/src/common/utils/form/validate.ts index 1581cf081f..4a97326e8a 100644 --- a/src/common/utils/form/validate.ts +++ b/src/common/utils/form/validate.ts @@ -343,7 +343,11 @@ export const validateAmount = (value: number, lang: Language) => { } } -export const validateDonationAmount = (value: number, lang: Language) => { +export const validateDonationAmount = ( + value: number, + balance: number, + lang: Language +) => { if (typeof value !== 'number') { return translate({ id: 'required', lang }) } @@ -352,6 +356,16 @@ export const validateDonationAmount = (value: number, lang: Language) => { return translate({ zh_hant: '請選擇或輸入金額', zh_hans: '请选择或输入金额', + en: 'Please select or enter amount', + lang, + }) + } + + if (balance < value) { + return translate({ + zh_hant: '自訂金額大於餘額', + zh_hans: '自订金额大于余额', + en: 'Custom amount is greater than your balance', lang, }) } diff --git a/src/components/Dialogs/DonationDialog/index.tsx b/src/components/Dialogs/DonationDialog/index.tsx index 5fba7dcc04..88508ee93f 100644 --- a/src/components/Dialogs/DonationDialog/index.tsx +++ b/src/components/Dialogs/DonationDialog/index.tsx @@ -121,12 +121,14 @@ const BaseDonationDialog = ({ openDialog: baseOpenDialog, closeDialog: baseCloseDialog, } = useDialogSwitch(true) - const { currStep, forward, back, reset } = useStep(defaultStep) + const { currStep, forward, back } = useStep(defaultStep) const [windowRef, setWindowRef] = useState(undefined) const [amount, setAmount] = useState(0) const [currency, setCurrency] = useState(CURRENCY.HKD) const [payToTx, setPayToTx] = useState>() + const [tabUrl, setTabUrl] = useState('') + const [tx, setTx] = useState() const openDialog = () => { forward(defaultStep) @@ -145,7 +147,7 @@ const BaseDonationDialog = ({ forward( viewer.status?.hasPaymentPassword ? 'confirm' : 'setPaymentPassword' ) - } else if (values.currency === CURRENCY.USDT) { + } else { forward('confirm') } } @@ -235,14 +237,7 @@ const BaseDonationDialog = ({ {isCurrencyChoice && ( { - reset(defaultStep) - closeDialog() - }} - defaultCurrency={currency} - openTabCallback={setAmountOpenTabCallback} recipient={recipient} - submitCallback={setAmountCallback} switchToSetAmount={(c: CURRENCY) => { setCurrency(c) forward('setAmount') @@ -250,7 +245,6 @@ const BaseDonationDialog = ({ switchToWalletSelect={() => { forward('walletSelect') }} - targetId={targetId} /> )} @@ -269,9 +263,7 @@ const BaseDonationDialog = ({ {isSetAmount && ( { @@ -280,6 +272,8 @@ const BaseDonationDialog = ({ switchToAddCredit={() => { forward('addCredit') }} + setTabUrl={setTabUrl} + setTx={setTx} targetId={targetId} /> )} @@ -294,6 +288,9 @@ const BaseDonationDialog = ({ switchToResetPassword={() => forward('resetPassword')} switchToCurrencyChoice={() => forward('currencyChoice')} targetId={targetId} + openTabCallback={setAmountOpenTabCallback} + tabUrl={tabUrl} + tx={tx} /> )} diff --git a/src/components/Form/AmountInput/index.tsx b/src/components/Form/AmountInput/index.tsx index 60f0f0e8dd..56bd152bf2 100644 --- a/src/components/Form/AmountInput/index.tsx +++ b/src/components/Form/AmountInput/index.tsx @@ -54,11 +54,12 @@ const AmountInput = forwardRef( extraButton={extraButton} /> - + {currency} props... @@ -28,6 +28,16 @@ interface BaseOptionProps { name: string } +type CustomAmountProps = { + customAmount: { + error?: string + hint?: React.ReactNode + } & React.DetailedHTMLProps< + React.InputHTMLAttributes, + HTMLInputElement + > +} + type AmountOptionProps = { amount: number } & BaseOptionProps & @@ -37,14 +47,16 @@ type AmountOptionProps = { HTMLInputElement > -type AmountRadioInputProps = { +type ComposedAmountInputProps = { amounts: { [key in CURRENCY]: number[] } + lang: Language } & BaseOptionProps & Omit & React.DetailedHTMLProps< React.InputHTMLAttributes, HTMLInputElement - > + > & + CustomAmountProps const AmountOption: React.FC = ({ amount, @@ -67,7 +79,7 @@ const AmountOption: React.FC = ({ const isActive = value === amount const amountClasses = classNames({ - amount: true, + ['radio-input-item']: true, active: isActive, 'u-area-disable': disabled || isBalanceInsufficient, }) @@ -105,7 +117,7 @@ const AmountOption: React.FC = ({ ) } -const AmountRadioInput: React.FC = ({ +const ComposedAmountInput: React.FC = ({ currency, balance, amounts, @@ -114,6 +126,9 @@ const AmountRadioInput: React.FC = ({ hint, error, + lang, + customAmount, + ...inputProps }) => { const fieldMsgId = `field-msg-${name}` @@ -128,19 +143,46 @@ const AmountRadioInput: React.FC = ({ name, } - return ( - -
    - {options.map((option) => ( - - ))} -
+ const { + error: customAmountError, + hint: customAmountHint, + ...customAmountInputProps + } = customAmount - + return ( +
+ +
    + {options.map((option) => ( + + ))} +
+ + {customAmount && ( +
+ + + {customAmountHint &&

{customAmountHint}

} +
+ )} + + +
- +
) } -export default AmountRadioInput +export default ComposedAmountInput diff --git a/src/components/Form/AmountRadioInput/styles.css b/src/components/Form/ComposedAmountInput/styles.css similarity index 59% rename from src/components/Form/AmountRadioInput/styles.css rename to src/components/Form/ComposedAmountInput/styles.css index deb22ac8eb..5509385055 100644 --- a/src/components/Form/AmountRadioInput/styles.css +++ b/src/components/Form/ComposedAmountInput/styles.css @@ -1,17 +1,23 @@ -.amount-options { +.amount-input { + margin-bottom: var(--spacing-loose); +} + +.radio-input-options { @mixin flex-start-space-between; flex-wrap: wrap; } -.amount { +.radio-input-item { + @mixin all-transition; + display: flex; flex: 0 0 30%; font-size: var(--font-size-lg); font-weight: var(--font-weight-medium); color: var(--color-black); background: var(--color-white); - border: 2px solid var(--color-line-grey-light); + border: 1px solid var(--color-line-grey-light); border-radius: var(--spacing-xx-tight); &.active { @@ -32,3 +38,19 @@ text-align: center; } } + +.custom-input { + margin-top: var(--spacing-base); + + & input { + @mixin form-input-round; + + text-align: center; + } + + & .hint { + padding-top: var(--spacing-loose); + font-size: var(--font-size-xs); + color: var(--color-grey-dark); + } +} diff --git a/src/components/Form/Field/Content/index.tsx b/src/components/Form/Field/Content/index.tsx index 78bbe4d843..af373f58a3 100644 --- a/src/components/Form/Field/Content/index.tsx +++ b/src/components/Form/Field/Content/index.tsx @@ -1,13 +1,27 @@ +import classNames from 'classnames' + import styles from './styles.css' -const Content: React.FC> = ({ +type ContentProps = { + noMargin?: boolean +} + +const Content: React.FC> = ({ + noMargin, children, -}) => ( -
- {children} +}) => { + const contentClass = classNames({ + 'input-container': true, + 'no-margin': noMargin, + }) + + return ( +
+ {children} - -
-) + +
+ ) +} export default Content diff --git a/src/components/Form/Field/Content/styles.css b/src/components/Form/Field/Content/styles.css index 98fcafec07..178c3f41be 100644 --- a/src/components/Form/Field/Content/styles.css +++ b/src/components/Form/Field/Content/styles.css @@ -5,4 +5,8 @@ section { @media (--sm-up) { margin: 0; } + + &.no-margin { + margin: 0; + } } diff --git a/src/components/Form/PinInput/Item.tsx b/src/components/Form/PinInput/Item.tsx index ed98ccc74f..e806f4266c 100644 --- a/src/components/Form/PinInput/Item.tsx +++ b/src/components/Form/PinInput/Item.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames' import { forwardRef } from 'react' import { KEYCODES } from '~/common/enums' @@ -5,6 +6,7 @@ import { KEYCODES } from '~/common/enums' import styles from './styles.css' type ItemProps = { + error: boolean onChange: (value: string) => void onBackspace: () => void onPaste: (event: React.ClipboardEvent) => void @@ -17,7 +19,10 @@ type ItemProps = { > const Item = forwardRef( - ({ onPaste, onChange, onBackspace, ...inputProps }: ItemProps, ref: any) => { + ( + { error, onPaste, onChange, onBackspace, ...inputProps }: ItemProps, + ref: any + ) => { const handleChange = (event: React.ChangeEvent) => { event.preventDefault() onChange(event.target.value.slice(-1)) @@ -36,12 +41,16 @@ const Item = forwardRef( } const value = ((inputProps.value as string) || '').slice(-1) + const pinItemClasses = classNames({ + 'pin-input-item': true, + error: !!error, + }) return ( <> = ({ value={val} id={`field-${name}-${index + 1}`} ref={itemRefs[index]} + error={!!error} onChange={(v: string) => onItemChange(v, index)} onPaste={(event: React.ClipboardEvent) => onItemPaste(event, index) diff --git a/src/components/Form/PinInput/styles.css b/src/components/Form/PinInput/styles.css index a99da93560..5f2533cb98 100644 --- a/src/components/Form/PinInput/styles.css +++ b/src/components/Form/PinInput/styles.css @@ -6,6 +6,8 @@ margin: 0 auto; & .pin-input-item { + @mixin all-transition; + display: flex; width: 16.6%; height: 3rem; @@ -13,18 +15,15 @@ line-height: 1.5rem; color: var(--color-black); text-align: center; - border: 2px solid var(--color-grey-light); + border: 1px solid var(--color-grey-light); + border-radius: 4px; & + .pin-input-item { - border-left: 0; - } - - &:first-child { - border-radius: var(--spacing-xx-tight) 0 0 var(--spacing-xx-tight); + margin-left: var(--spacing-tight); } - &:last-child { - border-radius: 0 var(--spacing-xx-tight) var(--spacing-xx-tight) 0; + &.error { + border-color: var(--color-red); } } diff --git a/src/components/Form/index.tsx b/src/components/Form/index.tsx index a1ee77d6c7..05b6363c8d 100644 --- a/src/components/Form/index.tsx +++ b/src/components/Form/index.tsx @@ -1,6 +1,6 @@ import AmountInput from './AmountInput' -import AmountRadioInput from './AmountRadioInput' import CheckBox from './CheckBox' +import ComposedAmountInput from './ComposedAmountInput' import CurrencyRadioInput from './CurrencyRadioInput' import DropdownInput from './DropdownInput' import Field from './Field' @@ -28,7 +28,7 @@ export const Form: React.FC & { List: typeof List Field: typeof Field Select: typeof Select - AmountRadioInput: typeof AmountRadioInput + ComposedAmountInput: typeof ComposedAmountInput CurrencyRadioInput: typeof CurrencyRadioInput } = ({ noBackground, children, ...formProps }) => (
= ({ likerId }) => { } > diff --git a/src/components/Forms/PaymentForm/PayTo/Confirm/index.tsx b/src/components/Forms/PaymentForm/PayTo/Confirm/index.tsx index 5a65aba6c0..d84bdd996a 100644 --- a/src/components/Forms/PaymentForm/PayTo/Confirm/index.tsx +++ b/src/components/Forms/PaymentForm/PayTo/Confirm/index.tsx @@ -8,6 +8,7 @@ import { Button, Dialog, Form, + IconExternalLink16, LanguageContext, Spinner, TextIcon, @@ -25,9 +26,17 @@ import PaymentInfo from '../../PaymentInfo' import styles from './styles.css' import { UserDonationRecipient } from '~/components/Dialogs/DonationDialog/__generated__/UserDonationRecipient' -import { PayTo as PayToMutate } from '~/components/GQL/mutations/__generated__/PayTo' +import { + PayTo as PayToMutate, + PayTo_payTo_transaction as PayToTx, +} from '~/components/GQL/mutations/__generated__/PayTo' import { WalletBalance } from '~/components/GQL/queries/__generated__/WalletBalance' +interface SetAmountOpenTabCallbackValues { + window: Window + transaction: PayToTx +} + interface FormProps { amount: number currency: CURRENCY @@ -37,6 +46,9 @@ interface FormProps { switchToSetAmount: () => void switchToResetPassword: () => void switchToCurrencyChoice: () => void + openTabCallback: (values: SetAmountOpenTabCallbackValues) => void + tabUrl?: string + tx?: PayToTx } interface FormValues { @@ -52,6 +64,9 @@ const Confirm: React.FC = ({ switchToSetAmount, switchToResetPassword, switchToCurrencyChoice, + openTabCallback, + tabUrl, + tx, }) => { const formId = 'pay-to-confirm-form' @@ -190,7 +205,11 @@ const Confirm: React.FC = ({ {currency === CURRENCY.HKD && !isWalletInsufficient && ( <>

- +

{InnerForm} @@ -207,7 +226,7 @@ const Confirm: React.FC = ({ textColor="grey" onClick={switchToResetPassword} > - ? + ? )} {currency === CURRENCY.USDT && ( @@ -219,6 +238,23 @@ const Confirm: React.FC = ({ )} + {currency === CURRENCY.LIKE && ( + { + const payWindow = window.open(tabUrl, '_blank') + if (payWindow && tx) { + openTabCallback({ window: payWindow, transaction: tx }) + } + }} + icon={} + > + + + )} ) diff --git a/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/LikeCoinChoice.tsx b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/LikeCoinChoice.tsx new file mode 100644 index 0000000000..ade159c4fe --- /dev/null +++ b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/LikeCoinChoice.tsx @@ -0,0 +1,110 @@ +import { useContext } from 'react' + +import { + Button, + CurrencyFormatter, + IconLikeCoin40, + TextIcon, + Translate, + ViewerContext, +} from '~/components' + +import { PATHS } from '~/common/enums' +import { formatAmount } from '~/common/utils' + +import styles from './styles.css' + +import { UserDonationRecipient } from '~/components/Dialogs/DonationDialog/__generated__/UserDonationRecipient' + +type LikeCoinChoiceProps = { + balance: number + recipient: UserDonationRecipient + switchToSetAmount: () => void +} + +const IconLikeDisabled = () => ( + } + size="md" + spacing="xtight" + color="grey" + > + LikeCoin + +) + +const LikeCoinChoice: React.FC = ({ + balance, + recipient, + switchToSetAmount, +}) => { + const viewer = useContext(ViewerContext) + const hasLikerId = !!viewer.liker.likerId + const canReceiveLike = !!recipient.liker.likerId + + if (!hasLikerId) { + return ( +
+ + + + + +
+ ) + } + + if (!canReceiveLike) { + return ( +
+ + + + + + + +
+ ) + } + + return ( +
+ } + size="md" + spacing="xtight" + > + LikeCoin + + + + + +
+ ) +} + +export default LikeCoinChoice diff --git a/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/USDTChoice.tsx b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/USDTChoice.tsx index 58224a1b13..7bdee373bc 100644 --- a/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/USDTChoice.tsx +++ b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/USDTChoice.tsx @@ -4,6 +4,7 @@ import { useAccount } from 'wagmi' import { Button, CurrencyFormatter, + IconSpinner16, IconUSDT40, IconUSDTActive40, TextIcon, @@ -21,7 +22,7 @@ import { UserDonationRecipient } from '~/components/Dialogs/DonationDialog/__gen interface FormProps { recipient: UserDonationRecipient - switchToSetAmount: (c: CURRENCY) => void + switchToSetAmount: () => void switchToWalletSelect: () => void } @@ -33,7 +34,8 @@ const USDTChoice: React.FC = ({ const viewer = useContext(ViewerContext) const { address } = useAccount() - const { data: balanceUSDTData } = useBalanceUSDT({}) + const { data: balanceUSDTData, isLoading: balanceUSDTLoading } = + useBalanceUSDT({}) const balanceUSDT = parseFloat(balanceUSDTData?.formatted || '0') const curatorAddress = viewer.info.ethAddress @@ -68,9 +70,7 @@ const USDTChoice: React.FC = ({
{ - switchToSetAmount(CURRENCY.USDT) - }} + onClick={switchToSetAmount} > } @@ -79,10 +79,14 @@ const USDTChoice: React.FC = ({ > Tether - + {balanceUSDTLoading ? ( + + ) : ( + + )}
diff --git a/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/index.tsx b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/index.tsx index 55d048ea54..e39391b12a 100644 --- a/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/index.tsx +++ b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/index.tsx @@ -2,58 +2,37 @@ import { useQuery } from '@apollo/react-hooks' import _pickBy from 'lodash/pickBy' import { - Avatar, CurrencyFormatter, Dialog, IconFiatCurrency40, - IconLikeCoin40, Spinner, TextIcon, Translate, + UserDigest, } from '~/components' import WALLET_BALANCE from '~/components/GQL/queries/walletBalance' import { PAYMENT_CURRENCY as CURRENCY } from '~/common/enums' import { formatAmount } from '~/common/utils' +import LikeCoinChoice from './LikeCoinChoice' import styles from './styles.css' import Tips from './Tips' import USDTChoice from './USDTChoice' import { UserDonationRecipient } from '~/components/Dialogs/DonationDialog/__generated__/UserDonationRecipient' -import { PayTo_payTo_transaction as PayToTx } from '~/components/GQL/mutations/__generated__/PayTo' import { WalletBalance } from '~/components/GQL/queries/__generated__/WalletBalance' -interface SetAmountCallbackValues { - amount: number - currency: CURRENCY -} - -interface SetAmountOpenTabCallbackValues { - window: Window - transaction: PayToTx -} - interface FormProps { - closeDialog: () => void - defaultCurrency?: CURRENCY - openTabCallback: (values: SetAmountOpenTabCallbackValues) => void recipient: UserDonationRecipient - submitCallback: (values: SetAmountCallbackValues) => void switchToSetAmount: (c: CURRENCY) => void switchToWalletSelect: () => void - targetId: string } const CurrencyChoice: React.FC = ({ - closeDialog, - defaultCurrency, - openTabCallback, recipient, - submitCallback, switchToSetAmount, switchToWalletSelect, - targetId, }) => { // HKD balance const { data, loading } = useQuery(WALLET_BALANCE, { @@ -70,18 +49,26 @@ const CurrencyChoice: React.FC = ({ - - {recipient.displayName} + - + {/* USDT */} switchToSetAmount(CURRENCY.USDT)} switchToWalletSelect={switchToWalletSelect} /> @@ -104,25 +91,11 @@ const CurrencyChoice: React.FC = ({ {/* LikeCoin */} -
{ - switchToSetAmount(CURRENCY.LIKE) - }} - > - } - size="md" - spacing="xtight" - > - - - -
+ switchToSetAmount(CURRENCY.LIKE)} + /> diff --git a/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/styles.css b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/styles.css index c400ba6220..a21e9b6d08 100644 --- a/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/styles.css +++ b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/styles.css @@ -7,18 +7,16 @@ @mixin flex-center-start; margin: var(--spacing-base) 0; - font-size: 1rem; + font-size: var(--font-size-md-s); line-height: 1.5rem; - & .userInfo { - @mixin inline-flex-center-start; + & :global(> *) { + flex-shrink: 0; + } - gap: var(--spacing-xx-tight); + & .userInfo { + flex-shrink: 1; margin: 0 var(--spacing-x-tight); - - & .userName { - font-weight: var(--font-weight-semibold); - } } } diff --git a/src/components/Forms/PaymentForm/PayTo/NoLiker.tsx b/src/components/Forms/PaymentForm/PayTo/NoLiker.tsx deleted file mode 100644 index 9df52c7c3f..0000000000 --- a/src/components/Forms/PaymentForm/PayTo/NoLiker.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { Dialog, Translate } from '~/components' - -import { - OPEN_LIKE_COIN_DIALOG, - PAYMENT_CURRENCY as CURRENCY, -} from '~/common/enums' - -interface NoLikerId { - canPayLike: boolean - canReceiveLike: boolean -} - -export const NoLikerIdMessage = ({ canPayLike, canReceiveLike }: NoLikerId) => { - if (!canPayLike) { - return ( - - ) - } - - if (!canReceiveLike) { - return ( - - ) - } - - return null -} - -export const NoLikerIdButton = ({ - canPayLike, - canReceiveLike, - closeDialog, - setFieldValue, -}: NoLikerId & { - closeDialog: () => void - setFieldValue: (field: string, value: any) => void -}) => { - if (!canPayLike) { - return ( - { - window.dispatchEvent(new CustomEvent(OPEN_LIKE_COIN_DIALOG, {})) - closeDialog() - }} - > - - - ) - } - - if (!canReceiveLike) { - return ( - setFieldValue('currency', CURRENCY.HKD)} - > - - - ) - } - - return null -} diff --git a/src/components/Forms/PaymentForm/PayTo/SetAmount.tsx b/src/components/Forms/PaymentForm/PayTo/SetAmount.tsx deleted file mode 100644 index 4540774e94..0000000000 --- a/src/components/Forms/PaymentForm/PayTo/SetAmount.tsx +++ /dev/null @@ -1,648 +0,0 @@ -import { useQuery } from '@apollo/react-hooks' -import { BigNumber } from 'ethers' -import { useFormik } from 'formik' -import _pickBy from 'lodash/pickBy' -import { useContext, useEffect, useRef, useState } from 'react' -import { useAccount, useDisconnect, useNetwork, useSwitchNetwork } from 'wagmi' - -import { - Button, - CopyToClipboard, - Dialog, - Form, - IconCopy16, - IconExternalLink16, - IconFiatCurrency40, - IconInfo24, - IconLikeCoin40, - IconUSDTActive40, - LanguageContext, - Spinner, - TextIcon, - Translate, - useAllowanceUSDT, - useApproveUSDT, - useBalanceUSDT, - useMutation, - ViewerContext, -} from '~/components' -import PAY_TO from '~/components/GQL/mutations/payTo' -import WALLET_BALANCE from '~/components/GQL/queries/walletBalance' - -import { - GUIDE_LINKS, - PAYMENT_CURRENCY as CURRENCY, - PAYMENT_MAXIMUM_PAYTO_AMOUNT, -} from '~/common/enums' -import { - formatAmount, - maskAddress, - numRound, - translate, - validateCurrency, - validateDonationAmount, -} from '~/common/utils' - -import CivicLikerButton from './CivicLikerButton' -import { NoLikerIdButton, NoLikerIdMessage } from './NoLiker' -import styles from './styles.css' -import WhyPolygonDialog from './WhyPolygonDialog' - -import { UserDonationRecipient } from '~/components/Dialogs/DonationDialog/__generated__/UserDonationRecipient' -import { - PayTo as PayToMutate, - PayTo_payTo_transaction as PayToTx, -} from '~/components/GQL/mutations/__generated__/PayTo' -import { WalletBalance } from '~/components/GQL/queries/__generated__/WalletBalance' - -interface SetAmountCallbackValues { - amount: number - currency: CURRENCY -} - -interface SetAmountOpenTabCallbackValues { - window: Window - transaction: PayToTx -} - -interface FormProps { - closeDialog: () => void - defaultCurrency?: CURRENCY - openTabCallback: (values: SetAmountOpenTabCallbackValues) => void - recipient: UserDonationRecipient - submitCallback: (values: SetAmountCallbackValues) => void - switchToCurrencyChoice: () => void - switchToAddCredit: () => void - targetId: string -} - -interface FormValues { - amount: number - customAmount: number - currency: CURRENCY -} - -const AMOUNT_DEFAULT = { - [CURRENCY.USDT]: 3.0, - [CURRENCY.HKD]: 10, - [CURRENCY.LIKE]: 100, -} - -const AMOUNT_OPTIONS = { - [CURRENCY.USDT]: [1.0, 3.0, 5.0, 10.0, 20.0, 35.0], - [CURRENCY.HKD]: [5, 10, 30, 50, 100, 300], - [CURRENCY.LIKE]: [50, 100, 150, 500, 1000, 1500], -} - -const SetAmount: React.FC = ({ - closeDialog, - defaultCurrency, - openTabCallback, - recipient, - submitCallback, - switchToCurrencyChoice, - switchToAddCredit, - targetId, -}) => { - // contexts - const viewer = useContext(ViewerContext) - const { lang } = useContext(LanguageContext) - const { address } = useAccount() - const { disconnect } = useDisconnect() - const { chain } = useNetwork() - const { chains, switchNetwork } = useSwitchNetwork() - - const isConnectedAddress = - viewer.info.ethAddress?.toLowerCase() === address?.toLowerCase() - const isUnsupportedNetwork = !!chain?.unsupported - const targetChainName = chains[0]?.name - const targetChainId = chains[0]?.id - const switchToTargetNetwork = async () => { - if (!switchNetwork) return - - switchNetwork(targetChainId) - } - - // states - const [locked, setLocked] = useState(false) - const [tabUrl, setTabUrl] = useState('') - const [tx, setTx] = useState() - - const [payTo] = useMutation(PAY_TO) - - // HKD balance - const { data, loading } = useQuery(WALLET_BALANCE, { - fetchPolicy: 'network-only', - }) - - // USDT balance & allowance - const [approveConfirming, setApproveConfirming] = useState(false) - const { - data: allowanceData, - refetch: refetchAllowanceData, - isLoading: allowanceLoading, - } = useAllowanceUSDT() - const { - data: approveData, - isLoading: approving, - write: approveWrite, - } = useApproveUSDT() - const { data: balanceUSDTData } = useBalanceUSDT({}) - - const allowanceUSDT = allowanceData || BigNumber.from('0') - const balanceUSDT = parseFloat(balanceUSDTData?.formatted || '0') - const balanceHKD = data?.viewer?.wallet.balance.HKD || 0 - const balanceLike = data?.viewer?.liker.total || 0 - - // forms - const { - errors, - handleBlur, - handleSubmit, - isValid, - isSubmitting, - setFieldValue, - values, - } = useFormik({ - initialValues: { - amount: AMOUNT_DEFAULT[defaultCurrency || CURRENCY.HKD], - customAmount: 0, - currency: defaultCurrency || CURRENCY.HKD, - }, - validate: ({ amount, customAmount, currency }) => - _pickBy({ - amount: validateDonationAmount(customAmount || amount, lang), - currency: validateCurrency(currency, lang), - }), - onSubmit: async ({ amount, customAmount, currency }, { setSubmitting }) => { - const submitAmount = customAmount || amount - try { - if (currency === CURRENCY.LIKE) { - const result = await payTo({ - variables: { - amount: submitAmount, - currency, - purpose: 'donation', - recipientId: recipient.id, - targetId, - }, - }) - const redirectUrl = result?.data?.payTo.redirectUrl - const transaction = result?.data?.payTo.transaction - if (!redirectUrl || !transaction) { - throw new Error() - } - setLocked(true) - setTabUrl(redirectUrl) - setTx(transaction) - } - setSubmitting(false) - submitCallback({ amount: submitAmount, currency }) - } catch (error) { - setSubmitting(false) - } - }, - }) - - const formId = 'pay-to-set-amount-form' - const customInputRef: React.RefObject | null = useRef(null) - - const isUSDT = values.currency === CURRENCY.USDT - const isHKD = values.currency === CURRENCY.HKD - const isLike = values.currency === CURRENCY.LIKE - const canPayLike = isLike && !!viewer.liker.likerId - const canReceiveLike = isLike && !!recipient.liker.likerId - const canProcess = isUSDT || isHKD || (canPayLike && canReceiveLike) - const maxAmount = isHKD ? PAYMENT_MAXIMUM_PAYTO_AMOUNT.HKD : Infinity - const isBalanceInsufficient = - (isUSDT ? balanceUSDT : isHKD ? balanceHKD : balanceLike) < - (values.customAmount || values.amount) - - /** - * useEffect Hooks - */ - // go back to previous step if wallet is locked - useEffect(() => { - if (!address && defaultCurrency === CURRENCY.USDT) { - switchToCurrencyChoice() - } - }, [address]) - - // USDT approval - useEffect(() => { - ;(async () => { - if (approveData) { - setApproveConfirming(true) - await approveData.wait() - refetchAllowanceData() - setApproveConfirming(false) - } - })() - }, [approveData]) - - /** - * Rendering - */ - const InnerForm = ( - -
-
- {isUSDT && ( - } - size="md" - spacing="xtight" - weight="md" - > - - - )} - {isHKD && ( - } - size="md" - spacing="xtight" - weight="md" - > - - - )} - {isLike && ( - } - size="md" - spacing="xtight" - weight="md" - > - LikeCoin - - )} - - - -
-
- {isUSDT && !isConnectedAddress && ( - - )} - {isUSDT && isConnectedAddress && ( - <> - <> - {isUnsupportedNetwork ? ( - - ) : ( - - {targetChainName} - - )} - - - - {({ openDialog }) => ( - - )} - - - )} -
-
- -
- - - {isUSDT && {formatAmount(balanceUSDT)} USDT} - {isHKD && {formatAmount(balanceHKD)} HKD} - {isLike && {formatAmount(balanceLike, 0)} LIKE} - - {isHKD && ( - - )} - {isUSDT && balanceUSDT <= 0 && ( - - - - - - )} -
- - {canProcess && ( - { - const value = parseInt(e.target.value, 10) || 0 - await setFieldValue('amount', value, false) - await setFieldValue('customAmount', 0, true) - e.target.blur() - - if (customInputRef.current) { - customInputRef.current.value = '' - } - }} - /> - )} - - {canProcess && ( -
- { - let value = e.target.valueAsNumber || 0 - if (isHKD) { - value = Math.floor(value) - } - if (isUSDT) { - value = numRound(value, 2) - } - value = Math.abs(Math.min(value, maxAmount)) - - await setFieldValue('customAmount', value, false) - await setFieldValue('amount', 0, true) - - // correct the input value if not equal - const $el = customInputRef.current - const rawValue = parseFloat(e.target.value) - if ($el && rawValue !== value) { - $el.value = value <= 0 ? '' : value - $el.type = 'text' - $el.setSelectionRange($el.value.length, $el.value.length) - $el.type = 'number' - } - }} - ref={customInputRef} - autoComplete="nope" - autoCorrect="off" - autoCapitalize="off" - spellCheck="false" - /> - {isHKD && ( -
- - - -
- )} -
- )} - - {!canProcess && ( -
- -
- )} - - - ) - - if (loading) { - return - } - - return ( - <> - {InnerForm} - - - {canProcess && !isUSDT && !locked && ( - <> - {isLike && recipient.liker.likerId && ( - - )} - - {isBalanceInsufficient && isHKD ? ( - - - - ) : ( - - - - )} - - )} - - {isUSDT && ( - <> - {!isConnectedAddress && ( - <> -

- - - - -

- - { - disconnect() - }} - > - - - - - - )} - - {isConnectedAddress && isUnsupportedNetwork && ( - { - switchToTargetNetwork() - }} - > - - {targetChainName} - - )} - - {isConnectedAddress && - !isUnsupportedNetwork && - allowanceUSDT.lte(0) && ( - <> - { - if (approveWrite) { - approveWrite() - } - }} - > - - - - )} - - {isConnectedAddress && - !isUnsupportedNetwork && - allowanceUSDT.gt(0) && ( - - - - )} - - )} - - {!canProcess && ( - - )} - - {locked && ( - { - const payWindow = window.open(tabUrl, '_blank') - if (payWindow && tx) { - openTabCallback({ window: payWindow, transaction: tx }) - } - }} - icon={} - > - - - )} -
- - ) -} - -export default SetAmount diff --git a/src/components/Forms/PaymentForm/PayTo/SetAmount/ReconnectButton/index.tsx b/src/components/Forms/PaymentForm/PayTo/SetAmount/ReconnectButton/index.tsx new file mode 100644 index 0000000000..d30bcf223f --- /dev/null +++ b/src/components/Forms/PaymentForm/PayTo/SetAmount/ReconnectButton/index.tsx @@ -0,0 +1,67 @@ +import { useContext } from 'react' +import { useDisconnect } from 'wagmi' + +import { + Button, + CopyToClipboard, + Dialog, + IconCopy16, + LanguageContext, + TextIcon, + Translate, + ViewerContext, +} from '~/components' + +import { maskAddress, translate } from '~/common/utils' + +import styles from './styles.css' + +const ReconnectButton = () => { + const viewer = useContext(ViewerContext) + const { lang } = useContext(LanguageContext) + const { disconnect } = useDisconnect() + + return ( + <> +

+ + + + +

+ + { + disconnect() + }} + > + + + + + + ) +} + +export default ReconnectButton diff --git a/src/components/Forms/PaymentForm/PayTo/SetAmount/ReconnectButton/styles.css b/src/components/Forms/PaymentForm/PayTo/SetAmount/ReconnectButton/styles.css new file mode 100644 index 0000000000..29e49f5d6e --- /dev/null +++ b/src/components/Forms/PaymentForm/PayTo/SetAmount/ReconnectButton/styles.css @@ -0,0 +1,6 @@ +.reconnect-hint { + margin-top: var(--spacing-base); + font-size: var(--font-size-xs); + color: var(--color-red); + text-align: center; +} diff --git a/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountBalance/index.tsx b/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountBalance/index.tsx new file mode 100644 index 0000000000..7ebd711e8d --- /dev/null +++ b/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountBalance/index.tsx @@ -0,0 +1,79 @@ +import { useContext } from 'react' + +import { Button, LanguageContext, TextIcon, Translate } from '~/components' + +import { GUIDE_LINKS, PAYMENT_CURRENCY as CURRENCY } from '~/common/enums' +import { formatAmount } from '~/common/utils' + +import styles from './styles.css' + +type SetAmountBalanceProps = { + currency: CURRENCY + balanceUSDT: number + balanceHKD: number + balanceLike: number + isBalanceInsufficient: boolean + switchToAddCredit: () => void +} + +const SetAmountBalance: React.FC = ({ + currency, + balanceUSDT, + balanceHKD, + balanceLike, + isBalanceInsufficient, + switchToAddCredit, +}) => { + const { lang } = useContext(LanguageContext) + + const isUSDT = currency === CURRENCY.USDT + const isHKD = currency === CURRENCY.HKD + const isLike = currency === CURRENCY.LIKE + + return ( +
+ + + {isUSDT && {formatAmount(balanceUSDT)} USDT} + {isHKD && {formatAmount(balanceHKD)} HKD} + {isLike && {formatAmount(balanceLike, 0)} LIKE} + + + {isHKD && ( + + )} + {isUSDT && balanceUSDT <= 0 && ( + + + + + + )} + + +
+ ) +} + +export default SetAmountBalance diff --git a/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountBalance/styles.css b/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountBalance/styles.css new file mode 100644 index 0000000000..0a5aa6f4f5 --- /dev/null +++ b/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountBalance/styles.css @@ -0,0 +1,12 @@ +.set-amount-balance { + @mixin flex-center-space-between; + + padding: var(--spacing-loose) var(--spacing-base) 0; + + & .left { + font-size: 1rem; + font-weight: var(--font-weight-medium); + line-height: 1.5rem; + color: var(--color-black); + } +} diff --git a/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/CurrencyIndicator.tsx b/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/CurrencyIndicator.tsx new file mode 100644 index 0000000000..29581584b2 --- /dev/null +++ b/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/CurrencyIndicator.tsx @@ -0,0 +1,77 @@ +import { + Button, + IconFiatCurrency40, + IconLikeCoin40, + IconUSDTActive40, + TextIcon, + Translate, +} from '~/components' + +import { PAYMENT_CURRENCY as CURRENCY } from '~/common/enums' + +import styles from './styles.css' + +type CurrencyIndicatorProps = { + currency: CURRENCY + switchToCurrencyChoice: () => void +} + +const CurrencyIndicator: React.FC = ({ + currency, + switchToCurrencyChoice, +}) => { + const isUSDT = currency === CURRENCY.USDT + const isHKD = currency === CURRENCY.HKD + const isLike = currency === CURRENCY.LIKE + + return ( +
+ {isUSDT && ( + } + size="md" + spacing="xtight" + weight="md" + > + USDT + + )} + {isHKD && ( + } + size="md" + spacing="xtight" + weight="md" + > + + + )} + {isLike && ( + } + size="md" + spacing="xtight" + weight="md" + > + LikeCoin + + )} + + + + + + +
+ ) +} + +export default CurrencyIndicator diff --git a/src/components/Forms/PaymentForm/PayTo/WhyPolygonDialog/index.tsx b/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/WhyPolygonDialog/index.tsx similarity index 100% rename from src/components/Forms/PaymentForm/PayTo/WhyPolygonDialog/index.tsx rename to src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/WhyPolygonDialog/index.tsx diff --git a/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/index.tsx b/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/index.tsx new file mode 100644 index 0000000000..3c0360202e --- /dev/null +++ b/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/index.tsx @@ -0,0 +1,92 @@ +import { useDisconnect } from 'wagmi' + +import { Button, IconInfo24, TextIcon, Translate } from '~/components' + +import { PAYMENT_CURRENCY as CURRENCY } from '~/common/enums' + +import CurrencyIndicator from './CurrencyIndicator' +import styles from './styles.css' +import WhyPolygonDialog from './WhyPolygonDialog' + +type SetAmountHeaderProps = { + currency: CURRENCY + isConnectedAddress: boolean + isUnsupportedNetwork: boolean + targetChainName: string + switchToCurrencyChoice: () => void + switchToTargetNetwork: () => void +} + +const SetAmountHeader: React.FC = ({ + currency, + isConnectedAddress, + isUnsupportedNetwork, + targetChainName, + switchToCurrencyChoice, + switchToTargetNetwork, +}) => { + const { disconnect } = useDisconnect() + + const isUSDT = currency === CURRENCY.USDT + + return ( +
+ + +
+ {isUSDT && !isConnectedAddress && ( + + )} + {isUSDT && isConnectedAddress && ( + <> + <> + {isUnsupportedNetwork ? ( + + ) : ( + + {targetChainName} + + )} + + + + {({ openDialog }) => ( + + )} + + + )} +
+ + +
+ ) +} + +export default SetAmountHeader diff --git a/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/styles.css b/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/styles.css new file mode 100644 index 0000000000..30f5ac2db7 --- /dev/null +++ b/src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/styles.css @@ -0,0 +1,12 @@ +.set-amount-header { + @mixin flex-center-space-between; + + padding: var(--spacing-base) 0; + margin: 0 var(--spacing-base); + border-bottom: 0.5px solid var(--color-line-grey); +} + +.change-button { + display: inline-block; + margin-left: var(--spacing-x-tight); +} diff --git a/src/components/Forms/PaymentForm/PayTo/SetAmount/index.tsx b/src/components/Forms/PaymentForm/PayTo/SetAmount/index.tsx new file mode 100644 index 0000000000..b44da53205 --- /dev/null +++ b/src/components/Forms/PaymentForm/PayTo/SetAmount/index.tsx @@ -0,0 +1,412 @@ +import { useQuery } from '@apollo/react-hooks' +import { BigNumber } from 'ethers' +import { useFormik } from 'formik' +import _pickBy from 'lodash/pickBy' +import { useContext, useEffect, useRef, useState } from 'react' +import { useAccount, useNetwork, useSwitchNetwork } from 'wagmi' + +import { + Dialog, + Form, + LanguageContext, + Spinner, + Translate, + useAllowanceUSDT, + useApproveUSDT, + useBalanceUSDT, + useMutation, + ViewerContext, +} from '~/components' +import PAY_TO from '~/components/GQL/mutations/payTo' +import WALLET_BALANCE from '~/components/GQL/queries/walletBalance' + +import { + PAYMENT_CURRENCY as CURRENCY, + PAYMENT_MAXIMUM_PAYTO_AMOUNT, +} from '~/common/enums' +import { + numRound, + validateCurrency, + validateDonationAmount, + WALLET_ERROR_MESSAGES, +} from '~/common/utils' + +import CivicLikerButton from '../CivicLikerButton' +import ReconnectButton from './ReconnectButton' +import SetAmountBalance from './SetAmountBalance' +import SetAmountHeader from './SetAmountHeader' + +import { UserDonationRecipient } from '~/components/Dialogs/DonationDialog/__generated__/UserDonationRecipient' +import { + PayTo as PayToMutate, + PayTo_payTo_transaction as PayToTx, +} from '~/components/GQL/mutations/__generated__/PayTo' +import { WalletBalance } from '~/components/GQL/queries/__generated__/WalletBalance' + +interface SetAmountCallbackValues { + amount: number + currency: CURRENCY +} + +interface FormProps { + currency: CURRENCY + recipient: UserDonationRecipient + submitCallback: (values: SetAmountCallbackValues) => void + switchToCurrencyChoice: () => void + switchToAddCredit: () => void + setTabUrl: (url: string) => void + setTx: (tx: PayToTx) => void + targetId: string +} + +interface FormValues { + amount: number + customAmount: number +} + +const AMOUNT_DEFAULT = { + [CURRENCY.USDT]: 3.0, + [CURRENCY.HKD]: 10, + [CURRENCY.LIKE]: 100, +} + +const AMOUNT_OPTIONS = { + [CURRENCY.USDT]: [1.0, 3.0, 5.0, 10.0, 20.0, 35.0], + [CURRENCY.HKD]: [5, 10, 30, 50, 100, 300], + [CURRENCY.LIKE]: [50, 100, 150, 500, 1000, 1500], +} + +const SetAmount: React.FC = ({ + currency, + recipient, + submitCallback, + switchToCurrencyChoice, + switchToAddCredit, + setTabUrl, + setTx, + targetId, +}) => { + const formId = 'pay-to-set-amount-form' + const customInputRef: React.RefObject | null = useRef(null) + const isUSDT = currency === CURRENCY.USDT + const isHKD = currency === CURRENCY.HKD + const isLike = currency === CURRENCY.LIKE + + // contexts + const viewer = useContext(ViewerContext) + const { lang } = useContext(LanguageContext) + const { address } = useAccount() + const { chain } = useNetwork() + const { chains, switchNetwork } = useSwitchNetwork() + + const isConnectedAddress = + viewer.info.ethAddress?.toLowerCase() === address?.toLowerCase() + const isUnsupportedNetwork = !!chain?.unsupported + const targetChainName = chains[0]?.name + const targetChainId = chains[0]?.id + const switchToTargetNetwork = async () => { + if (!switchNetwork) return + + switchNetwork(targetChainId) + } + + // states + const [payTo] = useMutation(PAY_TO) + + // HKD balance + const { data, loading, error } = useQuery(WALLET_BALANCE, { + fetchPolicy: 'network-only', + }) + + // USDT balance & allowance + const [approveConfirming, setApproveConfirming] = useState(false) + const { + data: allowanceData, + refetch: refetchAllowanceData, + isLoading: allowanceLoading, + error: allowanceError, + } = useAllowanceUSDT() + const { + data: approveData, + isLoading: approving, + write: approveWrite, + error: approveError, + } = useApproveUSDT() + const { data: balanceUSDTData, error: balanceUSDTError } = useBalanceUSDT({}) + + const allowanceUSDT = allowanceData || BigNumber.from('0') + const balanceUSDT = parseFloat(balanceUSDTData?.formatted || '0') + const balanceHKD = data?.viewer?.wallet.balance.HKD || 0 + const balanceLike = data?.viewer?.liker.total || 0 + const balance = isUSDT ? balanceUSDT : isHKD ? balanceHKD : balanceLike + const maxAmount = isHKD ? PAYMENT_MAXIMUM_PAYTO_AMOUNT.HKD : Infinity + const networkEerror = + error || + (isUSDT ? allowanceError || balanceUSDTError || approveError : undefined) + ? WALLET_ERROR_MESSAGES[lang].unknown + : '' + + // forms + const { + errors, + handleBlur, + handleSubmit, + isValid, + isSubmitting, + setFieldValue, + values, + } = useFormik({ + initialValues: { + amount: AMOUNT_DEFAULT[currency], + customAmount: 0, + }, + validate: ({ amount, customAmount }) => + _pickBy({ + amount: validateDonationAmount(customAmount || amount, balance, lang), + currency: validateCurrency(currency, lang), + }), + onSubmit: async ({ amount, customAmount }, { setSubmitting }) => { + const submitAmount = customAmount || amount + try { + if (currency === CURRENCY.LIKE) { + const result = await payTo({ + variables: { + amount: submitAmount, + currency, + purpose: 'donation', + recipientId: recipient.id, + targetId, + }, + }) + const redirectUrl = result?.data?.payTo.redirectUrl + const transaction = result?.data?.payTo.transaction + if (!redirectUrl || !transaction) { + throw new Error() + } + setTabUrl(redirectUrl) + setTx(transaction) + } + setSubmitting(false) + submitCallback({ amount: submitAmount, currency }) + } catch (error) { + setSubmitting(false) + } + }, + }) + + const isBalanceInsufficient = balance < (values.customAmount || values.amount) + + /** + * useEffect Hooks + */ + // go back to previous step if wallet is locked + useEffect(() => { + if (!address && currency === CURRENCY.USDT) { + switchToCurrencyChoice() + } + }, [address]) + + // USDT approval + useEffect(() => { + ;(async () => { + if (approveData) { + setApproveConfirming(true) + await approveData.wait() + refetchAllowanceData() + setApproveConfirming(false) + } + })() + }, [approveData]) + + /** + * Rendering + */ + const InnerForm = ( +
+ + + + + { + const value = parseInt(e.target.value, 10) || 0 + await setFieldValue('amount', value, false) + await setFieldValue('customAmount', 0, true) + e.target.blur() + + if (customInputRef.current) { + customInputRef.current.value = '' + } + }} + // custom input + lang={lang} + customAmount={{ + disabled: isUSDT && !isConnectedAddress, + min: 0, + max: maxAmount, + step: isUSDT ? '0.01' : undefined, + onBlur: handleBlur, + onChange: async (e) => { + let value = e.target.valueAsNumber || 0 + if (isHKD) { + value = Math.floor(value) + } + if (isUSDT) { + value = numRound(value, 2) + } + value = Math.abs(Math.min(value, maxAmount)) + + await setFieldValue('customAmount', value, false) + await setFieldValue('amount', 0, true) + + // correct the input value if not equal + const $el = customInputRef.current + const rawValue = parseFloat(e.target.value) + if ($el && rawValue !== value) { + $el.value = value <= 0 ? '' : value + $el.type = 'text' + $el.setSelectionRange($el.value.length, $el.value.length) + $el.type = 'number' + } + }, + error: errors.amount, + hint: isHKD ? ( + + ) : null, + ref: customInputRef, + }} + /> + + ) + + if (loading) { + return + } + + return ( + <> + {InnerForm} + + + {!isUSDT && ( + <> + {isLike && recipient.liker.likerId && ( + + )} + + {isBalanceInsufficient && isHKD ? ( + + + + ) : ( + + + + )} + + )} + + {isUSDT && ( + <> + {!isConnectedAddress && } + + {isConnectedAddress && isUnsupportedNetwork && ( + + + {targetChainName} + + )} + + {isConnectedAddress && + !isUnsupportedNetwork && + allowanceUSDT.lte(0) && ( + <> + { + if (approveWrite) { + approveWrite() + } + }} + > + + + + )} + + {isConnectedAddress && + !isUnsupportedNetwork && + allowanceUSDT.gt(0) && ( + + + + )} + + )} + + + ) +} + +export default SetAmount diff --git a/src/components/Forms/PaymentForm/PayTo/styles.css b/src/components/Forms/PaymentForm/PayTo/styles.css deleted file mode 100644 index 547b699531..0000000000 --- a/src/components/Forms/PaymentForm/PayTo/styles.css +++ /dev/null @@ -1,74 +0,0 @@ -.set-amount-no-liker-id { - @mixin flex-center-all; - - margin: var(--spacing-xx-loose) var(--spacing-xxx-loose); - color: var(--color-grey-dark); - text-align: center; -} - -.set-amount-change-support-way { - @mixin flex-center-space-between; - - padding: var(--spacing-base) 0; - margin: 0 var(--spacing-base); - border-bottom: 0.5px solid var(--color-line-grey); - - & .button { - display: inline-block; - margin-left: var(--spacing-x-tight); - } -} - -.set-amount-balance { - @mixin flex-center-space-between; - - padding: var(--spacing-loose) var(--spacing-base) 0; - - & .left { - font-size: 1rem; - font-weight: var(--font-weight-medium); - line-height: 1.5rem; - color: var(--color-black); - } -} - -.set-amount-custom-amount-input { - padding: 0 var(--spacing-base); - - /* Chrome, Safari, Edge, Opera */ - & input::-webkit-outer-spin-button, - & input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - - /* Firefox */ - & input[type='number'] { - -moz-appearance: textfield; - } - - & input { - height: var(--input-height); - text-align: center; - border: 1px solid var(--color-grey-light); - border-radius: var(--spacing-x-tight); - caret-color: var(--color-matters-green); - - &:focus, - &.focus { - background-color: var(--color-green-lighter); - border-color: var(--color-matters-green); - } - } - - & .footer { - padding-top: var(--spacing-loose); - } -} - -.set-amount-reconnect-footer { - margin-top: var(--spacing-base); - font-size: var(--font-size-xs); - color: var(--color-red); - text-align: center; -} diff --git a/src/components/Forms/PaymentForm/PaymentInfo/index.tsx b/src/components/Forms/PaymentForm/PaymentInfo/index.tsx index d810597a5c..6b44a049e5 100644 --- a/src/components/Forms/PaymentForm/PaymentInfo/index.tsx +++ b/src/components/Forms/PaymentForm/PaymentInfo/index.tsx @@ -57,8 +57,7 @@ const PaymentInfo: React.FC = ({ } spacing="xxtight" - size="xs" - weight="md" + size="md" color="green" textPlacement="left" > @@ -77,12 +76,15 @@ const PaymentInfo: React.FC = ({ )} +

{currency} {formatAmount(amount, currency === CURRENCY.USDT ? 2 : 0)}

+ {children} + ) diff --git a/src/components/Forms/PaymentForm/PaymentInfo/styles.css b/src/components/Forms/PaymentForm/PaymentInfo/styles.css index 6c18b5699e..bee4eba6e8 100644 --- a/src/components/Forms/PaymentForm/PaymentInfo/styles.css +++ b/src/components/Forms/PaymentForm/PaymentInfo/styles.css @@ -1,5 +1,6 @@ .info { padding: var(--spacing-x-tight) var(--spacing-base); + margin-bottom: var(--spacing-base); text-align: center; } @@ -15,7 +16,7 @@ } .amount { - margin-top: var(--spacing-base); + margin-top: var(--spacing-loose); font-size: var(--font-size-xl); line-height: 1; } diff --git a/src/components/Forms/PaymentForm/Processing/index.tsx b/src/components/Forms/PaymentForm/Processing/index.tsx index 25de93a1ca..be183733b3 100644 --- a/src/components/Forms/PaymentForm/Processing/index.tsx +++ b/src/components/Forms/PaymentForm/Processing/index.tsx @@ -267,9 +267,9 @@ const USDTProcessingForm: React.FC = ({

diff --git a/src/components/Forms/PaymentForm/StripeCheckout/index.tsx b/src/components/Forms/PaymentForm/StripeCheckout/index.tsx index 7a1310c57f..26da7c5491 100644 --- a/src/components/Forms/PaymentForm/StripeCheckout/index.tsx +++ b/src/components/Forms/PaymentForm/StripeCheckout/index.tsx @@ -59,7 +59,7 @@ const StripeCheckout: React.FC = ({ error, onChange }) => { } /> - +
diff --git a/src/components/Forms/PaymentForm/StripeCheckout/styles.css b/src/components/Forms/PaymentForm/StripeCheckout/styles.css index afdb08e948..c72e76790e 100644 --- a/src/components/Forms/PaymentForm/StripeCheckout/styles.css +++ b/src/components/Forms/PaymentForm/StripeCheckout/styles.css @@ -1,3 +1,3 @@ .checkout-input { - min-height: 3rem; + min-height: var(--input-height); } diff --git a/src/components/Forms/PaymentForm/StripeCheckout/styles.global.css b/src/components/Forms/PaymentForm/StripeCheckout/styles.global.css index a8cb8b8c2b..28467e933d 100644 --- a/src/components/Forms/PaymentForm/StripeCheckout/styles.global.css +++ b/src/components/Forms/PaymentForm/StripeCheckout/styles.global.css @@ -2,21 +2,32 @@ .StripeElement { @mixin all-transition; - @mixin border-top-grey; - @mixin border-bottom-grey; + height: var(--input-height); padding: 0 0 0 var(--spacing-base); - background-color: var(--color-white); + border: 1px solid var(--color-grey-light); + border-radius: var(--spacing-x-tight); + caret-color: var(--color-matters-green); - @media (--sm-up) { - background-color: var(--color-grey-lighter); - border-top: none; - border-bottom: 1px solid var(--color-grey-light); - border-radius: var(--spacing-xxx-tight); + &.StripeElement--focus { + background-color: var(--color-green-lighter); + border-color: var(--color-matters-green); + } + + &.StripeElement--invalid { + background-color: transparent; + border-color: var(--color-red); + } + + /* Chrome, Safari, Edge, Opera */ + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + margin: 0; + -webkit-appearance: none; + } - &.StripeElement--focus { - background-color: var(--color-green-lighter); - border-bottom-color: var(--color-matters-green); - } + /* Firefox */ + &[type='number'] { + -moz-appearance: textfield; } } diff --git a/src/components/Hook/useERC20.ts b/src/components/Hook/useERC20.ts index 419f657735..1e743a5868 100644 --- a/src/components/Hook/useERC20.ts +++ b/src/components/Hook/useERC20.ts @@ -1,4 +1,5 @@ import { ethers } from 'ethers' +import { useContext } from 'react' import { erc20ABI, useAccount, @@ -8,6 +9,8 @@ import { usePrepareContractWrite, } from 'wagmi' +import { ViewerContext } from '~/components' + import { supportedChains } from '~/common/utils' export const useAllowanceUSDT = () => { @@ -29,13 +32,30 @@ export const useBalanceUSDT = ({ }: { address?: string | null }) => { - const { address } = useAccount() + const viewer = useContext(ViewerContext) + const viewerEthAddress = viewer.info.ethAddress return useBalance({ - addressOrName: (addr || address) as `0x${string}`, + addressOrName: (addr || viewerEthAddress) as `0x${string}`, token: (process.env.NEXT_PUBLIC_USDT_CONTRACT_ADDRESS || '') as `0x${string}`, chainId: supportedChains[0].id, + cacheTime: 5_000, + }) +} + +export const useBalanceEther = ({ + address: addr, +}: { + address?: string | null +}) => { + const viewer = useContext(ViewerContext) + const viewerEthAddress = viewer.info.ethAddress + + return useBalance({ + addressOrName: (addr || viewerEthAddress) as `0x${string}`, + chainId: supportedChains[0].id, + cacheTime: 5_000, }) } diff --git a/src/components/Notice/index.tsx b/src/components/Notice/index.tsx index b3d4bf904a..54c302069e 100644 --- a/src/components/Notice/index.tsx +++ b/src/components/Notice/index.tsx @@ -70,7 +70,9 @@ const fragments = { `, } -const BaseNotice: React.FC = ({ notice }) => { +export const Notice: React.FC & { + fragments: typeof fragments +} = ({ notice }) => { switch (notice.__typename) { case 'UserNotice': return @@ -99,13 +101,4 @@ const BaseNotice: React.FC = ({ notice }) => { } } -/** - * Memoizing - */ -type MemoizedNotice = React.MemoExoticComponent> & { - fragments: typeof fragments -} - -export const Notice = React.memo(BaseNotice, () => true) as MemoizedNotice - Notice.fragments = fragments diff --git a/src/components/UserDigest/Mini/index.tsx b/src/components/UserDigest/Mini/index.tsx index 0c7a6f7932..a98cb3b515 100644 --- a/src/components/UserDigest/Mini/index.tsx +++ b/src/components/UserDigest/Mini/index.tsx @@ -27,9 +27,10 @@ export type UserDigestMiniProps = { avatarSize?: Extract textSize?: 'xs' | 'sm-s' | 'sm' | 'md-s' | 'md' - textWeight?: 'md' + textWeight?: 'md' | 'semibold' nameColor?: 'black' | 'white' | 'grey-darker' | 'green' direction?: 'row' | 'column' + spacing?: 'xxtight' | 'xtight' hasAvatar?: boolean hasDisplayName?: boolean @@ -61,6 +62,7 @@ const Mini = ({ textWeight, nameColor = 'black', direction = 'row', + spacing = 'xtight', hasAvatar, hasDisplayName, @@ -79,6 +81,7 @@ const Mini = ({ [`text-size-${textSize}`]: !!textSize, [`text-weight-${textWeight}`]: !!textWeight, [`name-color-${nameColor}`]: !!nameColor, + [`spacing-${spacing}`]: !!spacing, hasAvatar, disabled: disabled || isArchived, }) diff --git a/src/components/UserDigest/Mini/styles.css b/src/components/UserDigest/Mini/styles.css index 33f17eb8f5..65695b3922 100644 --- a/src/components/UserDigest/Mini/styles.css +++ b/src/components/UserDigest/Mini/styles.css @@ -1,10 +1,6 @@ .container { @mixin flex-center-start; - &.hasAvatar .name { - margin-left: var(--spacing-x-tight); - } - &:not(.disabled) { &:hover, &:focus { @@ -40,10 +36,14 @@ } .displayname { + @mixin line-clamp; + line-height: 1.25; } .username { + @mixin line-clamp; + margin-left: var(--spacing-xx-tight); font-size: var(--font-size-sm); line-height: 1.25; @@ -73,6 +73,9 @@ .text-weight-md { font-weight: var(--font-weight-medium); } +.text-weight-semibold { + font-weight: var(--font-weight-semibold); +} .name-color-black { & .displayname { @@ -97,3 +100,15 @@ color: var(--color-matters-green); } } + +.spacing-xtight { + &.hasAvatar .name { + margin-left: var(--spacing-x-tight); + } +} + +.spacing-xxtight { + &.hasAvatar .name { + margin-left: var(--spacing-xx-tight); + } +} diff --git a/src/views/Me/Settings/ConnectWallet/index.tsx b/src/views/Me/Settings/ConnectWallet/index.tsx index 1e2d673f6b..db98d2c400 100644 --- a/src/views/Me/Settings/ConnectWallet/index.tsx +++ b/src/views/Me/Settings/ConnectWallet/index.tsx @@ -1,13 +1,49 @@ -import { Head, Layout, useStep, WalletAuthForm } from '~/components' +import { useContext } from 'react' + +import { + EmptyWarning, + Head, + Layout, + Translate, + useStep, + ViewerContext, + WalletAuthForm, +} from '~/components' import { PATHS } from '~/common/enums' type Step = 'wallet-select' | 'wallet-connect' const ConnectWallet = () => { + const viewer = useContext(ViewerContext) + const viewerEthAddress = viewer.info.ethAddress + const initStep = 'wallet-select' const { currStep, forward } = useStep(initStep) + if (viewerEthAddress) { + return ( + + + + } + right={} + /> + + + } + /> + + ) + } + return ( diff --git a/src/views/Me/Wallet/Balance/USDT.tsx b/src/views/Me/Wallet/Balance/USDT.tsx index 8e77a82682..f30560ba1b 100644 --- a/src/views/Me/Wallet/Balance/USDT.tsx +++ b/src/views/Me/Wallet/Balance/USDT.tsx @@ -16,7 +16,7 @@ import styles from './styles.css' export const USDTBalance = () => { const viewer = useContext(ViewerContext) const address = viewer.info.ethAddress - const { data: balanceUSDTData } = useBalanceUSDT({ address }) + const { data: balanceUSDTData } = useBalanceUSDT({}) const balanceUSDT = parseFloat(balanceUSDTData?.formatted || '0') if (!address) { diff --git a/src/views/User/Tags/UserTags.tsx b/src/views/User/Tags/UserTags.tsx index 1340d90e4c..62c41fc0d8 100644 --- a/src/views/User/Tags/UserTags.tsx +++ b/src/views/User/Tags/UserTags.tsx @@ -50,8 +50,8 @@ const UserTags = () => { // pagination const user = data?.user - const connectionPath = 'user.tags' - const { edges, pageInfo } = user?.tags || {} + const connectionPath = 'user.maintainedTags' + const { edges, pageInfo } = user?.maintainedTags || {} const hasSubscriptions = (user?.subscribedCircles.totalCount || 0) > 0 // load next page diff --git a/src/views/User/Tags/gql.ts b/src/views/User/Tags/gql.ts index 5cd53254bd..c1f4187ca7 100644 --- a/src/views/User/Tags/gql.ts +++ b/src/views/User/Tags/gql.ts @@ -19,7 +19,7 @@ export const USER_TAGS_PUBLIC = gql` subscribedCircles(input: { first: 0 }) { totalCount } - tags(input: { first: 20, after: $after }) { + maintainedTags(input: { first: 20, after: $after }) { pageInfo { startCursor endCursor