From 71371fa3748847473a46e5413ae4a27996841b5d Mon Sep 17 00:00:00 2001 From: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> Date: Wed, 4 Sep 2024 09:36:26 +0100 Subject: [PATCH 1/6] refactor(wallet-mobile): claim module --- .../.storybook/storybook.requires.js | 1 - .../AmountItem/AmountItem.stories.tsx | 79 -------- .../src/components/AmountItem/AmountItem.tsx | 183 ------------------ .../features/Claim/module/ClaimProvider.tsx | 6 +- .../features/Claim/module/api-faucet.mocks.ts | 12 +- .../src/features/Claim/module/api.mocks.ts | 78 +++++--- .../src/features/Claim/module/api.tests.ts | 15 +- .../src/features/Claim/module/api.ts | 45 +++-- .../src/features/Claim/module/state.mocks.ts | 6 +- .../src/features/Claim/module/state.ts | 23 +-- .../src/features/Claim/module/transformers.ts | 45 +++-- .../src/features/Claim/module/types.ts | 12 +- .../features/Claim/module/useClaimTokens.tsx | 11 +- .../Claim/useCases/ShowSuccessScreen.tsx | 40 ++-- .../Scan/common/useTriggerScanAction.tsx | 6 +- .../ManageCollateralScreen.stories.tsx | 12 +- .../ManageCollateralScreen.tsx | 15 +- .../SelectBuyTokenFromListScreen.tsx | 6 +- .../ListOrders/OpenOrders.tsx | 14 +- .../Transactions/TxHistoryNavigator.tsx | 5 +- .../yoroi-wallets/cardano/cardano-wallet.ts | 18 +- .../src/yoroi-wallets/cardano/types.ts | 2 +- .../src/yoroi-wallets/mocks/wallet.ts | 2 +- .../Transactions/TxHistoryNavigator.json | 176 ++++++++--------- 24 files changed, 296 insertions(+), 516 deletions(-) delete mode 100644 apps/wallet-mobile/src/components/AmountItem/AmountItem.stories.tsx delete mode 100644 apps/wallet-mobile/src/components/AmountItem/AmountItem.tsx diff --git a/apps/wallet-mobile/.storybook/storybook.requires.js b/apps/wallet-mobile/.storybook/storybook.requires.js index 74da8150ad..2a61fc7f04 100644 --- a/apps/wallet-mobile/.storybook/storybook.requires.js +++ b/apps/wallet-mobile/.storybook/storybook.requires.js @@ -56,7 +56,6 @@ const getStories = () => { return { "./.storybook/stories/Button/ExampleButton.stories.js": require("./stories/Button/ExampleButton.stories.js"), "./src/components/Accordion/Accordion.stories.tsx": require("../src/components/Accordion/Accordion.stories.tsx"), - "./src/components/AmountItem/AmountItem.stories.tsx": require("../src/components/AmountItem/AmountItem.stories.tsx"), "./src/components/Analytics/Analytics.stories.tsx": require("../src/components/Analytics/Analytics.stories.tsx"), "./src/components/BlueCheckbox/BlueCheckbox.stories.tsx": require("../src/components/BlueCheckbox/BlueCheckbox.stories.tsx"), "./src/components/Boundary/Boundary.stories.tsx": require("../src/components/Boundary/Boundary.stories.tsx"), diff --git a/apps/wallet-mobile/src/components/AmountItem/AmountItem.stories.tsx b/apps/wallet-mobile/src/components/AmountItem/AmountItem.stories.tsx deleted file mode 100644 index d91af12ac6..0000000000 --- a/apps/wallet-mobile/src/components/AmountItem/AmountItem.stories.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import {storiesOf} from '@storybook/react-native' -import React from 'react' -import {Text, View} from 'react-native' - -import {QueryProvider} from '../../../.storybook/decorators' -import {mocks} from '../../yoroi-wallets/mocks' -import {WalletManagerProviderMock} from '../../yoroi-wallets/mocks/WalletManagerProviderMock' -import {Amounts} from '../../yoroi-wallets/utils' -import {Spacer} from '..' -import {AmountItem} from './AmountItem' - -const primaryAmount = Amounts.getAmount(mocks.balances, mocks.wallet.portfolioPrimaryTokenInfo.id) -const secondaryAmount = Amounts.getAmount( - mocks.balances, - '698a6ea0ca99f315034072af31eaac6ec11fe8558d3f48e9775aab9d.7444524950', -) - -storiesOf('AmountItem', module) - .add('Gallery', () => ( - <QueryProvider> - <WalletManagerProviderMock wallet={mocks.wallet}> - <View style={{flex: 1, justifyContent: 'center', padding: 16}}> - <Text>Fungible primary token</Text> - - <AmountItem - wallet={mocks.wallet} - amount={primaryAmount} - style={{backgroundColor: 'white', padding: 16, borderRadius: 8}} - /> - - <Spacer height={40} /> - - <Text>Fungible non-primary token</Text> - - <AmountItem - wallet={mocks.wallet} - amount={secondaryAmount} - style={{backgroundColor: 'white', padding: 16, borderRadius: 8}} - /> - </View> - </WalletManagerProviderMock> - </QueryProvider> - )) - .add('Loading', () => ( - <QueryProvider> - <WalletManagerProviderMock - wallet={{ - ...mocks.wallet, - fetchTokenInfo: mocks.fetchTokenInfo.loading, - }} - > - <View style={{flex: 1, justifyContent: 'center', padding: 16}}> - <AmountItem - wallet={mocks.wallet} - amount={primaryAmount} - style={{backgroundColor: 'white', padding: 16, borderRadius: 8}} - /> - </View> - </WalletManagerProviderMock> - </QueryProvider> - )) - .add('Error', () => ( - <QueryProvider> - <WalletManagerProviderMock - wallet={{ - ...mocks.wallet, - fetchTokenInfo: mocks.fetchTokenInfo.error, - }} - > - <View style={{flex: 1, justifyContent: 'center', padding: 16}}> - <AmountItem - wallet={mocks.wallet} - amount={primaryAmount} - style={{backgroundColor: 'white', padding: 16, borderRadius: 8}} - /> - </View> - </WalletManagerProviderMock> - </QueryProvider> - )) diff --git a/apps/wallet-mobile/src/components/AmountItem/AmountItem.tsx b/apps/wallet-mobile/src/components/AmountItem/AmountItem.tsx deleted file mode 100644 index a1b9c7391a..0000000000 --- a/apps/wallet-mobile/src/components/AmountItem/AmountItem.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import {isPrimaryToken} from '@yoroi/portfolio' -import {useTheme} from '@yoroi/theme' -import {Balance, Swap} from '@yoroi/types' -import * as React from 'react' -import {StyleSheet, View, ViewProps} from 'react-native' - -import {usePriceImpactRiskTheme} from '../../features/Swap/common/helpers' -import {SwapPriceImpactRisk} from '../../features/Swap/common/types' -import {isEmptyString} from '../../kernel/utils' -import {YoroiWallet} from '../../yoroi-wallets/cardano/types' -import {useTokenInfo} from '../../yoroi-wallets/hooks' -import {Quantities} from '../../yoroi-wallets/utils' -import {Boundary, Icon, Spacer, Text, TokenIcon, TokenIconPlaceholder} from '..' - -export type AmountItemProps = { - wallet: YoroiWallet - amount: Balance.Amount - style?: ViewProps['style'] - isPrivacyActive?: boolean - inWallet?: boolean - variant?: 'swap' - priceImpactRisk?: SwapPriceImpactRisk - orderType?: Swap.OrderType -} - -export const AmountItem = ({ - isPrivacyActive, - wallet, - style, - amount, - inWallet, - variant, - priceImpactRisk, - orderType, -}: AmountItemProps) => { - const {styles, colors} = useStyles() - const {quantity, tokenId} = amount - const tokenInfo = useTokenInfo({wallet, tokenId}) - - const isPrimary = isPrimaryToken(tokenInfo.id) - const name = tokenInfo.ticker ?? tokenInfo.name - const nameLabel = isEmptyString(name) ? '-' : name - const detail = isPrimary ? tokenInfo.description : tokenInfo.fingerprint - - const formattedQuantity = Quantities.format(quantity, tokenInfo.decimals ?? 0) - const showSwapDetails = !isPrimary && variant === 'swap' - const priceImpactRiskTheme = usePriceImpactRiskTheme(priceImpactRisk ?? 'none') - const priceImpactRiskTextColor = orderType === 'market' ? priceImpactRiskTheme.text : colors.text - - return ( - <View style={[style, styles.container]} testID="assetItem"> - <Left> - <Boundary loading={{fallback: <TokenIconPlaceholder />}} error={{fallback: () => <TokenIconPlaceholder />}}> - <TokenIcon wallet={wallet} tokenId={tokenInfo.id} variant={variant} /> - </Boundary> - </Left> - - <Middle> - <View style={styles.row}> - <Text numberOfLines={1} ellipsizeMode="middle" style={styles.name} testID="tokenInfoText"> - {nameLabel} - </Text> - - {showSwapDetails && ( - <> - <Spacer width={4} /> - - {inWallet && <Icon.Portfolio size={22} color={colors.icon} />} - </> - )} - </View> - - <Text numberOfLines={1} ellipsizeMode="middle" style={styles.detail} testID="tokenFingerprintText"> - {detail} - </Text> - </Middle> - - <Right> - {tokenInfo.kind !== 'nft' && variant !== 'swap' && ( - <View style={styles.row} testID="tokenAmountText"> - {priceImpactRisk === 'moderate' && <Icon.Info size={24} color={priceImpactRiskTextColor} />} - - {priceImpactRisk === 'high' && <Icon.Warning size={24} color={priceImpactRiskTextColor} />} - - <Text style={[styles.quantity, {color: priceImpactRiskTextColor}]}> - {isPrivacyActive ? '**.*******' : formattedQuantity} - </Text> - </View> - )} - </Right> - </View> - ) -} - -const Left = ({style, ...props}: ViewProps) => <View style={style} {...props} /> -const Middle = ({style, ...props}: ViewProps) => { - const {styles} = useStyles() - - return <View style={[style, styles.middle]} {...props} /> -} -const Right = ({style, ...props}: ViewProps) => { - const {styles} = useStyles() - - return <View style={[style, styles.right]} {...props} /> -} - -export const AmountItemPlaceholder = ({style}: ViewProps) => { - const {styles} = useStyles() - return ( - <View style={[style, styles.placeholder]}> - <View style={styles.placeholderElement1} /> - - <View style={styles.placeholderElement2} /> - </View> - ) -} - -const useStyles = () => { - const {color, atoms} = useTheme() - const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - placeholder: { - display: 'flex', - flexDirection: 'row', - gap: 12, - height: 56, - }, - placeholderElement1: { - backgroundColor: color.gray_200, - borderRadius: 8, - flexGrow: 3, - }, - placeholderElement2: { - backgroundColor: color.gray_200, - borderRadius: 8, - flexGrow: 1, - }, - middle: { - flex: 1, - justifyContent: 'center', - paddingHorizontal: 8, - }, - right: { - flexDirection: 'column', - alignItems: 'flex-end', - }, - name: { - color: color.gray_900, - fontSize: 16, - lineHeight: 22, - fontWeight: '500', - fontFamily: 'Rubik-Medium', - }, - detail: { - color: color.gray_600, - fontSize: 12, - lineHeight: 18, - maxWidth: 140, - }, - quantity: { - color: color.gray_900, - ...atoms.body_1_lg_regular, - textAlign: 'right', - flexGrow: 1, - }, - row: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }, - }) - - const colors = { - text: color.gray_900, - icon: color.secondary_600, - } - - return {styles, colors} -} diff --git a/apps/wallet-mobile/src/features/Claim/module/ClaimProvider.tsx b/apps/wallet-mobile/src/features/Claim/module/ClaimProvider.tsx index 7b32481321..419c698b5e 100644 --- a/apps/wallet-mobile/src/features/Claim/module/ClaimProvider.tsx +++ b/apps/wallet-mobile/src/features/Claim/module/ClaimProvider.tsx @@ -4,7 +4,7 @@ import * as React from 'react' import {ScanActionClaim} from '../../Scan/common/types' import {claimApiMockInstances} from './api.mocks' import {ClaimActions, ClaimActionType, claimReducer, defaultClaimActions, defaultClaimState} from './state' -import {ClaimApi, ClaimState, ClaimToken} from './types' +import {ClaimApi, ClaimInfo, ClaimState} from './types' export type ClaimProviderContext = React.PropsWithChildren<ClaimApi & ClaimState & ClaimActions> @@ -26,8 +26,8 @@ export const ClaimProvider = ({children, claimApi, initialState}: ClaimProviderP }) const actions = React.useRef<ClaimActions>({ - claimTokenChanged: (claimToken: ClaimToken) => { - dispatch({type: ClaimActionType.ClaimTokenChanged, claimToken}) + claimInfoChanged: (claimInfo: ClaimInfo) => { + dispatch({type: ClaimActionType.ClaimInfoChanged, claimInfo}) }, scanActionClaimChanged: (scanActionClaim: ScanActionClaim) => { dispatch({type: ClaimActionType.ScanActionClaimChanged, scanActionClaim}) diff --git a/apps/wallet-mobile/src/features/Claim/module/api-faucet.mocks.ts b/apps/wallet-mobile/src/features/Claim/module/api-faucet.mocks.ts index 64dd6a1758..9e07fa022a 100644 --- a/apps/wallet-mobile/src/features/Claim/module/api-faucet.mocks.ts +++ b/apps/wallet-mobile/src/features/Claim/module/api-faucet.mocks.ts @@ -1,3 +1,5 @@ +import {tokenMocks} from '@yoroi/portfolio' + import {ClaimApiClaimTokensResponse} from './types' const claimTokens: Record<string, {[key: string]: ClaimApiClaimTokensResponse}> = { @@ -6,14 +8,16 @@ const claimTokens: Record<string, {[key: string]: ClaimApiClaimTokensResponse}> status: 'accepted', lovelaces: '2000000', tokens: { - '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65f.74484f444c52': '1000000', + [tokenMocks.nftCryptoKitty.info.id]: '44', + [tokenMocks.rnftWhatever.info.id]: '410', }, queue_position: 100, }, queued: { lovelaces: '2000000', tokens: { - '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65f.74484f444c52': '1000000', + [tokenMocks.nftCryptoKitty.info.id]: '44', + [tokenMocks.rnftWhatever.info.id]: '410', }, status: 'queued', queue_position: 1, @@ -21,8 +25,10 @@ const claimTokens: Record<string, {[key: string]: ClaimApiClaimTokensResponse}> claimed: { lovelaces: '2000000', tokens: { - '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65f.74484f444c52': '1000000', + [tokenMocks.nftCryptoKitty.info.id]: '44', + [tokenMocks.rnftWhatever.info.id]: '410', }, + status: 'claimed', tx_hash: 'tx_hash', }, diff --git a/apps/wallet-mobile/src/features/Claim/module/api.mocks.ts b/apps/wallet-mobile/src/features/Claim/module/api.mocks.ts index 0e700e36e4..17a31c4c6f 100644 --- a/apps/wallet-mobile/src/features/Claim/module/api.mocks.ts +++ b/apps/wallet-mobile/src/features/Claim/module/api.mocks.ts @@ -1,40 +1,58 @@ -import {ClaimApi, ClaimToken} from './types' +import {tokenMocks} from '@yoroi/portfolio' -const claimTokensResponse: {[key: string]: ClaimToken} = { +import {ClaimApi, ClaimInfo} from './types' + +const claimTokensResponse: {[key: string]: ClaimInfo} = { accepted: { status: 'accepted', - amounts: { - '.': '2000000', - '698a6ea0ca99f315034072af31eaac6ec11fe8558d3f48e9775aab9d.7444524950': '44', - '29d222ce763455e3d7a09a665ce554f00ac89d2e99a1a83d267170c6.4d494e': '410', - '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65f.74484f444c52': '5', - '1ca1fc0c880d25850cb00303788dfb51bdf2f902f6dce47d1ad09d5b.44': '2463889379', - '08d91ec4e6c743a92de97d2fde5ca0d81493555c535894a3097061f7.c8b0': '148', - '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65e.74484f444c53': '100008', - '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65e.74484f444c54': '10000000012', - '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65e.74484f444c55': '100000000000000020', - }, + amounts: [ + { + info: tokenMocks.primaryETH.info, + quantity: 2_000_000n, + }, + { + info: tokenMocks.nftCryptoKitty.info, + quantity: 44n, + }, + { + info: tokenMocks.rnftWhatever.info, + quantity: 410n, + }, + ], }, processing: { status: 'processing', - amounts: { - '.': '2000000', - '698a6ea0ca99f315034072af31eaac6ec11fe8558d3f48e9775aab9d.7444524950': '44', - }, + amounts: [ + { + info: tokenMocks.primaryETH.info, + quantity: 2_000_000n, + }, + { + info: tokenMocks.nftCryptoKitty.info, + quantity: 44n, + }, + { + info: tokenMocks.rnftWhatever.info, + quantity: 410n, + }, + ], }, done: { status: 'done', - amounts: { - '.': '2000000', - '698a6ea0ca99f315034072af31eaac6ec11fe8558d3f48e9775aab9d.7444524950': '44', - '29d222ce763455e3d7a09a665ce554f00ac89d2e99a1a83d267170c6.4d494e': '410', - '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65f.74484f444c52': '5', - '1ca1fc0c880d25850cb00303788dfb51bdf2f902f6dce47d1ad09d5b.44': '2463889379', - '08d91ec4e6c743a92de97d2fde5ca0d81493555c535894a3097061f7.c8b0': '148', - '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65e.74484f444c53': '100008', - '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65e.74484f444c54': '10000000012', - '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65e.74484f444c55': '100000000000000020', - }, + amounts: [ + { + info: tokenMocks.primaryETH.info, + quantity: 2_000_000n, + }, + { + info: tokenMocks.nftCryptoKitty.info, + quantity: 44n, + }, + { + info: tokenMocks.rnftWhatever.info, + quantity: 410n, + }, + ], txHash: '3a27ac29f4218a4503ed241a19e59291835b38ccdb1f1f71ae4dc889d7dbfeb4', }, } as const @@ -59,7 +77,7 @@ const claimTokensApi = { return Promise.reject(new Error('Something went wrong')) }, loading: () => { - return new Promise(() => null) as unknown as ClaimToken + return new Promise(() => null) as unknown as ClaimInfo }, } as const @@ -70,7 +88,7 @@ export const claimApiMockFetchers = { const claimApiError: ClaimApi = { claimTokens: claimTokensApi.error, address: 'address', - primaryTokenId: '.', + primaryTokenInfo: tokenMocks.primaryETH.info, } as const export const claimApiMockInstances = { diff --git a/apps/wallet-mobile/src/features/Claim/module/api.tests.ts b/apps/wallet-mobile/src/features/Claim/module/api.tests.ts index d4d13d04ed..42d856ed30 100644 --- a/apps/wallet-mobile/src/features/Claim/module/api.tests.ts +++ b/apps/wallet-mobile/src/features/Claim/module/api.tests.ts @@ -1,16 +1,21 @@ import {fetchData} from '@yoroi/common' +import {tokenInfoMocks} from '@yoroi/portfolio' +import {Portfolio} from '@yoroi/types' import {claimApiMaker} from './api' describe('claimApiMaker', () => { + const options = { + address: 'addr_test', + primaryTokenInfo: tokenInfoMocks.primaryETH, + tokenManager: {} as Portfolio.Manager.Token, + } + it('success', () => { - const appApi = claimApiMaker({address: 'addr_test', primaryTokenId: 'primaryTokenId'}) + const appApi = claimApiMaker(options) expect(appApi).toBeDefined() - const appApiWithFetcher = claimApiMaker( - {address: 'addr_test', primaryTokenId: 'primaryTokenId'}, - {request: fetchData}, - ) + const appApiWithFetcher = claimApiMaker(options, {request: fetchData}) expect(appApiWithFetcher).toBeDefined() }) }) diff --git a/apps/wallet-mobile/src/features/Claim/module/api.ts b/apps/wallet-mobile/src/features/Claim/module/api.ts index 81a1deeccd..5eb2900f50 100644 --- a/apps/wallet-mobile/src/features/Claim/module/api.ts +++ b/apps/wallet-mobile/src/features/Claim/module/api.ts @@ -1,48 +1,53 @@ import {FetchData, fetchData, isLeft} from '@yoroi/common' -import {Api, Balance} from '@yoroi/types' +import {Api, Portfolio} from '@yoroi/types' import {ScanActionClaim} from '../../Scan/common/types' import {asClaimApiError, asClaimToken} from './transformers' import {ClaimApi, ClaimApiClaimTokensResponse} from './types' import {ClaimTokensApiResponseSchema} from './validators' -type ClaimApiMaker = Readonly<{ +type ClaimApiMakerOptions = Readonly<{ address: string - primaryTokenId: Balance.TokenInfo['id'] + primaryTokenInfo: Portfolio.Token.Info + tokenManager: Portfolio.Manager.Token }> export const claimApiMaker = ( - {address, primaryTokenId}: ClaimApiMaker, + {address, primaryTokenInfo, tokenManager}: ClaimApiMakerOptions, deps: Readonly<{request: FetchData}> = {request: fetchData} as const, ): Readonly<ClaimApi> => { - const claimTokens = postClaimTokens({address, primaryTokenId}, deps) + const claimTokens = postClaimTokens({address, primaryTokenInfo, tokenManager}, deps) return { claimTokens, address, - primaryTokenId, + primaryTokenInfo, } as const } const postClaimTokens = - ({address, primaryTokenId}: ClaimApiMaker, {request} = {request: fetchData}) => + ({address, primaryTokenInfo, tokenManager}: ClaimApiMakerOptions, {request} = {request: fetchData}) => async (claimAction: ScanActionClaim) => { // builds the request from the action, overides address and code const {code, params, url} = claimAction const payload = {...params, address, code} - const response = await request<ClaimApiClaimTokensResponse>({ - url, - method: 'post', - data: payload, - }) - - if (isLeft(response)) { - return asClaimApiError(response.error) - } else { - const claimToken = response.value.data - if (!ClaimTokensApiResponseSchema.safeParse(claimToken).success) throw new Api.Errors.ResponseMalformed() - - return asClaimToken(claimToken, primaryTokenId) + try { + const response = await request<ClaimApiClaimTokensResponse>({ + url, + method: 'post', + data: payload, + }) + + if (isLeft(response)) { + return asClaimApiError(response.error) + } else { + const claimInfo = response.value.data + if (!ClaimTokensApiResponseSchema.safeParse(claimInfo).success) throw new Api.Errors.ResponseMalformed() + + return asClaimToken(claimInfo, primaryTokenInfo, tokenManager) + } + } catch (error) { + throw new Api.Errors.Unknown((error as Error)?.message) } } diff --git a/apps/wallet-mobile/src/features/Claim/module/state.mocks.ts b/apps/wallet-mobile/src/features/Claim/module/state.mocks.ts index b2e0673fc2..2fc0c69b2d 100644 --- a/apps/wallet-mobile/src/features/Claim/module/state.mocks.ts +++ b/apps/wallet-mobile/src/features/Claim/module/state.mocks.ts @@ -15,17 +15,17 @@ const withScanActionClaim: ClaimState = { const withClaimTokenAccepted: ClaimState = { ...withScanActionClaim, - claimToken: claimApiMockResponses.claimTokens['accepted'], + claimInfo: claimApiMockResponses.claimTokens['accepted'], } as const const withClaimTokenProcessing: ClaimState = { ...withScanActionClaim, - claimToken: claimApiMockResponses.claimTokens['processing'], + claimInfo: claimApiMockResponses.claimTokens['processing'], } as const const withClaimTokenDone: ClaimState = { ...withScanActionClaim, - claimToken: claimApiMockResponses.claimTokens['done'], + claimInfo: claimApiMockResponses.claimTokens['done'], } as const export const mocks = { diff --git a/apps/wallet-mobile/src/features/Claim/module/state.ts b/apps/wallet-mobile/src/features/Claim/module/state.ts index cb7c0defb4..38fabf84e2 100644 --- a/apps/wallet-mobile/src/features/Claim/module/state.ts +++ b/apps/wallet-mobile/src/features/Claim/module/state.ts @@ -1,24 +1,25 @@ import {invalid} from '@yoroi/common' -import {produce} from 'immer' +import {castDraft, produce} from 'immer' import {ScanActionClaim} from '../../Scan/common/types' -import {ClaimState, ClaimToken} from './types' +import {ClaimInfo, ClaimState} from './types' + export type ClaimActions = Readonly<{ - claimTokenChanged: (claimToken: ClaimToken) => void + claimInfoChanged: (claimInfo: ClaimInfo) => void scanActionClaimChanged: (scanActionClaim: ScanActionClaim) => void reset: () => void }> export enum ClaimActionType { - ClaimTokenChanged = 'claimTokenChanged', + ClaimInfoChanged = 'claimInfoChanged', ScanActionClaimChanged = 'scanActionClaimChanged', Reset = 'reset', } export type ClaimAction = | { - type: ClaimActionType.ClaimTokenChanged - claimToken: ClaimToken + type: ClaimActionType.ClaimInfoChanged + claimInfo: ClaimInfo } | { type: ClaimActionType.ScanActionClaimChanged @@ -29,12 +30,12 @@ export type ClaimAction = } export const defaultClaimState: ClaimState = { - claimToken: undefined, + claimInfo: undefined, scanActionClaim: undefined, } as const export const defaultClaimActions: ClaimActions = { - claimTokenChanged: () => invalid('missing init'), + claimInfoChanged: () => invalid('missing init'), scanActionClaimChanged: () => invalid('missing init'), reset: () => invalid('missing init'), } as const @@ -42,14 +43,14 @@ export const defaultClaimActions: ClaimActions = { export const claimReducer = (state: ClaimState, action: ClaimAction): ClaimState => { return produce(state, (draft) => { switch (action.type) { - case ClaimActionType.ClaimTokenChanged: - draft.claimToken = action.claimToken + case ClaimActionType.ClaimInfoChanged: + draft.claimInfo = castDraft(action.claimInfo) break case ClaimActionType.ScanActionClaimChanged: draft.scanActionClaim = action.scanActionClaim break case ClaimActionType.Reset: - draft.claimToken = undefined + draft.claimInfo = undefined draft.scanActionClaim = undefined break } diff --git a/apps/wallet-mobile/src/features/Claim/module/transformers.ts b/apps/wallet-mobile/src/features/Claim/module/transformers.ts index e3da0d87ec..8db9f3eae1 100644 --- a/apps/wallet-mobile/src/features/Claim/module/transformers.ts +++ b/apps/wallet-mobile/src/features/Claim/module/transformers.ts @@ -1,9 +1,9 @@ -import {getApiError} from '@yoroi/common' -import {Api, Balance} from '@yoroi/types' +import {getApiError, toBigInt} from '@yoroi/common' +import {isPrimaryToken, isTokenId} from '@yoroi/portfolio' +import {Api, Portfolio} from '@yoroi/types' -import {Amounts, asQuantity} from '../../../yoroi-wallets/utils/utils' import {claimApiErrors} from './errors' -import {ClaimApiClaimTokensResponse, ClaimToken} from './types' +import {ClaimApiClaimTokensResponse, ClaimInfo} from './types' // if the error is a known claim api error, throw it with a more specific error message otherwise throw the api error export const asClaimApiError = (error: Api.ResponseError) => { @@ -12,33 +12,50 @@ export const asClaimApiError = (error: Api.ResponseError) => { throw getApiError(error) } -export const asClaimToken = ( +export const asClaimToken = async ( claimItemResponse: ClaimApiClaimTokensResponse, - primaryTokenId: Balance.TokenInfo['id'], + primaryTokenInfo: Portfolio.Token.Info, + tokenManager: Portfolio.Manager.Token, ) => { const {lovelaces, tokens, status} = claimItemResponse - const ptQuantity = asQuantity(lovelaces) - const amounts = Amounts.fromArray( - Object.entries(tokens) - .concat([[primaryTokenId, ptQuantity]]) - .map(([tokenId, quantity]): Balance.Amount => ({tokenId, quantity: asQuantity(quantity)})), + const ptQuantity = toBigInt(lovelaces, 0, true) + // NOTE: filter out wrong token ids and pt if wrongly included + const ids = new Set( + Object.keys(tokens) + .filter(isTokenId) + .filter((id) => !isPrimaryToken(id)), ) + const infos = await tokenManager.sync({secondaryTokenIds: Array.from(ids), sourceId: 'claim-module'}) + const amounts: Array<Portfolio.Token.Amount> = [] + for (const [tokenId, cachedInfo] of infos.entries()) { + if (!cachedInfo?.record || !ids.has(tokenId)) continue + amounts.push({ + info: cachedInfo.record, + quantity: toBigInt(tokens[tokenId], 0, true), + }) + } + if (ptQuantity > 0n) { + amounts.push({ + info: primaryTokenInfo, + quantity: ptQuantity, + }) + } if (status === 'claimed') { - const claimed: Readonly<ClaimToken> = { + const claimed: Readonly<ClaimInfo> = { status: 'done', amounts, txHash: claimItemResponse.tx_hash, } return claimed } else if (status === 'queued') { - const queued: Readonly<ClaimToken> = { + const queued: Readonly<ClaimInfo> = { status: 'processing', amounts, } return queued } else { - const accepted: Readonly<ClaimToken> = { + const accepted: Readonly<ClaimInfo> = { status: 'accepted', amounts, } diff --git a/apps/wallet-mobile/src/features/Claim/module/types.ts b/apps/wallet-mobile/src/features/Claim/module/types.ts index 724fa8f4ba..5126775cbf 100644 --- a/apps/wallet-mobile/src/features/Claim/module/types.ts +++ b/apps/wallet-mobile/src/features/Claim/module/types.ts @@ -1,4 +1,4 @@ -import {Balance} from '@yoroi/types' +import {Portfolio} from '@yoroi/types' import {ScanActionClaim} from '../../Scan/common/types' @@ -33,20 +33,20 @@ export type ClaimApiClaimTokensResponse = { export type ClaimStatus = 'accepted' | 'processing' | 'done' -export type ClaimToken = Readonly<{ +export type ClaimInfo = Readonly<{ // api status: ClaimStatus - amounts: Balance.Amounts + amounts: ReadonlyArray<Portfolio.Token.Amount> txHash?: string }> export type ClaimApi = Readonly<{ - claimTokens: (action: ScanActionClaim) => Promise<ClaimToken> + claimTokens: (action: ScanActionClaim) => Promise<ClaimInfo> address: string - primaryTokenId: Balance.TokenInfo['id'] + primaryTokenInfo: Portfolio.Token.Info }> export type ClaimState = Readonly<{ - claimToken: ClaimToken | undefined + claimInfo: ClaimInfo | undefined scanActionClaim: ScanActionClaim | undefined }> diff --git a/apps/wallet-mobile/src/features/Claim/module/useClaimTokens.tsx b/apps/wallet-mobile/src/features/Claim/module/useClaimTokens.tsx index 006267faef..7e008ab9b1 100644 --- a/apps/wallet-mobile/src/features/Claim/module/useClaimTokens.tsx +++ b/apps/wallet-mobile/src/features/Claim/module/useClaimTokens.tsx @@ -3,19 +3,20 @@ import {UseMutationOptions} from 'react-query' import {ScanActionClaim} from '../../Scan/common/types' import {useClaim} from './ClaimProvider' -import {ClaimToken} from './types' +import {ClaimInfo} from './types' -export const useClaimTokens = (options: UseMutationOptions<ClaimToken, Error, ScanActionClaim> = {}) => { +export const useClaimTokens = (options: UseMutationOptions<ClaimInfo, Error, ScanActionClaim> = {}) => { const {claimTokens, address} = useClaim() - const mutation = useMutationWithInvalidations<ClaimToken, Error, ScanActionClaim>({ + + const mutation = useMutationWithInvalidations<ClaimInfo, Error, ScanActionClaim>({ ...options, mutationFn: claimTokens, invalidateQueries: [['useClaimTokens', address]], }) return { - claimTokens: mutation.mutate, - ...mutation, + + claimTokens: mutation.mutate, } as const } diff --git a/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx index 7feeafe0fb..ff7e35a2cc 100644 --- a/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx +++ b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx @@ -1,21 +1,18 @@ -import {invalid} from '@yoroi/common' import {useExplorers} from '@yoroi/explorers' +import {sortTokenAmountsByInfo} from '@yoroi/portfolio' import {useTheme} from '@yoroi/theme' -import {Balance} from '@yoroi/types' +import {App, Portfolio} from '@yoroi/types' import React from 'react' import {FlatList, Linking, Platform, StyleSheet, Text, TextProps, View, ViewProps} from 'react-native' import {SafeAreaView} from 'react-native-safe-area-context' import {CopyButton, Icon} from '../../../components' -import {AmountItem} from '../../../components/AmountItem/AmountItem' import {Button} from '../../../components/Button/Button' import {PressableIcon} from '../../../components/PressableIcon/PressableIcon' import {Space} from '../../../components/Space/Space' import {Spacer} from '../../../components/Spacer/Spacer' import {isEmptyString} from '../../../kernel/utils' -import {useTokenInfos} from '../../../yoroi-wallets/hooks' -import {sortTokenInfos} from '../../../yoroi-wallets/utils/sorting' -import {Amounts} from '../../../yoroi-wallets/utils/utils' +import {TokenAmountItem} from '../../Portfolio/common/TokenAmountItem/TokenAmountItem' import {useSelectedWallet} from '../../WalletManager/common/hooks/useSelectedWallet' import {useDialogs} from '../common/useDialogs' import {useNavigateTo} from '../common/useNavigateTo' @@ -28,11 +25,11 @@ export const ShowSuccessScreen = () => { const {styles} = useStyles() const strings = useStrings() const navigateTo = useNavigateTo() - const {claimToken} = useClaim() + const {claimInfo} = useClaim() - if (!claimToken) invalid('Should never happen') + if (!claimInfo) throw new App.Errors.InvalidState('ClaimInfo is not set, reached an invalid state') - const {status, txHash, amounts} = claimToken + const {status, txHash, amounts} = claimInfo return ( <SafeAreaView edges={['top', 'left', 'right']} style={styles.root}> @@ -125,23 +122,17 @@ const TxHash = ({txHash}: {txHash: string}) => { ) } -export const AmountList = ({amounts}: {amounts: Balance.Amounts}) => { +export const AmountList = ({amounts}: {amounts: ReadonlyArray<Portfolio.Token.Amount>}) => { const {wallet} = useSelectedWallet() - - const tokenInfos = useTokenInfos({ - wallet, - tokenIds: Amounts.toArray(amounts).map(({tokenId}) => tokenId), - }) + const {styles} = useStyles() return ( <FlatList - data={sortTokenInfos({tokenInfos})} - renderItem={({item: tokenInfo}) => ( - <AmountItem wallet={wallet} amount={Amounts.getAmount(amounts, tokenInfo.id)} /> - )} - ItemSeparatorComponent={() => <Spacer height={16} />} - style={{paddingHorizontal: 16}} - keyExtractor={({id}) => id} + data={sortTokenAmountsByInfo({amounts, primaryTokenInfo: wallet.portfolioPrimaryTokenInfo})} + renderItem={({item: amount}) => <TokenAmountItem amount={amount} />} + ItemSeparatorComponent={() => <Space height="lg" />} + style={styles.list} + keyExtractor={({info}) => info.id} /> ) } @@ -152,6 +143,9 @@ const useStyles = () => { flex: { ...atoms.flex_1, }, + list: { + ...atoms.px_lg, + }, root: { ...atoms.flex_1, color: color.bg_color_max, @@ -197,5 +191,5 @@ const useStyles = () => { icon: color.el_gray_medium, } - return {styles, colors} + return {styles, colors} as const } diff --git a/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx b/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx index a0ce33e7b1..362a7679dc 100644 --- a/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx +++ b/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx @@ -29,11 +29,11 @@ export const useTriggerScanAction = ({insideFeature}: {insideFeature: ScanFeatur memoChanged, } = useTransfer() - const {reset: resetClaimState, scanActionClaimChanged, address, claimTokenChanged} = useClaim() + const {reset: resetClaimState, scanActionClaimChanged, address, claimInfoChanged} = useClaim() const claimErrorResolver = useClaimErrorResolver() const {claimTokens} = useClaimTokens({ - onSuccess: (claimToken) => { - claimTokenChanged(claimToken) + onSuccess: (claimInfo) => { + claimInfoChanged(claimInfo) closeModal() navigateTo.claimShowSuccess() }, diff --git a/apps/wallet-mobile/src/features/Settings/ManageCollateral/ManageCollateralScreen.stories.tsx b/apps/wallet-mobile/src/features/Settings/ManageCollateral/ManageCollateralScreen.stories.tsx index 6e63f07388..9125e3d7b3 100644 --- a/apps/wallet-mobile/src/features/Settings/ManageCollateral/ManageCollateralScreen.stories.tsx +++ b/apps/wallet-mobile/src/features/Settings/ManageCollateral/ManageCollateralScreen.stories.tsx @@ -20,8 +20,8 @@ const goneCollateral: YoroiWallet = { getCollateralInfo: () => { return { amount: { - quantity: '0', - tokenId: mocks.wallet.portfolioPrimaryTokenInfo.id, + quantity: 0n, + info: mocks.wallet.portfolioPrimaryTokenInfo, }, collateralId: mocks.wallet.collateralId, utxo: undefined, @@ -36,8 +36,8 @@ const noCollateral: YoroiWallet = { getCollateralInfo: () => { return { amount: { - quantity: '0', - tokenId: mocks.wallet.portfolioPrimaryTokenInfo.id, + quantity: 0n, + info: mocks.wallet.portfolioPrimaryTokenInfo, }, collateralId: '', utxo: undefined, @@ -53,8 +53,8 @@ const noFundsWallet: YoroiWallet = { getCollateralInfo: () => { return { amount: { - quantity: '0', - tokenId: mocks.wallet.portfolioPrimaryTokenInfo.id, + quantity: 0n, + info: mocks.wallet.portfolioPrimaryTokenInfo, }, collateralId: '', utxo: undefined, diff --git a/apps/wallet-mobile/src/features/Settings/ManageCollateral/ManageCollateralScreen.tsx b/apps/wallet-mobile/src/features/Settings/ManageCollateral/ManageCollateralScreen.tsx index 586f79109b..175f4ff312 100644 --- a/apps/wallet-mobile/src/features/Settings/ManageCollateral/ManageCollateralScreen.tsx +++ b/apps/wallet-mobile/src/features/Settings/ManageCollateral/ManageCollateralScreen.tsx @@ -1,7 +1,7 @@ import {toBigInt} from '@yoroi/common' import {useTheme} from '@yoroi/theme' import {useTransfer} from '@yoroi/transfer' -import {Balance, Portfolio} from '@yoroi/types' +import {Portfolio} from '@yoroi/types' import BigNumber from 'bignumber.js' import * as React from 'react' import { @@ -18,19 +18,17 @@ import {SafeAreaView} from 'react-native-safe-area-context' import {useMutation} from 'react-query' import {Button, CopyButton, Icon, Spacer, Text} from '../../../components' -import {AmountItem} from '../../../components/AmountItem/AmountItem' import {ErrorPanel} from '../../../components/ErrorPanel/ErrorPanel' import {Space} from '../../../components/Space/Space' import {SettingsStackRoutes, useUnsafeParams} from '../../../kernel/navigation' -import {YoroiWallet} from '../../../yoroi-wallets/cardano/types' import {useCollateralInfo} from '../../../yoroi-wallets/cardano/utxoManager/useCollateralInfo' import {useSetCollateralId} from '../../../yoroi-wallets/cardano/utxoManager/useSetCollateralId' import {collateralConfig, utxosMaker} from '../../../yoroi-wallets/cardano/utxoManager/utxos' import {useBalances} from '../../../yoroi-wallets/hooks' import {RawUtxo, YoroiEntry} from '../../../yoroi-wallets/types' import {Amounts, asQuantity, Quantities} from '../../../yoroi-wallets/utils' +import {TokenAmountItem} from '../../Portfolio/common/TokenAmountItem/TokenAmountItem' import {useSelectedWallet} from '../../WalletManager/common/hooks/useSelectedWallet' -import {usePrivacyMode} from '../PrivacyMode/PrivacyMode' import {createCollateralEntry} from './helpers' import {useNavigateTo} from './navigation' import {useStrings} from './strings' @@ -135,7 +133,6 @@ export const ManageCollateralScreen = () => { <ActionableAmount amount={amount} - wallet={wallet} onRemove={handleRemoveCollateral} collateralId={collateralId} disabled={isLoading} @@ -186,21 +183,19 @@ export const ManageCollateralScreen = () => { type ActionableAmountProps = { collateralId: RawUtxo['utxo_id'] - wallet: YoroiWallet - amount: Balance.Amount + amount: Portfolio.Token.Amount onRemove(): void disabled?: boolean } -const ActionableAmount = ({amount, onRemove, wallet, collateralId, disabled}: ActionableAmountProps) => { +const ActionableAmount = ({amount, onRemove, collateralId, disabled}: ActionableAmountProps) => { const {styles} = useStyles() - const {isPrivacyActive} = usePrivacyMode() const handleRemove = () => onRemove() return ( <View style={styles.amountItem} testID="amountItem"> <Left> - <AmountItem amount={amount} wallet={wallet} isPrivacyActive={isPrivacyActive} /> + <TokenAmountItem amount={amount} /> </Left> {collateralId !== '' && ( diff --git a/apps/wallet-mobile/src/features/Swap/useCases/StartOrderSwapScreen/CreateOrder/EditBuyAmount/SelectBuyTokenFromListScreen/SelectBuyTokenFromListScreen.tsx b/apps/wallet-mobile/src/features/Swap/useCases/StartOrderSwapScreen/CreateOrder/EditBuyAmount/SelectBuyTokenFromListScreen/SelectBuyTokenFromListScreen.tsx index 8bb118ddd6..b2dac937f9 100644 --- a/apps/wallet-mobile/src/features/Swap/useCases/StartOrderSwapScreen/CreateOrder/EditBuyAmount/SelectBuyTokenFromListScreen/SelectBuyTokenFromListScreen.tsx +++ b/apps/wallet-mobile/src/features/Swap/useCases/StartOrderSwapScreen/CreateOrder/EditBuyAmount/SelectBuyTokenFromListScreen/SelectBuyTokenFromListScreen.tsx @@ -9,11 +9,13 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native' import {SafeAreaView} from 'react-native-safe-area-context' import {Boundary, Icon, Spacer, Text} from '../../../../../../../components' -import {AmountItemPlaceholder} from '../../../../../../../components/AmountItem/AmountItem' import {useMetrics} from '../../../../../../../kernel/metrics/metricsManager' import {YoroiWallet} from '../../../../../../../yoroi-wallets/cardano/types' import {usePortfolioBalances} from '../../../../../../Portfolio/common/hooks/usePortfolioBalances' -import {TokenAmountItem} from '../../../../../../Portfolio/common/TokenAmountItem/TokenAmountItem' +import { + AmountItemPlaceholder, + TokenAmountItem, +} from '../../../../../../Portfolio/common/TokenAmountItem/TokenAmountItem' import {useSearch, useSearchOnNavBar} from '../../../../../../Search/SearchContext' import {NoAssetFoundImage} from '../../../../../../Send/common/NoAssetFoundImage' import {useSelectedWallet} from '../../../../../../WalletManager/common/hooks/useSelectedWallet' diff --git a/apps/wallet-mobile/src/features/Swap/useCases/StartOrderSwapScreen/ListOrders/OpenOrders.tsx b/apps/wallet-mobile/src/features/Swap/useCases/StartOrderSwapScreen/ListOrders/OpenOrders.tsx index 1ed71ffcab..9b05d37f02 100644 --- a/apps/wallet-mobile/src/features/Swap/useCases/StartOrderSwapScreen/ListOrders/OpenOrders.tsx +++ b/apps/wallet-mobile/src/features/Swap/useCases/StartOrderSwapScreen/ListOrders/OpenOrders.tsx @@ -33,7 +33,6 @@ import {convertBech32ToHex, getTransactionSigners} from '../../../../../yoroi-wa import {YoroiWallet} from '../../../../../yoroi-wallets/cardano/types' import {createRawTxSigningKey, generateCIP30UtxoCbor} from '../../../../../yoroi-wallets/cardano/utils' import {useTokenInfos, useTransactionInfos} from '../../../../../yoroi-wallets/hooks' -import {Quantities} from '../../../../../yoroi-wallets/utils' import {useSearch} from '../../../../Search/SearchContext' import {getCollateralAmountInLovelace} from '../../../../Settings/ManageCollateral/helpers' import {useSelectedWallet} from '../../../../WalletManager/common/hooks/useSelectedWallet' @@ -161,13 +160,8 @@ export const OpenOrders = () => { const showCollateralNotFoundAlert = useShowCollateralNotFoundAlert(wallet) const hasCollateral = () => { - const info = wallet.getCollateralInfo() - const primaryTokenDecimals = wallet.portfolioPrimaryTokenInfo.decimals - return ( - !!info.utxo && - Quantities.integer(info.amount.quantity, primaryTokenDecimals) >= - Quantities.integer(getCollateralAmountInLovelace(), primaryTokenDecimals) - ) + const collateral = wallet.getCollateralInfo() + return !!collateral.utxo && collateral.amount.quantity >= BigInt(getCollateralAmountInLovelace()) } const onOrderCancelConfirm = (order: MappedOpenOrder) => { @@ -716,8 +710,8 @@ const useShowCollateralNotFoundAlert = (wallet: YoroiWallet) => { const swapNavigateTo = useNavigateTo() return () => { - const info = wallet.getCollateralInfo() - const isCollateralUtxoPending = !info.isConfirmed && info.collateralId.length > 0 + const collateral = wallet.getCollateralInfo() + const isCollateralUtxoPending = !collateral.isConfirmed && collateral.collateralId.length > 0 if (isCollateralUtxoPending) { Alert.alert(strings.collateralTxPendingTitle, strings.collateralTxPending) diff --git a/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx b/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx index 86044bc87c..2079a2f99a 100644 --- a/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx +++ b/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx @@ -82,9 +82,10 @@ export const TxHistoryNavigator = () => { const claimApi = React.useMemo(() => { return claimApiMaker({ address: wallet.externalAddresses[0], - primaryTokenId: wallet.portfolioPrimaryTokenInfo.id, + primaryTokenInfo: wallet.portfolioPrimaryTokenInfo, + tokenManager: wallet.networkManager.tokenManager, }) - }, [wallet.externalAddresses, wallet.portfolioPrimaryTokenInfo.id]) + }, [wallet.externalAddresses, wallet.networkManager.tokenManager, wallet.portfolioPrimaryTokenInfo]) // navigator components const headerRightHistory = React.useCallback(() => <HeaderRightHistory />, []) diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/cardano-wallet.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/cardano-wallet.ts index e9587b842c..e566669d09 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/cardano/cardano-wallet.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/cardano-wallet.ts @@ -9,6 +9,7 @@ import {Api, App, Balance, HW, Network, Portfolio, Wallet} from '@yoroi/types' import assert from 'assert' import {BigNumber} from 'bignumber.js' import {Buffer} from 'buffer' +import {freeze} from 'immer' import _ from 'lodash' import {defaultMemoize} from 'reselect' import {Observable} from 'rxjs' @@ -21,6 +22,7 @@ import LocalizableError from '../../kernel/i18n/LocalizableError' import {throwLoggedError} from '../../kernel/logger/helpers/throw-logged-error' import {logger} from '../../kernel/logger/logger' import {makeWalletEncryptedStorage, WalletEncryptedStorage} from '../../kernel/storage/EncryptedStorage' +import {isEmptyString} from '../../kernel/utils' import type { AccountStateResponse, DefaultAsset, @@ -34,7 +36,7 @@ import type { YoroiEntry, } from '../types' import {StakingInfo, YoroiSignedTx, YoroiUnsignedTx} from '../types' -import {asQuantity, Quantities} from '../utils' +import {Quantities} from '../utils' import {Cardano, CardanoMobile} from '../wallets' import {AccountManager, accountManagerMaker, Addresses} from './account-manager/account-manager' import * as legacyApi from './api/api' @@ -1036,17 +1038,19 @@ export const makeCardanoWallet = ( const utxos = utxosMaker(this._utxos) const collateralId = this.collateralId const collateralUtxo = utxos.findById(collateralId) - const quantity = collateralUtxo?.amount !== undefined ? asQuantity(collateralUtxo.amount) : Quantities.zero - const txInfos = this.transactions + const quantity = + collateralUtxo?.amount !== undefined && !isEmptyString(collateralUtxo.amount) + ? BigInt(collateralUtxo?.amount) + : 0n const collateralTxId = collateralId ? collateralId.split(':')[0] : null - const isConfirmed = !!collateralTxId && Object.values(txInfos).some((tx) => tx.id === collateralTxId) + const isConfirmed = !!collateralTxId && Object.values(this.transactions).some((tx) => tx.id === collateralTxId) - return { + return freeze({ utxo: collateralUtxo, - amount: {quantity, tokenId: this.portfolioPrimaryTokenInfo.id}, + amount: {quantity, info: this.portfolioPrimaryTokenInfo}, collateralId, isConfirmed, - } + }) } async setCollateralId(id: RawUtxo['utxo_id']): Promise<void> { diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts index 3978283f55..f8745fd4cc 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/types.ts @@ -169,7 +169,7 @@ export interface YoroiWallet { get collateralId(): string getCollateralInfo(): { utxo: RawUtxo | undefined - amount: Balance.Amount + amount: Portfolio.Token.Amount collateralId: RawUtxo['utxo_id'] isConfirmed: boolean } diff --git a/apps/wallet-mobile/src/yoroi-wallets/mocks/wallet.ts b/apps/wallet-mobile/src/yoroi-wallets/mocks/wallet.ts index eca15eace6..38e50ee72d 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/mocks/wallet.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/mocks/wallet.ts @@ -130,7 +130,7 @@ const wallet: YoroiWallet = { assets: [], }, - amount: {quantity: '5449549', tokenId: '.'}, + amount: {quantity: 5449549n, info: primaryTokenInfoMainnet}, collateralId: '22d391c7a97559cb4784bd975214919618acce75cde573a7150a176700e76181:2', isConfirmed: true, } diff --git a/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json b/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json index 24de1fcd81..0597aae766 100644 --- a/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json +++ b/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json @@ -4,14 +4,14 @@ "defaultMessage": "!!!Receive", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 417, + "line": 418, "column": 16, - "index": 15168 + "index": 15256 }, "end": { - "line": 420, + "line": 421, "column": 3, - "index": 15257 + "index": 15345 } }, { @@ -19,14 +19,14 @@ "defaultMessage": "!!!Address details", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 421, + "line": 422, "column": 32, - "index": 15291 + "index": 15379 }, "end": { - "line": 424, + "line": 425, "column": 3, - "index": 15404 + "index": 15492 } }, { @@ -34,14 +34,14 @@ "defaultMessage": "!!!Swap", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 425, + "line": 426, "column": 13, - "index": 15419 + "index": 15507 }, "end": { - "line": 428, + "line": 429, "column": 3, - "index": 15492 + "index": 15580 } }, { @@ -49,14 +49,14 @@ "defaultMessage": "!!!Swap from", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 429, + "line": 430, "column": 17, - "index": 15511 + "index": 15599 }, "end": { - "line": 432, + "line": 433, "column": 3, - "index": 15588 + "index": 15676 } }, { @@ -64,14 +64,14 @@ "defaultMessage": "!!!Swap to", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 433, + "line": 434, "column": 15, - "index": 15605 + "index": 15693 }, "end": { - "line": 436, + "line": 437, "column": 3, - "index": 15678 + "index": 15766 } }, { @@ -79,14 +79,14 @@ "defaultMessage": "!!!Slippage Tolerance", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 437, + "line": 438, "column": 21, - "index": 15701 + "index": 15789 }, "end": { - "line": 440, + "line": 441, "column": 3, - "index": 15796 + "index": 15884 } }, { @@ -94,14 +94,14 @@ "defaultMessage": "!!!Select pool", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 441, + "line": 442, "column": 14, - "index": 15812 + "index": 15900 }, "end": { - "line": 444, + "line": 445, "column": 3, - "index": 15893 + "index": 15981 } }, { @@ -109,14 +109,14 @@ "defaultMessage": "!!!Send", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 445, + "line": 446, "column": 13, - "index": 15908 + "index": 15996 }, "end": { - "line": 448, + "line": 449, "column": 3, - "index": 15988 + "index": 16076 } }, { @@ -124,14 +124,14 @@ "defaultMessage": "!!!Scan QR code address", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 449, + "line": 450, "column": 18, - "index": 16008 + "index": 16096 }, "end": { - "line": 452, + "line": 453, "column": 3, - "index": 16109 + "index": 16197 } }, { @@ -139,14 +139,14 @@ "defaultMessage": "!!!Select asset", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 453, + "line": 454, "column": 20, - "index": 16131 + "index": 16219 }, "end": { - "line": 456, + "line": 457, "column": 3, - "index": 16220 + "index": 16308 } }, { @@ -154,14 +154,14 @@ "defaultMessage": "!!!Assets added", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 457, + "line": 458, "column": 26, - "index": 16248 + "index": 16336 }, "end": { - "line": 460, + "line": 461, "column": 3, - "index": 16349 + "index": 16437 } }, { @@ -169,14 +169,14 @@ "defaultMessage": "!!!Edit amount", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 461, + "line": 462, "column": 19, - "index": 16370 + "index": 16458 }, "end": { - "line": 464, + "line": 465, "column": 3, - "index": 16463 + "index": 16551 } }, { @@ -184,14 +184,14 @@ "defaultMessage": "!!!Confirm", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 465, + "line": 466, "column": 16, - "index": 16481 + "index": 16569 }, "end": { - "line": 468, + "line": 469, "column": 3, - "index": 16567 + "index": 16655 } }, { @@ -199,14 +199,14 @@ "defaultMessage": "!!!Share this address to receive payments. To protect your privacy, new addresses are generated automatically once you use them.", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 469, + "line": 470, "column": 19, - "index": 16588 + "index": 16676 }, "end": { - "line": 475, + "line": 476, "column": 3, - "index": 16826 + "index": 16914 } }, { @@ -214,14 +214,14 @@ "defaultMessage": "!!!Confirm transaction", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 476, + "line": 477, "column": 27, - "index": 16855 + "index": 16943 }, "end": { - "line": 479, + "line": 480, "column": 3, - "index": 16948 + "index": 17036 } }, { @@ -229,14 +229,14 @@ "defaultMessage": "!!!Please scan a QR code", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 480, + "line": 481, "column": 13, - "index": 16963 + "index": 17051 }, "end": { - "line": 483, + "line": 484, "column": 3, - "index": 17038 + "index": 17126 } }, { @@ -244,14 +244,14 @@ "defaultMessage": "!!!Success", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 484, + "line": 485, "column": 25, - "index": 17065 + "index": 17153 }, "end": { - "line": 487, + "line": 488, "column": 3, - "index": 17139 + "index": 17227 } }, { @@ -259,14 +259,14 @@ "defaultMessage": "!!!Request specific amount", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 488, + "line": 489, "column": 18, - "index": 17159 + "index": 17247 }, "end": { - "line": 491, + "line": 492, "column": 3, - "index": 17273 + "index": 17361 } }, { @@ -274,14 +274,14 @@ "defaultMessage": "!!!Buy/Sell ADA", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 492, + "line": 493, "column": 28, - "index": 17303 + "index": 17391 }, "end": { - "line": 495, + "line": 496, "column": 3, - "index": 17399 + "index": 17487 } }, { @@ -289,14 +289,14 @@ "defaultMessage": "!!!Buy provider", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 496, + "line": 497, "column": 29, - "index": 17430 + "index": 17518 }, "end": { - "line": 499, + "line": 500, "column": 3, - "index": 17538 + "index": 17626 } }, { @@ -304,14 +304,14 @@ "defaultMessage": "!!!Sell provider", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 500, + "line": 501, "column": 30, - "index": 17570 + "index": 17658 }, "end": { - "line": 503, + "line": 504, "column": 3, - "index": 17680 + "index": 17768 } }, { @@ -319,14 +319,14 @@ "defaultMessage": "!!!Tx Details", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 504, + "line": 505, "column": 18, - "index": 17700 + "index": 17788 }, "end": { - "line": 507, + "line": 508, "column": 3, - "index": 17794 + "index": 17882 } } ] \ No newline at end of file From 0837599ed17c4be806a0c3cbc7175109ac94286b Mon Sep 17 00:00:00 2001 From: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:32:45 +0100 Subject: [PATCH 2/6] refactor(wallet-mobile): claim module - stage 2 --- CONTRIBUTING.md | 33 +- .../Claim/common/useClaimErrorResolver.tsx | 2 +- .../features/Claim/module/ClaimProvider.tsx | 52 -- .../src/features/Claim/module/types.ts | 52 -- .../useCases/ShowSuccessScreen.stories.tsx | 6 +- .../Claim/useCases/ShowSuccessScreen.tsx | 4 +- .../Scan/common/useTriggerScanAction.tsx | 4 +- .../Transactions/TxHistoryNavigator.tsx | 4 +- apps/wallet-mobile/tsconfig.json | 2 +- packages/claim/.dependency-cruiser.js | 449 ++++++++++++++++++ packages/claim/.gitignore | 71 +++ packages/claim/.watchmanconfig | 1 + packages/claim/babel.config.js | 3 + packages/claim/jest.setup.js | 5 + packages/claim/package.json | 230 +++++++++ packages/claim/scripts/flowgen.sh | 3 + .../claim/src}/api-faucet.mocks.ts | 8 +- packages/claim/src/errors.ts | 10 + packages/claim/src/fixtures/wrapperMaker.tsx | 27 ++ packages/claim/src/index.ts | 10 + .../claim/src/manager.mocks.ts | 20 +- .../claim/src/manager.test.ts | 12 +- .../api.ts => packages/claim/src/manager.ts | 29 +- .../claim/src}/transformers.ts | 31 +- .../reactjs/hooks/useClaimTokens.test.tsx | 0 .../reactjs/hooks}/useClaimTokens.tsx | 15 +- .../reactjs/provider/ClaimProvider.test.tsx | 69 +++ .../reactjs/provider/ClaimProvider.tsx | 68 +++ .../translators/reactjs/state}/state.mocks.ts | 11 +- .../src/translators/reactjs/state}/state.ts | 44 +- .../claim/src}/validators.ts | 0 packages/claim/tsconfig.build.json | 5 + packages/claim/tsconfig.json | 25 + packages/types/src/claim/api.ts | 28 ++ packages/types/src/claim/claim.ts | 18 + .../types/src/claim}/errors.ts | 10 - packages/types/src/index.ts | 55 +++ .../types/src/scan/actions.ts | 13 +- packages/types/src/scan/errors.ts | 2 + scripts/install-dev-pkg.sh | 1 + scripts/install-pkgs.sh | 1 + yoroi.code-workspace | 12 +- 42 files changed, 1214 insertions(+), 231 deletions(-) delete mode 100644 apps/wallet-mobile/src/features/Claim/module/ClaimProvider.tsx delete mode 100644 apps/wallet-mobile/src/features/Claim/module/types.ts create mode 100644 packages/claim/.dependency-cruiser.js create mode 100644 packages/claim/.gitignore create mode 100644 packages/claim/.watchmanconfig create mode 100644 packages/claim/babel.config.js create mode 100644 packages/claim/jest.setup.js create mode 100644 packages/claim/package.json create mode 100644 packages/claim/scripts/flowgen.sh rename {apps/wallet-mobile/src/features/Claim/module => packages/claim/src}/api-faucet.mocks.ts (85%) create mode 100644 packages/claim/src/errors.ts create mode 100644 packages/claim/src/fixtures/wrapperMaker.tsx create mode 100644 packages/claim/src/index.ts rename apps/wallet-mobile/src/features/Claim/module/api.mocks.ts => packages/claim/src/manager.mocks.ts (77%) rename apps/wallet-mobile/src/features/Claim/module/api.tests.ts => packages/claim/src/manager.test.ts (51%) rename apps/wallet-mobile/src/features/Claim/module/api.ts => packages/claim/src/manager.ts (61%) rename {apps/wallet-mobile/src/features/Claim/module => packages/claim/src}/transformers.ts (69%) create mode 100644 packages/claim/src/translators/reactjs/hooks/useClaimTokens.test.tsx rename {apps/wallet-mobile/src/features/Claim/module => packages/claim/src/translators/reactjs/hooks}/useClaimTokens.tsx (51%) create mode 100644 packages/claim/src/translators/reactjs/provider/ClaimProvider.test.tsx create mode 100644 packages/claim/src/translators/reactjs/provider/ClaimProvider.tsx rename {apps/wallet-mobile/src/features/Claim/module => packages/claim/src/translators/reactjs/state}/state.mocks.ts (67%) rename {apps/wallet-mobile/src/features/Claim/module => packages/claim/src/translators/reactjs/state}/state.ts (73%) rename {apps/wallet-mobile/src/features/Claim/module => packages/claim/src}/validators.ts (100%) create mode 100644 packages/claim/tsconfig.build.json create mode 100644 packages/claim/tsconfig.json create mode 100644 packages/types/src/claim/api.ts create mode 100644 packages/types/src/claim/claim.ts rename {apps/wallet-mobile/src/features/Claim/module => packages/types/src/claim}/errors.ts (64%) rename apps/wallet-mobile/src/features/Scan/common/types.ts => packages/types/src/scan/actions.ts (62%) create mode 100644 packages/types/src/scan/errors.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43c933c5c5..5a6da0a51c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,31 +19,25 @@ While developing, you can run the [example app](/example/) to test your changes. To start the packager: ```sh -yarn example start +yarn start ``` To run the example app on Android: ```sh -yarn example android +yarn run:android:<> ``` To run the example app on iOS: ```sh -yarn example ios -``` - -To run the example app on Web: - -```sh -yarn example web +yarn run:ios:<> ``` Make sure your code passes TypeScript and ESLint. Run the following to verify: ```sh -yarn typecheck +yarn tsc yarn lint ``` @@ -59,17 +53,18 @@ Remember to add tests for your change if possible. Run the unit tests by: yarn test ``` - ### Commit message convention We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: - `fix`: bug fixes, e.g. fix crash due to deprecated method. - `feat`: new features, e.g. add new method to the module. +- `feature`: new features, e.g. add new method to the module. - `refactor`: code refactor, e.g. migrate from class components to hooks. - `docs`: changes into documentation, e.g. add usage example for the module.. - `test`: adding or updating tests, e.g. add integration tests using detox. - `chore`: tooling changes, e.g. change CI config. +- `merge`: merging actions. Our pre-commit hooks verify that your commit message matches this format when committing. @@ -88,23 +83,9 @@ We use [release-it](https://github.com/release-it/release-it) to make it easier To publish new versions, run the following: ```sh -yarn release +fastlane release <> ``` -### Scripts - -The `package.json` file contains various scripts for common tasks: - -- `yarn bootstrap`: setup project by installing all dependencies and pods. -- `yarn typecheck`: type-check files with TypeScript. -- `yarn lint`: lint files with ESLint. -- `yarn test`: run unit tests with Jest. -- `yarn example start`: start the Metro server for the example app. -- `yarn example android`: run the example app on Android. -- `yarn example ios`: run the example app on iOS. - -### Sending a pull request - > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). When you're sending a pull request: diff --git a/apps/wallet-mobile/src/features/Claim/common/useClaimErrorResolver.tsx b/apps/wallet-mobile/src/features/Claim/common/useClaimErrorResolver.tsx index b04bae9d54..b89de17493 100644 --- a/apps/wallet-mobile/src/features/Claim/common/useClaimErrorResolver.tsx +++ b/apps/wallet-mobile/src/features/Claim/common/useClaimErrorResolver.tsx @@ -6,7 +6,7 @@ import { ClaimApiErrorsNotFound, ClaimApiErrorsRateLimited, ClaimApiErrorsTooEarly, -} from '../module/errors' +} from '../../../../../../packages/claim/src/errors' import {useDialogs} from './useDialogs' export const useClaimErrorResolver = () => { diff --git a/apps/wallet-mobile/src/features/Claim/module/ClaimProvider.tsx b/apps/wallet-mobile/src/features/Claim/module/ClaimProvider.tsx deleted file mode 100644 index 419c698b5e..0000000000 --- a/apps/wallet-mobile/src/features/Claim/module/ClaimProvider.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import {invalid} from '@yoroi/common' -import * as React from 'react' - -import {ScanActionClaim} from '../../Scan/common/types' -import {claimApiMockInstances} from './api.mocks' -import {ClaimActions, ClaimActionType, claimReducer, defaultClaimActions, defaultClaimState} from './state' -import {ClaimApi, ClaimInfo, ClaimState} from './types' - -export type ClaimProviderContext = React.PropsWithChildren<ClaimApi & ClaimState & ClaimActions> - -const initialClaimProvider: ClaimProviderContext = { - ...defaultClaimState, - ...defaultClaimActions, - ...claimApiMockInstances.error, -} -const ClaimContext = React.createContext<ClaimProviderContext>(initialClaimProvider) - -type ClaimProviderProps = React.PropsWithChildren<{ - claimApi: ClaimApi - initialState?: ClaimState -}> -export const ClaimProvider = ({children, claimApi, initialState}: ClaimProviderProps) => { - const [state, dispatch] = React.useReducer(claimReducer, { - ...defaultClaimState, - ...initialState, - }) - - const actions = React.useRef<ClaimActions>({ - claimInfoChanged: (claimInfo: ClaimInfo) => { - dispatch({type: ClaimActionType.ClaimInfoChanged, claimInfo}) - }, - scanActionClaimChanged: (scanActionClaim: ScanActionClaim) => { - dispatch({type: ClaimActionType.ScanActionClaimChanged, scanActionClaim}) - }, - reset: () => { - dispatch({type: ClaimActionType.Reset}) - }, - }).current - - const context = React.useMemo<ClaimProviderContext>( - () => ({ - ...state, - ...claimApi, - ...actions, - }), - [state, claimApi, actions], - ) - - return <ClaimContext.Provider value={context}>{children}</ClaimContext.Provider> -} -export const useClaim = () => - React.useContext(ClaimContext) ?? invalid('useClaim: needs to be wrapped in a ClaimManagerProvider') diff --git a/apps/wallet-mobile/src/features/Claim/module/types.ts b/apps/wallet-mobile/src/features/Claim/module/types.ts deleted file mode 100644 index 5126775cbf..0000000000 --- a/apps/wallet-mobile/src/features/Claim/module/types.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {Portfolio} from '@yoroi/types' - -import {ScanActionClaim} from '../../Scan/common/types' - -export type ClaimApiClaimTokensRequestPayload = { - address: string - code: string - [key: string]: unknown -} - -export type ClaimApiClaimTokensResponse = { - lovelaces: string - tokens: { - [tokenId: string]: string - } -} & ( - | { - // code: 200 - status: 'accepted' - queue_position: number - } - | { - // code: 201 - status: 'queued' - queue_position: number - } - | { - // code: 202 - status: 'claimed' - tx_hash: string - } -) - -export type ClaimStatus = 'accepted' | 'processing' | 'done' - -export type ClaimInfo = Readonly<{ - // api - status: ClaimStatus - amounts: ReadonlyArray<Portfolio.Token.Amount> - txHash?: string -}> - -export type ClaimApi = Readonly<{ - claimTokens: (action: ScanActionClaim) => Promise<ClaimInfo> - address: string - primaryTokenInfo: Portfolio.Token.Info -}> - -export type ClaimState = Readonly<{ - claimInfo: ClaimInfo | undefined - scanActionClaim: ScanActionClaim | undefined -}> diff --git a/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.stories.tsx b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.stories.tsx index 95dbba24af..c57fae2725 100644 --- a/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.stories.tsx +++ b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.stories.tsx @@ -6,9 +6,9 @@ import {QueryClientProvider} from 'react-query' import {queryClientFixture} from '../../../kernel/fixtures/fixtures' import {mocks as walletMocks} from '../../../yoroi-wallets/mocks/wallet' import {WalletManagerProviderMock} from '../../../yoroi-wallets/mocks/WalletManagerProviderMock' -import {claimApiMockInstances} from '../module/api.mocks' -import {ClaimProvider} from '../module/ClaimProvider' -import {mocks as claimMocks} from '../module/state.mocks' +import {claimApiMockInstances} from '../../../../../../packages/claim/src/manager.mocks' +import {ClaimProvider} from '../../../../../../packages/claim/src/translators/reactjs/ClaimProvider' +import {mocks as claimMocks} from '../../../../../../packages/claim/src/translators/reactjs/state.mocks' import {ShowSuccessScreen} from './ShowSuccessScreen' const AppDecorator: DecoratorFunction<React.ReactNode> = (story) => { diff --git a/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx index ff7e35a2cc..7ebf4a63d8 100644 --- a/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx +++ b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx @@ -18,8 +18,8 @@ import {useDialogs} from '../common/useDialogs' import {useNavigateTo} from '../common/useNavigateTo' import {useStrings} from '../common/useStrings' import {ClaimSuccessIllustration} from '../illustrations/ClaimSuccessIllustration' -import {useClaim} from '../module/ClaimProvider' -import {ClaimStatus} from '../module/types' +import {useClaim} from '../../../../../../packages/claim/src/translators/reactjs/ClaimProvider' +import {ClaimStatus} from '../../../../../../packages/claim/src/types' export const ShowSuccessScreen = () => { const {styles} = useStyles() diff --git a/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx b/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx index 362a7679dc..759de02b7e 100644 --- a/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx +++ b/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx @@ -6,8 +6,8 @@ import {Alert} from 'react-native' import {useModal} from '../../../components/Modal/ModalContext' import {useClaimErrorResolver} from '../../../features/Claim/common/useClaimErrorResolver' import {useStrings as useStringsClaim} from '../../../features/Claim/common/useStrings' -import {useClaim} from '../../../features/Claim/module/ClaimProvider' -import {useClaimTokens} from '../../../features/Claim/module/useClaimTokens' +import {useClaim} from '../../../../../../packages/claim/src/translators/reactjs/ClaimProvider' +import {useClaimTokens} from '../../../../../../packages/claim/src/translators/reactjs/useClaimTokens' import {AskConfirmation} from '../../../features/Claim/useCases/AskConfirmation' import {pastedFormatter} from '../../../yoroi-wallets/utils/amountUtils' import {useSelectedWallet} from '../../WalletManager/common/hooks/useSelectedWallet' diff --git a/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx b/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx index 2079a2f99a..f286e9bb19 100644 --- a/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx +++ b/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx @@ -18,8 +18,8 @@ import { TxHistoryRouteNavigation, TxHistoryRoutes, } from '../../kernel/navigation' -import {claimApiMaker} from '../Claim/module/api' -import {ClaimProvider} from '../Claim/module/ClaimProvider' +import {claimApiMaker} from '../../../../../packages/claim/src/manager' +import {ClaimProvider} from '../../../../../packages/claim/src/translators/reactjs/ClaimProvider' import {ShowSuccessScreen} from '../Claim/useCases/ShowSuccessScreen' import {CreateExchangeOrderScreen} from '../Exchange/useCases/CreateExchangeOrderScreen/CreateExchangeOrderScreen' import {SelectProviderFromListScreen} from '../Exchange/useCases/SelectProviderFromListScreen/SelectProviderFromListScreen' diff --git a/apps/wallet-mobile/tsconfig.json b/apps/wallet-mobile/tsconfig.json index 2c4459c40b..680ebe3489 100644 --- a/apps/wallet-mobile/tsconfig.json +++ b/apps/wallet-mobile/tsconfig.json @@ -13,5 +13,5 @@ "*.md": ["text-loader"] } }, - "include": ["./.d.ts", "react-native-svg-charts.d.ts", "./src", ".storybook/decorators"] + "include": ["./.d.ts", "react-native-svg-charts.d.ts", "./src", ".storybook/decorators", "../../packages/claim/src/api-faucet.mocks.ts", "../../packages/claim/src/manager.mocks.ts", "../../packages/claim/src/manager.tests.ts", "../../packages/claim/src/manager.ts", "../../packages/claim/src/translators/reactjs/ClaimProvider.tsx", "../../packages/claim/src/errors.ts", "../../packages/claim/src/translators/reactjs/state.mocks.ts", "../../packages/claim/src/translators/reactjs/state.ts", "../../packages/claim/src/transformers.ts", "../../packages/claim/src/types.ts", "../../packages/claim/src/translators/reactjs/useClaimTokens.tsx", "../../packages/claim/src/validators.ts"] } diff --git a/packages/claim/.dependency-cruiser.js b/packages/claim/.dependency-cruiser.js new file mode 100644 index 0000000000..fff414129a --- /dev/null +++ b/packages/claim/.dependency-cruiser.js @@ -0,0 +1,449 @@ +/** @type {import('dependency-cruiser').IConfiguration} */ +module.exports = { + forbidden: [ + /* rules from the 'recommended' preset: */ + { + name: 'fix-circular', + severity: 'error', + comment: + 'This dependency is part of a circular relationship. You might want to revise ' + + 'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ', + from: {}, + to: { + circular: true + } + }, + { + name: 'no-orphans', + comment: + "This is an orphan module - it's likely not used (anymore?). Either use it or " + + "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + + "add an exception for it in your dependency-cruiser configuration. By default " + + "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + + "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", + severity: 'warn', + from: { + orphan: true, + pathNot: [ + '(^|/)\\.[^/]+\\.(js|cjs|mjs|ts|json)$', // dot files + '\\.d\\.ts$', // TypeScript declaration files + '(^|/)tsconfig\\.json$', // TypeScript config + '(^|/)(babel|webpack)\\.config\\.(js|cjs|mjs|ts|json)$' // other configs + ] + }, + to: {}, + }, + { + name: 'no-deprecated-core', + comment: + 'A module depends on a node core module that has been deprecated. Find an alternative - these are ' + + "bound to exist - node doesn't deprecate lightly.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'core' + ], + path: [ + '^(v8\/tools\/codemap)$', + '^(v8\/tools\/consarray)$', + '^(v8\/tools\/csvparser)$', + '^(v8\/tools\/logreader)$', + '^(v8\/tools\/profile_view)$', + '^(v8\/tools\/profile)$', + '^(v8\/tools\/SourceMap)$', + '^(v8\/tools\/splaytree)$', + '^(v8\/tools\/tickprocessor-driver)$', + '^(v8\/tools\/tickprocessor)$', + '^(node-inspect\/lib\/_inspect)$', + '^(node-inspect\/lib\/internal\/inspect_client)$', + '^(node-inspect\/lib\/internal\/inspect_repl)$', + '^(async_hooks)$', + '^(punycode)$', + '^(domain)$', + '^(constants)$', + '^(sys)$', + '^(_linklist)$', + '^(_stream_wrap)$' + ], + } + }, + { + name: 'not-to-deprecated', + comment: + 'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' + + 'version of that module, or find an alternative. Deprecated modules are a security risk.', + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'deprecated' + ] + } + }, + { + name: 'no-non-package-json', + severity: 'error', + comment: + "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + + "That's problematic as the package either (1) won't be available on live (2 - worse) will be " + + "available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " + + "in your package.json.", + from: {}, + to: { + dependencyTypes: [ + 'npm-no-pkg', + 'npm-unknown' + ] + } + }, + { + name: 'not-to-unresolvable', + comment: + "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + + 'module: add it to your package.json. In all other cases you likely already know what to do.', + severity: 'error', + from: {}, + to: { + couldNotResolve: true + } + }, + { + name: 'no-duplicate-dep-types', + comment: + "Likely this module depends on an external ('npm') package that occurs more than once " + + "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + + "maintenance problems later on.", + severity: 'warn', + from: {}, + to: { + moreThanOneDependencyType: true, + // as it's pretty common to have a type import be a type only import + // _and_ (e.g.) a devDependency - don't consider type-only dependency + // types for this rule + dependencyTypesNot: ["type-only"] + } + }, + + /* rules you might want to tweak for your specific situation: */ + { + name: 'not-to-spec', + comment: + 'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' + + "If there's something in a spec that's of use to other modules, it doesn't have that single " + + 'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.', + severity: 'error', + from: {}, + to: { + path: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$' + } + }, + { + name: 'not-to-dev-dep', + severity: 'error', + comment: + "This module depends on an npm package from the 'devDependencies' section of your " + + 'package.json. It looks like something that ships to production, though. To prevent problems ' + + "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + + 'section of your package.json. If this module is development only - add it to the ' + + 'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration', + from: { + path: '^(src)', + pathNot: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$' + }, + to: { + dependencyTypes: [ + 'npm-dev' + ] + } + }, + { + name: 'optional-deps-used', + severity: 'info', + comment: + "This module depends on an npm package that is declared as an optional dependency " + + "in your package.json. As this makes sense in limited situations only, it's flagged here. " + + "If you're using an optional dependency here by design - add an exception to your" + + "dependency-cruiser configuration.", + from: {}, + to: { + dependencyTypes: [ + 'npm-optional' + ] + } + }, + { + name: 'peer-deps-used', + comment: + "This module depends on an npm package that is declared as a peer dependency " + + "in your package.json. This makes sense if your package is e.g. a plugin, but in " + + "other cases - maybe not so much. If the use of a peer dependency is intentional " + + "add an exception to your dependency-cruiser configuration.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'npm-peer' + ] + } + } + ], + options: { + + /* conditions specifying which files not to follow further when encountered: + - path: a regular expression to match + - dependencyTypes: see https://github.com/sverweij/dependency-cruiser/blob/master/doc/rules-reference.md#dependencytypes-and-dependencytypesnot + for a complete list + */ + doNotFollow: { + path: 'node_modules' + }, + + /* conditions specifying which dependencies to exclude + - path: a regular expression to match + - dynamic: a boolean indicating whether to ignore dynamic (true) or static (false) dependencies. + leave out if you want to exclude neither (recommended!) + */ + // exclude : { + // path: '', + // dynamic: true + // }, + + /* pattern specifying which files to include (regular expression) + dependency-cruiser will skip everything not matching this pattern + */ + // includeOnly : '', + + /* dependency-cruiser will include modules matching against the focus + regular expression in its output, as well as their neighbours (direct + dependencies and dependents) + */ + // focus : '', + + /* list of module systems to cruise */ + // moduleSystems: ['amd', 'cjs', 'es6', 'tsd'], + + /* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/develop/' + to open it on your online repo or `vscode://file/${process.cwd()}/` to + open it in visual studio code), + */ + // prefix: '', + + /* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation + true: also detect dependencies that only exist before typescript-to-javascript compilation + "specify": for each dependency identify whether it only exists before compilation or also after + */ + tsPreCompilationDeps: true, + + /* + list of extensions to scan that aren't javascript or compile-to-javascript. + Empty by default. Only put extensions in here that you want to take into + account that are _not_ parsable. + */ + // extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"], + + /* if true combines the package.jsons found from the module up to the base + folder the cruise is initiated from. Useful for how (some) mono-repos + manage dependencies & dependency definitions. + */ + // combinedDependencies: false, + + /* if true leave symlinks untouched, otherwise use the realpath */ + // preserveSymlinks: false, + + /* TypeScript project file ('tsconfig.json') to use for + (1) compilation and + (2) resolution (e.g. with the paths property) + + The (optional) fileName attribute specifies which file to take (relative to + dependency-cruiser's current working directory). When not provided + defaults to './tsconfig.json'. + */ + tsConfig: { + fileName: 'tsconfig.json' + }, + + /* Webpack configuration to use to get resolve options from. + + The (optional) fileName attribute specifies which file to take (relative + to dependency-cruiser's current working directory. When not provided defaults + to './webpack.conf.js'. + + The (optional) `env` and `args` attributes contain the parameters to be passed if + your webpack config is a function and takes them (see webpack documentation + for details) + */ + // webpackConfig: { + // fileName: './webpack.config.js', + // env: {}, + // args: {}, + // }, + + /* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use + for compilation (and whatever other naughty things babel plugins do to + source code). This feature is well tested and usable, but might change + behavior a bit over time (e.g. more precise results for used module + systems) without dependency-cruiser getting a major version bump. + */ + // babelConfig: { + // fileName: './.babelrc' + // }, + + /* List of strings you have in use in addition to cjs/ es6 requires + & imports to declare module dependencies. Use this e.g. if you've + re-declared require, use a require-wrapper or use window.require as + a hack. + */ + // exoticRequireStrings: [], + /* options to pass on to enhanced-resolve, the package dependency-cruiser + uses to resolve module references to disk. You can set most of these + options in a webpack.conf.js - this section is here for those + projects that don't have a separate webpack config file. + + Note: settings in webpack.conf.js override the ones specified here. + */ + enhancedResolveOptions: { + /* List of strings to consider as 'exports' fields in package.json. Use + ['exports'] when you use packages that use such a field and your environment + supports it (e.g. node ^12.19 || >=14.7 or recent versions of webpack). + + If you have an `exportsFields` attribute in your webpack config, that one + will have precedence over the one specified here. + */ + exportsFields: ["exports"], + /* List of conditions to check for in the exports field. e.g. use ['imports'] + if you're only interested in exposed es6 modules, ['require'] for commonjs, + or all conditions at once `(['import', 'require', 'node', 'default']`) + if anything goes for you. Only works when the 'exportsFields' array is + non-empty. + + If you have a 'conditionNames' attribute in your webpack config, that one will + have precedence over the one specified here. + */ + conditionNames: ["import", "require", "node", "default"], + /* + The extensions, by default are the same as the ones dependency-cruiser + can access (run `npx depcruise --info` to see which ones that are in + _your_ environment. If that list is larger than what you need (e.g. + it contains .js, .jsx, .ts, .tsx, .cts, .mts - but you don't use + TypeScript you can pass just the extensions you actually use (e.g. + [".js", ".jsx"]). This can speed up the most expensive step in + dependency cruising (module resolution) quite a bit. + */ + // extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"], + /* + If your TypeScript project makes use of types specified in 'types' + fields in package.jsons of external dependencies, specify "types" + in addition to "main" in here, so enhanced-resolve (the resolver + dependency-cruiser uses) knows to also look there. You can also do + this if you're not sure, but still use TypeScript. In a future version + of dependency-cruiser this will likely become the default. + */ + mainFields: ["main", "types"], + }, + reporterOptions: { + dot: { + /* pattern of modules that can be consolidated in the detailed + graphical dependency graph. The default pattern in this configuration + collapses everything in node_modules to one folder deep so you see + the external modules, but not the innards your app depends upon. + */ + collapsePattern: 'node_modules/[^/]+', + + /* Options to tweak the appearance of your graph.See + https://github.com/sverweij/dependency-cruiser/blob/master/doc/options-reference.md#reporteroptions + for details and some examples. If you don't specify a theme + don't worry - dependency-cruiser will fall back to the default one. + */ + // theme: { + // graph: { + // /* use splines: "ortho" for straight lines. Be aware though + // graphviz might take a long time calculating ortho(gonal) + // routings. + // */ + // splines: "true" + // }, + // modules: [ + // { + // criteria: { matchesFocus: true }, + // attributes: { + // fillcolor: "lime", + // penwidth: 2, + // }, + // }, + // { + // criteria: { matchesFocus: false }, + // attributes: { + // fillcolor: "lightgrey", + // }, + // }, + // { + // criteria: { matchesReaches: true }, + // attributes: { + // fillcolor: "lime", + // penwidth: 2, + // }, + // }, + // { + // criteria: { matchesReaches: false }, + // attributes: { + // fillcolor: "lightgrey", + // }, + // }, + // { + // criteria: { source: "^src/model" }, + // attributes: { fillcolor: "#ccccff" } + // }, + // { + // criteria: { source: "^src/view" }, + // attributes: { fillcolor: "#ccffcc" } + // }, + // ], + // dependencies: [ + // { + // criteria: { "rules[0].severity": "error" }, + // attributes: { fontcolor: "red", color: "red" } + // }, + // { + // criteria: { "rules[0].severity": "warn" }, + // attributes: { fontcolor: "orange", color: "orange" } + // }, + // { + // criteria: { "rules[0].severity": "info" }, + // attributes: { fontcolor: "blue", color: "blue" } + // }, + // { + // criteria: { resolved: "^src/model" }, + // attributes: { color: "#0000ff77" } + // }, + // { + // criteria: { resolved: "^src/view" }, + // attributes: { color: "#00770077" } + // } + // ] + // } + }, + archi: { + /* pattern of modules that can be consolidated in the high level + graphical dependency graph. If you use the high level graphical + dependency graph reporter (`archi`) you probably want to tweak + this collapsePattern to your situation. + */ + collapsePattern: '^(packages|src|lib|app|bin|test(s?)|spec(s?))/[^/]+|node_modules/[^/]+', + + /* Options to tweak the appearance of your graph.See + https://github.com/sverweij/dependency-cruiser/blob/master/doc/options-reference.md#reporteroptions + for details and some examples. If you don't specify a theme + for 'archi' dependency-cruiser will use the one specified in the + dot section (see above), if any, and otherwise use the default one. + */ + // theme: { + // }, + }, + "text": { + "highlightFocused": true + }, + } + } +}; +// generated: dependency-cruiser@12.10.0 on 2023-03-08T01:53:10.874Z \ No newline at end of file diff --git a/packages/claim/.gitignore b/packages/claim/.gitignore new file mode 100644 index 0000000000..32c6d7db77 --- /dev/null +++ b/packages/claim/.gitignore @@ -0,0 +1,71 @@ +# OSX +# +.DS_Store + +# XDE +.expo/ + +# VSCode +.vscode/ +!.vscode/launch.json +jsconfig.json + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IJ +# +.classpath +.cxx +.gradle +.idea +.project +.settings +local.properties +android.iml + +# Cocoapods +# +example/ios/Pods + +# Ruby +example/vendor/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +android/app/libs +android/keystores/debug.keystore + +# Expo +.expo/ + +# Turborepo +.turbo/ + +# generated by bob +lib/ diff --git a/packages/claim/.watchmanconfig b/packages/claim/.watchmanconfig new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/claim/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/packages/claim/babel.config.js b/packages/claim/babel.config.js new file mode 100644 index 0000000000..f842b77fcf --- /dev/null +++ b/packages/claim/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:metro-react-native-babel-preset'], +}; diff --git a/packages/claim/jest.setup.js b/packages/claim/jest.setup.js new file mode 100644 index 0000000000..cc9ceed812 --- /dev/null +++ b/packages/claim/jest.setup.js @@ -0,0 +1,5 @@ +jest.mock('@react-native-async-storage/async-storage', () => + require('@react-native-async-storage/async-storage/jest/async-storage-mock'), +) + +jest.setTimeout(80000) \ No newline at end of file diff --git a/packages/claim/package.json b/packages/claim/package.json new file mode 100644 index 0000000000..80100196fe --- /dev/null +++ b/packages/claim/package.json @@ -0,0 +1,230 @@ +{ + "name": "@yoroi/claim", + "version": "2.0.1", + "description": "The Claim (proof-of-onboarding) package of Yoroi SDK", + "keywords": [ + "yoroi", + "cardano", + "claim", + "claims", + "browser", + "mobile", + "react", + "react-native", + "cip99", + "cip-99", + "cip-0099", + "proof-of-onboarding" + ], + "homepage": "https://github.com/Emurgo/yoroi/packages/claim#readme", + "bugs": { + "url": "https://github.com/Emurgo/yoroi/issues" + }, + "repository": { + "type": "github", + "url": "https://github.com/Emurgo/yoroi.git", + "directory": "packages/claim" + }, + "license": "Apache-2.0", + "author": "EMURGO Fintech <support@emurgo.com> (https://github.com/Emurgo/yoroi)", + "contributors": [ + { + "name": "Juliano Lazzarotto", + "email": "30806844+stackchain@users.noreply.github.com" + } + ], + "main": "lib/commonjs/index", + "module": "lib/module/index", + "source": "src/index", + "browser": "lib/module/index", + "types": "lib/typescript/index.d.ts", + "files": [ + "src", + "lib", + "!ios/build", + "!android/build", + "!android/gradle", + "!android/gradlew", + "!android/gradlew.bat", + "!android/local.properties", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__", + "!**/.*" + ], + "scripts": { + "build": "yarn tsc && yarn lint && yarn test --ci --silent && yarn clean && bob build", + "build:dev": "yarn tsc && yarn clean && bob build", + "build:release": "yarn build && yarn flow", + "clean": "del-cli lib", + "dev": "yarn clean && bob build", + "dgraph": "depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg", + "flow": ". ./scripts/flowgen.sh", + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "prepack": "yarn build:release", + "prepublish:beta": "yarn build:release", + "publish:beta": "npm publish --scope yoroi --tag beta --access beta", + "prepublish:prod": "yarn build:release", + "publish:prod": "npm publish --scope yoroi --access public", + "release": "release-it", + "test": "jest --maxWorkers=1", + "test:watch": "jest --watch", + "tsc": "tsc --noEmit -p tsconfig.json" + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "prettier": { + "bracketSpacing": false, + "quoteProps": "consistent", + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false + }, + "eslintConfig": { + "extends": [ + "@react-native-community", + "prettier" + ], + "rules": { + "prettier/prettier": [ + "error", + { + "quoteProps": "consistent", + "bracketSpacing": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false, + "semi": false + } + ] + }, + "root": true + }, + "eslintIgnore": [ + "node_modules/", + "lib/", + "babel.config.js", + "jest.setup.js", + "coverage/" + ], + "jest": { + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{js,jsx,ts,tsx}", + "!src/**/*.d.ts", + "!src/**/*.mocks.{js,jsx,ts,tsx}" + ], + "coverageReporters": [ + "text-summary", + "html" + ], + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": 100 + } + }, + "modulePathIgnorePatterns": [ + "<rootDir>/example/node_modules", + "<rootDir>/lib/" + ], + "preset": "react-native", + "setupFiles": [ + "<rootDir>/jest.setup.js" + ] + }, + "devDependencies": { + "@commitlint/config-conventional": "^17.0.2", + "@emurgo/yoroi-lib": "^1.0.1", + "@react-native-async-storage/async-storage": "^1.19.3", + "@react-native-community/eslint-config": "^3.0.2", + "@release-it/conventional-changelog": "^5.0.0", + "@testing-library/react-hooks": "^8.0.1", + "@testing-library/react-native": "^12.3.0", + "@tsconfig/react-native": "^3.0.3", + "@types/jest": "^29.5.12", + "@types/react": "^18.2.55", + "@types/react-test-renderer": "^18.0.7", + "@yoroi/common": "^1.5.4", + "@yoroi/portfolio": "1.0.3", + "@yoroi/types": "^1.5.7", + "bignumber.js": "^9.0.1", + "commitlint": "^17.0.2", + "del-cli": "^5.0.0", + "dependency-cruiser": "^13.1.1", + "eslint": "^8.4.1", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-ft-flow": "^3.0.0", + "eslint-plugin-prettier": "^4.0.0", + "flowgen": "^1.21.0", + "immer": "^10.0.2", + "jest": "^29.7.0", + "pod-install": "^0.1.0", + "prettier": "^2.0.5", + "react": "18.2.0", + "react-native": "~0.71.0", + "react-native-builder-bob": "^0.23.2", + "react-query": "^3.39.3", + "react-test-renderer": "^18.2.0", + "release-it": "^15.0.0", + "typescript": "^5.3.3" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": ">= 1.19.3 <= 1.20.0", + "@yoroi/common": "1.5.4", + "@yoroi/portfolio": "1.0.3", + "immer": "^10.0.2", + "react": ">= 16.8.0 <= 19.0.0", + "react-query": "^3.39.3" + }, + "optionalDependencies": { + "@react-native-async-storage/async-storage": "^1.19.3" + }, + "packageManager": "yarn@1.22.21", + "engines": { + "node": ">= 16.19.0" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + "commonjs", + "module", + [ + "typescript", + { + "project": "tsconfig.build.json", + "tsc": "./node_modules/.bin/tsc" + } + ] + ] + }, + "release-it": { + "git": { + "commitMessage": "chore: release ${version}", + "tagName": "v${version}" + }, + "npm": { + "publish": true + }, + "github": { + "release": false + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": "angular" + } + } + } +} diff --git a/packages/claim/scripts/flowgen.sh b/packages/claim/scripts/flowgen.sh new file mode 100644 index 0000000000..2638ef8d27 --- /dev/null +++ b/packages/claim/scripts/flowgen.sh @@ -0,0 +1,3 @@ +for i in $(find lib -type f -name "*.d.ts"); + do sh -c "npx flowgen $i -o ${i%.*.*}.js.flow"; +done; diff --git a/apps/wallet-mobile/src/features/Claim/module/api-faucet.mocks.ts b/packages/claim/src/api-faucet.mocks.ts similarity index 85% rename from apps/wallet-mobile/src/features/Claim/module/api-faucet.mocks.ts rename to packages/claim/src/api-faucet.mocks.ts index 9e07fa022a..5dcb7a5222 100644 --- a/apps/wallet-mobile/src/features/Claim/module/api-faucet.mocks.ts +++ b/packages/claim/src/api-faucet.mocks.ts @@ -1,8 +1,10 @@ import {tokenMocks} from '@yoroi/portfolio' +import {Claim} from '@yoroi/types' -import {ClaimApiClaimTokensResponse} from './types' - -const claimTokens: Record<string, {[key: string]: ClaimApiClaimTokensResponse}> = { +const claimTokens: Record< + string, + {[key: string]: Claim.Api.ClaimTokensResponse} +> = { success: { accepted: { status: 'accepted', diff --git a/packages/claim/src/errors.ts b/packages/claim/src/errors.ts new file mode 100644 index 0000000000..ce55f2f6c9 --- /dev/null +++ b/packages/claim/src/errors.ts @@ -0,0 +1,10 @@ +import {Claim} from '@yoroi/types' + +export const claimApiErrors = [ + Claim.Api.Errors.InvalidRequest, + Claim.Api.Errors.NotFound, + Claim.Api.Errors.AlreadyClaimed, + Claim.Api.Errors.Expired, + Claim.Api.Errors.TooEarly, + Claim.Api.Errors.RateLimited, +] as const diff --git a/packages/claim/src/fixtures/wrapperMaker.tsx b/packages/claim/src/fixtures/wrapperMaker.tsx new file mode 100644 index 0000000000..11323458d0 --- /dev/null +++ b/packages/claim/src/fixtures/wrapperMaker.tsx @@ -0,0 +1,27 @@ +import {ErrorBoundary, SuspenseBoundary} from '@yoroi/common' +import {Claim} from '@yoroi/types' +import * as React from 'react' +import {QueryClient, QueryClientProvider} from 'react-query' +import {ClaimProvider} from '../translators/reactjs/provider/ClaimProvider' +import {ClaimState} from '../translators/reactjs/state/state' + +type Props = { + queryClient: QueryClient + claimManager: Claim.Manager + initialState?: ClaimState +} + +export const wrapperMaker = + ({queryClient, claimManager, initialState}: Props) => + ({children}: {children: React.ReactNode}) => + ( + <QueryClientProvider client={queryClient}> + <ErrorBoundary> + <SuspenseBoundary> + <ClaimProvider manager={claimManager} initialState={initialState}> + {children} + </ClaimProvider> + </SuspenseBoundary> + </ErrorBoundary> + </QueryClientProvider> + ) diff --git a/packages/claim/src/index.ts b/packages/claim/src/index.ts new file mode 100644 index 0000000000..6462533927 --- /dev/null +++ b/packages/claim/src/index.ts @@ -0,0 +1,10 @@ +export * from './errors' + +export * from './manager' +export * from './manager.mocks' + +export * from './translators/reactjs/hooks/useClaimTokens' +export * from './translators/reactjs/provider/ClaimProvider' + +export * from './transformers' +export * from './validators' diff --git a/apps/wallet-mobile/src/features/Claim/module/api.mocks.ts b/packages/claim/src/manager.mocks.ts similarity index 77% rename from apps/wallet-mobile/src/features/Claim/module/api.mocks.ts rename to packages/claim/src/manager.mocks.ts index 17a31c4c6f..a2810e9183 100644 --- a/apps/wallet-mobile/src/features/Claim/module/api.mocks.ts +++ b/packages/claim/src/manager.mocks.ts @@ -1,8 +1,7 @@ import {tokenMocks} from '@yoroi/portfolio' +import {Claim} from '@yoroi/types' -import {ClaimApi, ClaimInfo} from './types' - -const claimTokensResponse: {[key: string]: ClaimInfo} = { +const claimTokensResponse: {[key: string]: Claim.Info} = { accepted: { status: 'accepted', amounts: [ @@ -77,7 +76,7 @@ const claimTokensApi = { return Promise.reject(new Error('Something went wrong')) }, loading: () => { - return new Promise(() => null) as unknown as ClaimInfo + return new Promise(() => null) as unknown as Claim.Info }, } as const @@ -85,12 +84,19 @@ export const claimApiMockFetchers = { claimTokens: claimTokensApi, } as const -const claimApiError: ClaimApi = { +const claimManagerError: Claim.Manager = { claimTokens: claimTokensApi.error, address: 'address', primaryTokenInfo: tokenMocks.primaryETH.info, } as const -export const claimApiMockInstances = { - error: claimApiError, +const claimManagerSuccessProcessing: Claim.Manager = { + claimTokens: claimTokensApi.success.processing as () => Promise<Claim.Info>, + address: 'address', + primaryTokenInfo: tokenMocks.primaryETH.info, +} as const + +export const claimManagerMockInstances = { + error: claimManagerError, + processing: claimManagerSuccessProcessing, } as const diff --git a/apps/wallet-mobile/src/features/Claim/module/api.tests.ts b/packages/claim/src/manager.test.ts similarity index 51% rename from apps/wallet-mobile/src/features/Claim/module/api.tests.ts rename to packages/claim/src/manager.test.ts index 42d856ed30..5146cb447d 100644 --- a/apps/wallet-mobile/src/features/Claim/module/api.tests.ts +++ b/packages/claim/src/manager.test.ts @@ -2,9 +2,9 @@ import {fetchData} from '@yoroi/common' import {tokenInfoMocks} from '@yoroi/portfolio' import {Portfolio} from '@yoroi/types' -import {claimApiMaker} from './api' +import {claimManagerMaker} from './manager' -describe('claimApiMaker', () => { +describe('claimManagerMaker', () => { const options = { address: 'addr_test', primaryTokenInfo: tokenInfoMocks.primaryETH, @@ -12,10 +12,10 @@ describe('claimApiMaker', () => { } it('success', () => { - const appApi = claimApiMaker(options) - expect(appApi).toBeDefined() + const manager = claimManagerMaker(options) + expect(manager).toBeDefined() - const appApiWithFetcher = claimApiMaker(options, {request: fetchData}) - expect(appApiWithFetcher).toBeDefined() + const managerWithFetcher = claimManagerMaker(options, {request: fetchData}) + expect(managerWithFetcher).toBeDefined() }) }) diff --git a/apps/wallet-mobile/src/features/Claim/module/api.ts b/packages/claim/src/manager.ts similarity index 61% rename from apps/wallet-mobile/src/features/Claim/module/api.ts rename to packages/claim/src/manager.ts index 5eb2900f50..3207f1875a 100644 --- a/apps/wallet-mobile/src/features/Claim/module/api.ts +++ b/packages/claim/src/manager.ts @@ -1,22 +1,23 @@ import {FetchData, fetchData, isLeft} from '@yoroi/common' -import {Api, Portfolio} from '@yoroi/types' +import {Api, Claim, Portfolio, Scan} from '@yoroi/types' -import {ScanActionClaim} from '../../Scan/common/types' import {asClaimApiError, asClaimToken} from './transformers' -import {ClaimApi, ClaimApiClaimTokensResponse} from './types' import {ClaimTokensApiResponseSchema} from './validators' -type ClaimApiMakerOptions = Readonly<{ +type ClaimManagerMakerOptions = Readonly<{ address: string primaryTokenInfo: Portfolio.Token.Info tokenManager: Portfolio.Manager.Token }> -export const claimApiMaker = ( - {address, primaryTokenInfo, tokenManager}: ClaimApiMakerOptions, +export const claimManagerMaker = ( + {address, primaryTokenInfo, tokenManager}: ClaimManagerMakerOptions, deps: Readonly<{request: FetchData}> = {request: fetchData} as const, -): Readonly<ClaimApi> => { - const claimTokens = postClaimTokens({address, primaryTokenInfo, tokenManager}, deps) +): Readonly<Claim.Manager> => { + const claimTokens = postClaimTokens( + {address, primaryTokenInfo, tokenManager}, + deps, + ) return { claimTokens, @@ -26,14 +27,17 @@ export const claimApiMaker = ( } const postClaimTokens = - ({address, primaryTokenInfo, tokenManager}: ClaimApiMakerOptions, {request} = {request: fetchData}) => - async (claimAction: ScanActionClaim) => { + ( + {address, primaryTokenInfo, tokenManager}: ClaimManagerMakerOptions, + {request} = {request: fetchData}, + ) => + async (claimAction: Scan.ActionClaim) => { // builds the request from the action, overides address and code const {code, params, url} = claimAction const payload = {...params, address, code} try { - const response = await request<ClaimApiClaimTokensResponse>({ + const response = await request<Claim.Api.ClaimTokensResponse>({ url, method: 'post', data: payload, @@ -43,7 +47,8 @@ const postClaimTokens = return asClaimApiError(response.error) } else { const claimInfo = response.value.data - if (!ClaimTokensApiResponseSchema.safeParse(claimInfo).success) throw new Api.Errors.ResponseMalformed() + if (!ClaimTokensApiResponseSchema.safeParse(claimInfo).success) + throw new Api.Errors.ResponseMalformed() return asClaimToken(claimInfo, primaryTokenInfo, tokenManager) } diff --git a/apps/wallet-mobile/src/features/Claim/module/transformers.ts b/packages/claim/src/transformers.ts similarity index 69% rename from apps/wallet-mobile/src/features/Claim/module/transformers.ts rename to packages/claim/src/transformers.ts index 8db9f3eae1..081c6d1ff8 100644 --- a/apps/wallet-mobile/src/features/Claim/module/transformers.ts +++ b/packages/claim/src/transformers.ts @@ -1,19 +1,20 @@ import {getApiError, toBigInt} from '@yoroi/common' import {isPrimaryToken, isTokenId} from '@yoroi/portfolio' -import {Api, Portfolio} from '@yoroi/types' +import {Api, Claim, Portfolio} from '@yoroi/types' import {claimApiErrors} from './errors' -import {ClaimApiClaimTokensResponse, ClaimInfo} from './types' // if the error is a known claim api error, throw it with a more specific error message otherwise throw the api error export const asClaimApiError = (error: Api.ResponseError) => { - const ClaimApiError = claimApiErrors.find(({statusCode}) => statusCode === error.status) + const ClaimApiError = claimApiErrors.find( + ({statusCode}) => statusCode === error.status, + ) if (ClaimApiError) throw new ClaimApiError() throw getApiError(error) } export const asClaimToken = async ( - claimItemResponse: ClaimApiClaimTokensResponse, + claimItemResponse: Claim.Api.ClaimTokensResponse, primaryTokenInfo: Portfolio.Token.Info, tokenManager: Portfolio.Manager.Token, ) => { @@ -25,14 +26,20 @@ export const asClaimToken = async ( .filter(isTokenId) .filter((id) => !isPrimaryToken(id)), ) - const infos = await tokenManager.sync({secondaryTokenIds: Array.from(ids), sourceId: 'claim-module'}) + const infos = await tokenManager.sync({ + secondaryTokenIds: Array.from(ids), + sourceId: 'claim-module', + }) const amounts: Array<Portfolio.Token.Amount> = [] + for (const [tokenId, cachedInfo] of infos.entries()) { if (!cachedInfo?.record || !ids.has(tokenId)) continue - amounts.push({ - info: cachedInfo.record, - quantity: toBigInt(tokens[tokenId], 0, true), - }) + const quantity = tokens[tokenId] + if (quantity) + amounts.push({ + info: cachedInfo.record, + quantity: toBigInt(quantity, 0, true), + }) } if (ptQuantity > 0n) { amounts.push({ @@ -42,20 +49,20 @@ export const asClaimToken = async ( } if (status === 'claimed') { - const claimed: Readonly<ClaimInfo> = { + const claimed: Readonly<Claim.Info> = { status: 'done', amounts, txHash: claimItemResponse.tx_hash, } return claimed } else if (status === 'queued') { - const queued: Readonly<ClaimInfo> = { + const queued: Readonly<Claim.Info> = { status: 'processing', amounts, } return queued } else { - const accepted: Readonly<ClaimInfo> = { + const accepted: Readonly<Claim.Info> = { status: 'accepted', amounts, } diff --git a/packages/claim/src/translators/reactjs/hooks/useClaimTokens.test.tsx b/packages/claim/src/translators/reactjs/hooks/useClaimTokens.test.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/wallet-mobile/src/features/Claim/module/useClaimTokens.tsx b/packages/claim/src/translators/reactjs/hooks/useClaimTokens.tsx similarity index 51% rename from apps/wallet-mobile/src/features/Claim/module/useClaimTokens.tsx rename to packages/claim/src/translators/reactjs/hooks/useClaimTokens.tsx index 7e008ab9b1..ffab03accb 100644 --- a/apps/wallet-mobile/src/features/Claim/module/useClaimTokens.tsx +++ b/packages/claim/src/translators/reactjs/hooks/useClaimTokens.tsx @@ -1,14 +1,19 @@ +import {Claim, Scan} from '@yoroi/types' import {useMutationWithInvalidations} from '@yoroi/common' import {UseMutationOptions} from 'react-query' -import {ScanActionClaim} from '../../Scan/common/types' -import {useClaim} from './ClaimProvider' -import {ClaimInfo} from './types' +import {useClaim} from '../provider/ClaimProvider' -export const useClaimTokens = (options: UseMutationOptions<ClaimInfo, Error, ScanActionClaim> = {}) => { +export const useClaimTokens = ( + options: UseMutationOptions<Claim.Info, Error, Scan.ActionClaim> = {}, +) => { const {claimTokens, address} = useClaim() - const mutation = useMutationWithInvalidations<ClaimInfo, Error, ScanActionClaim>({ + const mutation = useMutationWithInvalidations< + Claim.Info, + Error, + Scan.ActionClaim + >({ ...options, mutationFn: claimTokens, invalidateQueries: [['useClaimTokens', address]], diff --git a/packages/claim/src/translators/reactjs/provider/ClaimProvider.test.tsx b/packages/claim/src/translators/reactjs/provider/ClaimProvider.test.tsx new file mode 100644 index 0000000000..234f24b067 --- /dev/null +++ b/packages/claim/src/translators/reactjs/provider/ClaimProvider.test.tsx @@ -0,0 +1,69 @@ +import {act, renderHook} from '@testing-library/react-hooks' +import {queryClientFixture} from '@yoroi/common' +import {QueryClient} from 'react-query' + +import {useClaim} from './ClaimProvider' +import {wrapperMaker} from '../../../fixtures/wrapperMaker' +import {claimManagerMockInstances} from '../../../manager.mocks' +import {defaultClaimState} from '../state/state' + +describe('ClaimProvider', () => { + let queryClient: QueryClient + + beforeEach(() => { + jest.clearAllMocks() + queryClient = queryClientFixture() + }) + + afterEach(() => { + queryClient.clear() + }) + + test('state changes', () => { + const {result} = renderHook(() => useClaim(), { + wrapper: wrapperMaker({ + claimManager: claimManagerMockInstances.processing, + queryClient, + }), + }) + + act(() => { + result.current.scanActionClaimChanged({ + action: 'claim', + code: 'code', + params: {}, + url: 'https://example.com', + }) + }) + + expect(result.current.scanActionClaim).toEqual({ + action: 'claim', + code: 'code', + params: {}, + url: 'https://example.com', + }) + + act(() => { + result.current.claimInfoChanged({ + txHash: 'txHash', + status: 'processing', + amounts: [], + }) + }) + + expect(result.current.claimInfo).toEqual({ + txHash: 'txHash', + status: 'processing', + amounts: [], + }) + + act(() => { + result.current.reset() + }) + + expect(result.current.scanActionClaim).toEqual( + defaultClaimState.scanActionClaim, + ) + expect(result.current.claimInfo).toEqual(defaultClaimState.claimInfo) + }) +}) diff --git a/packages/claim/src/translators/reactjs/provider/ClaimProvider.tsx b/packages/claim/src/translators/reactjs/provider/ClaimProvider.tsx new file mode 100644 index 0000000000..b4abf9c6fc --- /dev/null +++ b/packages/claim/src/translators/reactjs/provider/ClaimProvider.tsx @@ -0,0 +1,68 @@ +import {invalid} from '@yoroi/common' +import {Claim, Scan} from '@yoroi/types' +import * as React from 'react' + +import {claimManagerMockInstances} from '../../../manager.mocks' +import { + ClaimActionType, + ClaimActions, + ClaimState, + claimReducer, + defaultClaimActions, + defaultClaimState, +} from '../state/state' + +export type ClaimProviderContext = React.PropsWithChildren< + Claim.Manager & ClaimState & ClaimActions +> + +const initialClaimProvider: ClaimProviderContext = { + ...defaultClaimState, + ...defaultClaimActions, + ...claimManagerMockInstances.error, +} +const ClaimContext = + React.createContext<ClaimProviderContext>(initialClaimProvider) + +type ClaimProviderProps = React.PropsWithChildren<{ + manager: Claim.Manager + initialState?: ClaimState +}> +export const ClaimProvider = ({ + children, + manager, + initialState, +}: ClaimProviderProps) => { + const [state, dispatch] = React.useReducer(claimReducer, { + ...defaultClaimState, + ...initialState, + }) + + const actions = React.useRef<ClaimActions>({ + claimInfoChanged: (claimInfo: Claim.Info) => { + dispatch({type: ClaimActionType.ClaimInfoChanged, claimInfo}) + }, + scanActionClaimChanged: (scanActionClaim: Scan.ActionClaim) => { + dispatch({type: ClaimActionType.ScanActionClaimChanged, scanActionClaim}) + }, + reset: () => { + dispatch({type: ClaimActionType.Reset}) + }, + }).current + + const context = React.useMemo<ClaimProviderContext>( + () => ({ + ...state, + ...manager, + ...actions, + }), + [state, manager, actions], + ) + + return ( + <ClaimContext.Provider value={context}>{children}</ClaimContext.Provider> + ) +} +export const useClaim = () => + React.useContext(ClaimContext) ?? + invalid('useClaim: needs to be wrapped in a ClaimManagerProvider') diff --git a/apps/wallet-mobile/src/features/Claim/module/state.mocks.ts b/packages/claim/src/translators/reactjs/state/state.mocks.ts similarity index 67% rename from apps/wallet-mobile/src/features/Claim/module/state.mocks.ts rename to packages/claim/src/translators/reactjs/state/state.mocks.ts index 2fc0c69b2d..f72a089f54 100644 --- a/apps/wallet-mobile/src/features/Claim/module/state.mocks.ts +++ b/packages/claim/src/translators/reactjs/state/state.mocks.ts @@ -1,6 +1,5 @@ -import {claimApiMockResponses} from './api.mocks' -import {defaultClaimState} from './state' -import {ClaimState} from './types' +import {claimApiMockResponses} from '../../../manager.mocks' +import {ClaimState, defaultClaimState} from './state' const empty: ClaimState = {...defaultClaimState} as const const withScanActionClaim: ClaimState = { @@ -15,17 +14,17 @@ const withScanActionClaim: ClaimState = { const withClaimTokenAccepted: ClaimState = { ...withScanActionClaim, - claimInfo: claimApiMockResponses.claimTokens['accepted'], + claimInfo: claimApiMockResponses.claimTokens.accepted, } as const const withClaimTokenProcessing: ClaimState = { ...withScanActionClaim, - claimInfo: claimApiMockResponses.claimTokens['processing'], + claimInfo: claimApiMockResponses.claimTokens.processing, } as const const withClaimTokenDone: ClaimState = { ...withScanActionClaim, - claimInfo: claimApiMockResponses.claimTokens['done'], + claimInfo: claimApiMockResponses.claimTokens.done, } as const export const mocks = { diff --git a/apps/wallet-mobile/src/features/Claim/module/state.ts b/packages/claim/src/translators/reactjs/state/state.ts similarity index 73% rename from apps/wallet-mobile/src/features/Claim/module/state.ts rename to packages/claim/src/translators/reactjs/state/state.ts index 38fabf84e2..3e2306267e 100644 --- a/apps/wallet-mobile/src/features/Claim/module/state.ts +++ b/packages/claim/src/translators/reactjs/state/state.ts @@ -1,12 +1,10 @@ import {invalid} from '@yoroi/common' +import {Claim, Scan} from '@yoroi/types' import {castDraft, produce} from 'immer' -import {ScanActionClaim} from '../../Scan/common/types' -import {ClaimInfo, ClaimState} from './types' - export type ClaimActions = Readonly<{ - claimInfoChanged: (claimInfo: ClaimInfo) => void - scanActionClaimChanged: (scanActionClaim: ScanActionClaim) => void + claimInfoChanged: (claimInfo: Claim.Info) => void + scanActionClaimChanged: (scanActionClaim: Scan.ActionClaim) => void reset: () => void }> @@ -16,19 +14,6 @@ export enum ClaimActionType { Reset = 'reset', } -export type ClaimAction = - | { - type: ClaimActionType.ClaimInfoChanged - claimInfo: ClaimInfo - } - | { - type: ClaimActionType.ScanActionClaimChanged - scanActionClaim: ScanActionClaim - } - | { - type: ClaimActionType.Reset - } - export const defaultClaimState: ClaimState = { claimInfo: undefined, scanActionClaim: undefined, @@ -40,7 +25,10 @@ export const defaultClaimActions: ClaimActions = { reset: () => invalid('missing init'), } as const -export const claimReducer = (state: ClaimState, action: ClaimAction): ClaimState => { +export const claimReducer = ( + state: ClaimState, + action: ClaimAction, +): ClaimState => { return produce(state, (draft) => { switch (action.type) { case ClaimActionType.ClaimInfoChanged: @@ -56,3 +44,21 @@ export const claimReducer = (state: ClaimState, action: ClaimAction): ClaimState } }) } + +export type ClaimState = Readonly<{ + claimInfo: Claim.Info | undefined + scanActionClaim: Scan.ActionClaim | undefined +}> + +export type ClaimAction = + | { + type: ClaimActionType.ClaimInfoChanged + claimInfo: Claim.Info + } + | { + type: ClaimActionType.ScanActionClaimChanged + scanActionClaim: Scan.ActionClaim + } + | { + type: ClaimActionType.Reset + } diff --git a/apps/wallet-mobile/src/features/Claim/module/validators.ts b/packages/claim/src/validators.ts similarity index 100% rename from apps/wallet-mobile/src/features/Claim/module/validators.ts rename to packages/claim/src/validators.ts diff --git a/packages/claim/tsconfig.build.json b/packages/claim/tsconfig.build.json new file mode 100644 index 0000000000..1382480b0c --- /dev/null +++ b/packages/claim/tsconfig.build.json @@ -0,0 +1,5 @@ + +{ + "extends": "./tsconfig", + "exclude": ["example", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/claim/tsconfig.json b/packages/claim/tsconfig.json new file mode 100644 index 0000000000..a074f3ce47 --- /dev/null +++ b/packages/claim/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "declaration": true, + "baseUrl": "./src", + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "esnext" + } +} diff --git a/packages/types/src/claim/api.ts b/packages/types/src/claim/api.ts new file mode 100644 index 0000000000..6b377450da --- /dev/null +++ b/packages/types/src/claim/api.ts @@ -0,0 +1,28 @@ +export type ClaimApiClaimTokensRequestPayload = { + address: string + code: string + [key: string]: unknown +} + +export type ClaimApiClaimTokensResponse = { + lovelaces: string + tokens: { + [tokenId: string]: string + } +} & ( + | { + // code: 200 + status: 'accepted' + queue_position: number + } + | { + // code: 201 + status: 'queued' + queue_position: number + } + | { + // code: 202 + status: 'claimed' + tx_hash: string + } +) diff --git a/packages/types/src/claim/claim.ts b/packages/types/src/claim/claim.ts new file mode 100644 index 0000000000..8454adaae3 --- /dev/null +++ b/packages/types/src/claim/claim.ts @@ -0,0 +1,18 @@ +import {PortfolioTokenAmount} from '../portfolio/amount' +import {PortfolioTokenInfo} from '../portfolio/info' +import {ScanActionClaim} from '../scan/actions' + +export type ClaimStatus = 'accepted' | 'processing' | 'done' + +export type ClaimInfo = Readonly<{ + // api + status: ClaimStatus + amounts: ReadonlyArray<PortfolioTokenAmount> + txHash?: string +}> + +export type ClaimManager = Readonly<{ + claimTokens: (action: ScanActionClaim) => Promise<ClaimInfo> + address: string + primaryTokenInfo: PortfolioTokenInfo +}> diff --git a/apps/wallet-mobile/src/features/Claim/module/errors.ts b/packages/types/src/claim/errors.ts similarity index 64% rename from apps/wallet-mobile/src/features/Claim/module/errors.ts rename to packages/types/src/claim/errors.ts index 1b1fe489fb..9410a73689 100644 --- a/apps/wallet-mobile/src/features/Claim/module/errors.ts +++ b/packages/types/src/claim/errors.ts @@ -16,13 +16,3 @@ export class ClaimApiErrorsTooEarly extends Error { export class ClaimApiErrorsRateLimited extends Error { static readonly statusCode = 429 } - -// API errors that when returned from a faucet have a more specific error message -export const claimApiErrors = [ - ClaimApiErrorsInvalidRequest, - ClaimApiErrorsNotFound, - ClaimApiErrorsAlreadyClaimed, - ClaimApiErrorsExpired, - ClaimApiErrorsTooEarly, - ClaimApiErrorsRateLimited, -] as const diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 825709d863..00994a6b61 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -231,6 +231,27 @@ import { AppLoggerEntry, AppLoggerManager, } from './app/logger' +import {ScanErrorUnknown, ScanErrorUnknownContent} from './scan/errors' +import { + ScanAction, + ScanActionClaim, + ScanActionSendOnlyReceiver, + ScanActionSendSinglePt, + ScanFeature, +} from './scan/actions' +import {ClaimInfo, ClaimManager, ClaimStatus} from './claim/claim' +import { + ClaimApiErrorsAlreadyClaimed, + ClaimApiErrorsExpired, + ClaimApiErrorsInvalidRequest, + ClaimApiErrorsNotFound, + ClaimApiErrorsRateLimited, + ClaimApiErrorsTooEarly, +} from './claim/errors' +import { + ClaimApiClaimTokensRequestPayload, + ClaimApiClaimTokensResponse, +} from './claim/api' export namespace App { export namespace Errors { @@ -609,6 +630,40 @@ export namespace Network { export type EpochProgress = NetworkEpochProgress } +export namespace Scan { + export namespace Errors { + export class UnknownContent extends ScanErrorUnknownContent {} + export class Unknown extends ScanErrorUnknown {} + } + + export type Feature = ScanFeature + export type Action = ScanAction + export type ActionClaim = ScanActionClaim + export type ActionSendOnlyReceiver = ScanActionSendOnlyReceiver + export type ActionSendSinglePt = ScanActionSendSinglePt +} + +export namespace Claim { + // eslint-disable-next-line @typescript-eslint/no-shadow + export namespace Api { + export namespace Errors { + export class AlreadyClaimed extends ClaimApiErrorsAlreadyClaimed {} + export class Expired extends ClaimApiErrorsExpired {} + export class InvalidRequest extends ClaimApiErrorsInvalidRequest {} + export class NotFound extends ClaimApiErrorsNotFound {} + export class RateLimited extends ClaimApiErrorsRateLimited {} + export class TooEarly extends ClaimApiErrorsTooEarly {} + } + + export type ClaimTokensRequestPayload = ClaimApiClaimTokensRequestPayload + export type ClaimTokensResponse = ClaimApiClaimTokensResponse + } + + export type Status = ClaimStatus + export type Info = ClaimInfo + export type Manager = ClaimManager +} + export * from './helpers/types' export * from './helpers/storage' export * from './api/cardano' diff --git a/apps/wallet-mobile/src/features/Scan/common/types.ts b/packages/types/src/scan/actions.ts similarity index 62% rename from apps/wallet-mobile/src/features/Scan/common/types.ts rename to packages/types/src/scan/actions.ts index 907f245da0..a05a66da28 100644 --- a/apps/wallet-mobile/src/features/Scan/common/types.ts +++ b/packages/types/src/scan/actions.ts @@ -1,11 +1,8 @@ -// TODO: migrate to yoroi types after fully implemented -export class ScanErrorUnknownContent extends Error {} -export class ScanErrorUnknown extends Error {} - export type ScanActionSendOnlyReceiver = Readonly<{ action: 'send-only-receiver' receiver: string }> + export type ScanActionSendSinglePt = Readonly<{ action: 'send-single-pt' receiver: string @@ -17,13 +14,17 @@ export type ScanActionSendSinglePt = Readonly<{ } | undefined }> + export type ScanActionClaim = Readonly<{ action: 'claim' url: string code: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record<string, any> | undefined }> -export type ScanAction = ScanActionSendOnlyReceiver | ScanActionSendSinglePt | ScanActionClaim + +export type ScanAction = + | ScanActionSendOnlyReceiver + | ScanActionSendSinglePt + | ScanActionClaim export type ScanFeature = 'send' | 'scan' diff --git a/packages/types/src/scan/errors.ts b/packages/types/src/scan/errors.ts new file mode 100644 index 0000000000..f0b688d1b7 --- /dev/null +++ b/packages/types/src/scan/errors.ts @@ -0,0 +1,2 @@ +export class ScanErrorUnknownContent extends Error {} +export class ScanErrorUnknown extends Error {} diff --git a/scripts/install-dev-pkg.sh b/scripts/install-dev-pkg.sh index cab122b04f..3df213199f 100644 --- a/scripts/install-dev-pkg.sh +++ b/scripts/install-dev-pkg.sh @@ -16,6 +16,7 @@ yarn workspace @yoroi/api add -D "$1" yarn workspace @yoroi/exchange add -D "$1" yarn workspace @yoroi/explorers add -D "$1" yarn workspace @yoroi/common add -D "$1" +yarn workspace @yoroi/claim add -D "$1" yarn workspace @yoroi/links add -D "$1" yarn workspace @yoroi/resolver add -D "$1" yarn workspace @yoroi/staking add -D "$1" diff --git a/scripts/install-pkgs.sh b/scripts/install-pkgs.sh index feae674b51..5736d01015 100644 --- a/scripts/install-pkgs.sh +++ b/scripts/install-pkgs.sh @@ -65,5 +65,6 @@ yarn workspace @yoroi/wallet-mobile add @yoroi/theme@"$1" yarn workspace @yoroi/wallet-mobile add @yoroi/dapp-connector@"$1" yarn workspace @yoroi/wallet-mobile add @yoroi/identicon@"$1" yarn workspace @yoroi/wallet-mobile add @yoroi/notifications@"$1" +yarn workspace @yoroi/wallet-mobile add @yoroi/claim@"$1" echo "Using new packages..." diff --git a/yoroi.code-workspace b/yoroi.code-workspace index cfa392bdc4..5ee69dcce1 100644 --- a/yoroi.code-workspace +++ b/yoroi.code-workspace @@ -68,14 +68,18 @@ "name": "@yoroi/wallet-mobile", "path": "./apps/wallet-mobile" }, + { + "name": "@yoroi/notifications", + "path": "./packages/notifications" + }, + { + "name": "@yoroi/claim", + "path": "./packages/claim" + }, { "name": "@yoroi/e2e-wallet-mobile", "path": "./e2e/wallet-mobile" }, - { - "name": "@yoroi/notifications", - "path": "./packages/notifications" - } ], "settings": { "jest.disabledWorkspaceFolders": [ From 43fc82d858e3bfa2ee900850b2acc3d15493f300 Mon Sep 17 00:00:00 2001 From: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:21:35 +0100 Subject: [PATCH 3/6] feature(claim): added claim package --- packages/claim/README.md | 82 ++++++++ packages/claim/package.json | 2 +- packages/claim/src/api-faucet.mocks.ts | 7 +- packages/claim/src/index.ts | 1 + packages/claim/src/manager.mocks.ts | 2 +- packages/claim/src/manager.test.ts | 120 +++++++++++- packages/claim/src/manager.ts | 39 ++-- packages/claim/src/transformers.test.ts | 184 ++++++++++++++++++ packages/claim/src/transformers.ts | 13 +- .../translators/reactjs/hooks/useClaim.tsx | 5 + .../reactjs/hooks/useClaimTokens.test.tsx | 57 ++++++ .../reactjs/hooks/useClaimTokens.tsx | 2 +- .../reactjs/provider/ClaimProvider.test.tsx | 2 +- .../reactjs/provider/ClaimProvider.tsx | 6 +- .../translators/reactjs/state/state.mocks.ts | 2 +- .../translators/reactjs/state/state.test.ts | 73 +++++++ .../src/translators/reactjs/state/state.ts | 41 ++-- 17 files changed, 576 insertions(+), 62 deletions(-) create mode 100644 packages/claim/README.md create mode 100644 packages/claim/src/transformers.test.ts create mode 100644 packages/claim/src/translators/reactjs/hooks/useClaim.tsx create mode 100644 packages/claim/src/translators/reactjs/state/state.test.ts diff --git a/packages/claim/README.md b/packages/claim/README.md new file mode 100644 index 0000000000..e3fec99556 --- /dev/null +++ b/packages/claim/README.md @@ -0,0 +1,82 @@ +# @yoroi/claim + +## Overview + +The `@yoroi/claim` is a utility package designed to handle token claims on the Cardano blockchain, following the CIP-99 (Cardano Improvement Proposal 99) standard for Proof of Ownership (POO) in decentralized token claiming. It provides an API for managing claim requests and responses, including error handling, token synchronization, and status management for claims. + +The package works seamlessly with the Yoroi ecosystem and interacts with the Cardano blockchain to allow users to claim various tokens (both native and NFT) and manage their status in real-time. + +## Features + +Claim Token Handling: Process token claims for native assets, NFTs, and Lovelace based on claim responses. +Status Management: Handles multiple claim statuses such as `accepted`, `queued`, and `claimed`. +Error Handling: Provides detailed error messages and error handling based on claim API responses. +Token Synchronization: Synchronizes tokens with external sources to ensure the correct token information is used while displaying claims. +Support for [CIP-99](https://cips.cardano.org/cip/CIP-0099): Follows the Cardano Improvement Proposal 99 standard for Proof of Ownership (POO), enabling secure and decentralized token claims. + +## Installation + +To install the package, you can use npm or yarn: + +```bash +npm install @yoroi/claim +``` + +or + +```bash +yarn add @yoroi/claim +``` + +## Peer Dependencies + +This package works alongside other `@yoroi` modules, such as `@yoroi/portfolio` and `@yoroi/types`. Ensure these dependencies are installed in your project: + +```bash +npm install @yoroi/{portfolio,types} +``` + +## Usage + +Creating a Claim Manager + +The main utility of this package is the `claimManagerMaker` function. This function sets up a claim manager that can be used to request tokens to be claimed and sent to a specified Cardano address. + +The `claimManagerMaker` requires the following inputs: + +- `address`: The Cardano address where the claimed tokens should be sent. +- `primaryTokenInfo`: Information about the primary token (e.g., Lovelace) being claimed. +- `tokenManager`: A token manager responsible for handling the synchronization and management of token data, please check on `@yoroi/portfolio` +- `deps`: Optional dependency injection, providing a request function for interacting with the API. + +```typescript +import {claimManagerMaker} from '@yoroi/claim' +import {tokenMocks} from '@yoroi/portfolio' +import {Portfolio} from '@yoroi/types' +import {fetchData} from '@yoroi/common' + +import {tokenManager, primaryTokenInfo} from '../your-code' + +const claimManagerOptions = { + address: 'addr1q...xyz', // Address where the tokens should be sent + primaryTokenInfo, + tokenManager, +} + +// Create a claim manager +const claimManager = claimManagerMaker(claimManagerOptions) + +// Now you can use `claimManager.claimTokens()` to process a claim based on a Scan.ActionClain +const claimAction: Scan.ActionClaim = { + action: 'claim', + code: 'claim_code', + params: {someParam: 'value'}, + url: 'https://api.example.com/claim', +} + +claimManager.claimTokens(claimAction) +``` + +## Warning + +This package will be interacting with an external API during the token claim process. Specifically, a scan action will trigger it and by providing the URL that will be hit during claim process. Be cautious when using this feature in production environments. Ensure that you are aware of the API's reliability, security, and any associated rate limits or costs. diff --git a/packages/claim/package.json b/packages/claim/package.json index 80100196fe..6fbaf3ae02 100644 --- a/packages/claim/package.json +++ b/packages/claim/package.json @@ -1,6 +1,6 @@ { "name": "@yoroi/claim", - "version": "2.0.1", + "version": "1.0.0", "description": "The Claim (proof-of-onboarding) package of Yoroi SDK", "keywords": [ "yoroi", diff --git a/packages/claim/src/api-faucet.mocks.ts b/packages/claim/src/api-faucet.mocks.ts index 5dcb7a5222..f7dff4d5f6 100644 --- a/packages/claim/src/api-faucet.mocks.ts +++ b/packages/claim/src/api-faucet.mocks.ts @@ -1,10 +1,6 @@ import {tokenMocks} from '@yoroi/portfolio' -import {Claim} from '@yoroi/types' -const claimTokens: Record< - string, - {[key: string]: Claim.Api.ClaimTokensResponse} -> = { +const claimTokens = { success: { accepted: { status: 'accepted', @@ -30,7 +26,6 @@ const claimTokens: Record< [tokenMocks.nftCryptoKitty.info.id]: '44', [tokenMocks.rnftWhatever.info.id]: '410', }, - status: 'claimed', tx_hash: 'tx_hash', }, diff --git a/packages/claim/src/index.ts b/packages/claim/src/index.ts index 6462533927..79c970d0ff 100644 --- a/packages/claim/src/index.ts +++ b/packages/claim/src/index.ts @@ -3,6 +3,7 @@ export * from './errors' export * from './manager' export * from './manager.mocks' +export * from './translators/reactjs/hooks/useClaim' export * from './translators/reactjs/hooks/useClaimTokens' export * from './translators/reactjs/provider/ClaimProvider' diff --git a/packages/claim/src/manager.mocks.ts b/packages/claim/src/manager.mocks.ts index a2810e9183..b452b808c0 100644 --- a/packages/claim/src/manager.mocks.ts +++ b/packages/claim/src/manager.mocks.ts @@ -1,7 +1,7 @@ import {tokenMocks} from '@yoroi/portfolio' import {Claim} from '@yoroi/types' -const claimTokensResponse: {[key: string]: Claim.Info} = { +const claimTokensResponse = { accepted: { status: 'accepted', amounts: [ diff --git a/packages/claim/src/manager.test.ts b/packages/claim/src/manager.test.ts index 5146cb447d..19b57cd9c0 100644 --- a/packages/claim/src/manager.test.ts +++ b/packages/claim/src/manager.test.ts @@ -1,8 +1,10 @@ import {fetchData} from '@yoroi/common' -import {tokenInfoMocks} from '@yoroi/portfolio' -import {Portfolio} from '@yoroi/types' +import {tokenInfoMocks, tokenMocks} from '@yoroi/portfolio' +import {Api, Portfolio, Scan} from '@yoroi/types' import {claimManagerMaker} from './manager' +import {claimFaucetResponses} from './api-faucet.mocks' +import {claimApiMockResponses} from './manager.mocks' describe('claimManagerMaker', () => { const options = { @@ -19,3 +21,117 @@ describe('claimManagerMaker', () => { expect(managerWithFetcher).toBeDefined() }) }) + +describe('claimManagerMaker - postClaimTokens', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + const tokenManagerMock = { + sync: jest.fn(), + api: { + tokenActivity: jest.fn(), + tokenDiscovery: jest.fn(), + tokenImageInvalidate: jest.fn(), + tokenInfo: jest.fn(), + tokenInfos: jest.fn(), + tokenTraits: jest.fn(), + }, + clear: jest.fn(), + destroy: jest.fn(), + hydrate: jest.fn(), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + observable$: {} as any, + } + + tokenManagerMock.sync.mockResolvedValue( + new Map([ + [ + tokenMocks.nftCryptoKitty.info.id, + {record: tokenMocks.nftCryptoKitty.info}, + ], + [tokenMocks.rnftWhatever.info.id, {record: tokenMocks.rnftWhatever.info}], + ]), + ) + + const options = { + address: 'addr_test', + primaryTokenInfo: tokenInfoMocks.primaryETH, + tokenManager: tokenManagerMock, + } + + const claimAction: Scan.ActionClaim = { + action: 'claim', + code: 'claim_code', + params: {someParam: 'value'}, + url: 'https://api.example.com/claim', + } + + it('should handle successful claim', async () => { + const mockResponse = { + tag: 'right', + value: { + status: Api.HttpStatusCode.Ok, + data: claimFaucetResponses.claimTokens.success.accepted, + }, + } + + const request = jest.fn().mockResolvedValue(mockResponse) + + const manager = claimManagerMaker(options, {request}) + const result = await manager.claimTokens(claimAction) + + expect(result).toEqual(claimApiMockResponses.claimTokens.accepted) + }) + + it('should throw API when not specific to claim', async () => { + const errorApiResponse: Api.ResponseError = { + status: Api.HttpStatusCode.Unauthorized, + message: 'Unauthorized', + responseData: null, + } + const mockErrorResponse = { + tag: 'left', + error: errorApiResponse, + } + + const request = jest.fn().mockResolvedValue(mockErrorResponse) + + const manager = claimManagerMaker(options, {request}) + + await expect(() => manager.claimTokens(claimAction)).rejects.toThrow( + Api.Errors.Unauthorized, + ) + }) + + it('should handle malformed API response', async () => { + const malformedResponse = { + tag: 'right', + value: { + status: Api.HttpStatusCode.Ok, + data: {something: 'else'}, + }, + } + + const request = jest.fn().mockResolvedValue(malformedResponse) + + const manager = claimManagerMaker(options, {request}) + + await expect(manager.claimTokens(claimAction)).rejects.toThrow( + Api.Errors.ResponseMalformed, + ) + }) + + it('should handle unknown errors', async () => { + const request = async () => { + throw new Api.Errors.Forbidden() + } + + const manager = claimManagerMaker(options, {request}) + + await expect(manager.claimTokens(claimAction)).rejects.toThrow( + Api.Errors.Forbidden, + ) + }) +}) diff --git a/packages/claim/src/manager.ts b/packages/claim/src/manager.ts index 3207f1875a..188ca975fe 100644 --- a/packages/claim/src/manager.ts +++ b/packages/claim/src/manager.ts @@ -1,5 +1,6 @@ import {FetchData, fetchData, isLeft} from '@yoroi/common' import {Api, Claim, Portfolio, Scan} from '@yoroi/types' +import {freeze} from 'immer' import {asClaimApiError, asClaimToken} from './transformers' import {ClaimTokensApiResponseSchema} from './validators' @@ -19,40 +20,36 @@ export const claimManagerMaker = ( deps, ) - return { + return freeze({ claimTokens, address, primaryTokenInfo, - } as const + }) } const postClaimTokens = ( {address, primaryTokenInfo, tokenManager}: ClaimManagerMakerOptions, - {request} = {request: fetchData}, + {request}: {request: FetchData}, ) => async (claimAction: Scan.ActionClaim) => { // builds the request from the action, overides address and code const {code, params, url} = claimAction const payload = {...params, address, code} - try { - const response = await request<Claim.Api.ClaimTokensResponse>({ - url, - method: 'post', - data: payload, - }) - - if (isLeft(response)) { - return asClaimApiError(response.error) - } else { - const claimInfo = response.value.data - if (!ClaimTokensApiResponseSchema.safeParse(claimInfo).success) - throw new Api.Errors.ResponseMalformed() - - return asClaimToken(claimInfo, primaryTokenInfo, tokenManager) - } - } catch (error) { - throw new Api.Errors.Unknown((error as Error)?.message) + const response = await request<Claim.Api.ClaimTokensResponse>({ + url, + method: 'post', + data: payload, + }) + + if (isLeft(response)) { + return asClaimApiError(response.error) + } else { + const claimInfo = response.value.data + if (!ClaimTokensApiResponseSchema.safeParse(claimInfo).success) + throw new Api.Errors.ResponseMalformed() + + return asClaimToken(claimInfo, primaryTokenInfo, tokenManager) } } diff --git a/packages/claim/src/transformers.test.ts b/packages/claim/src/transformers.test.ts new file mode 100644 index 0000000000..2c81677982 --- /dev/null +++ b/packages/claim/src/transformers.test.ts @@ -0,0 +1,184 @@ +import {Api, Claim, Portfolio} from '@yoroi/types' +import {tokenMocks} from '@yoroi/portfolio' + +import {asClaimApiError, asClaimToken} from './transformers' +import {claimFaucetResponses} from './api-faucet.mocks' +import {claimApiMockResponses} from './manager.mocks' + +const tokenManagerMock = { + sync: jest.fn(), + api: { + tokenActivity: jest.fn(), + tokenDiscovery: jest.fn(), + tokenImageInvalidate: jest.fn(), + tokenInfo: jest.fn(), + tokenInfos: jest.fn(), + tokenTraits: jest.fn(), + }, + clear: jest.fn(), + destroy: jest.fn(), + hydrate: jest.fn(), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + observable$: {} as any, +} + +describe('asClaimApiError', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + it('should throw specific claim API error when status matches', () => { + const error: Api.ResponseError = { + status: Api.HttpStatusCode.BadRequest, + message: 'Bad Request', + responseData: {}, + } + + expect(() => asClaimApiError(error)).toThrow( + Claim.Api.Errors.InvalidRequest, + ) + }) + + it('should throw generic API error when status does not match', () => { + const error: Api.ResponseError = { + status: Api.HttpStatusCode.Forbidden, + message: 'Forbidden', + responseData: {}, + } + + expect(() => asClaimApiError(error)).toThrow(Api.Errors.Forbidden) + }) +}) + +describe('asClaimToken', () => { + const primaryTokenInfo: Portfolio.Token.Info = tokenMocks.primaryETH.info + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should return a "claimed" status with correct token amounts', async () => { + const claimResponse = claimFaucetResponses.claimTokens.success.claimed + + tokenManagerMock.sync.mockResolvedValue( + new Map([ + [ + tokenMocks.nftCryptoKitty.info.id, + {record: tokenMocks.nftCryptoKitty.info}, + ], + [ + tokenMocks.rnftWhatever.info.id, + {record: tokenMocks.rnftWhatever.info}, + ], + ]), + ) + + const result = await asClaimToken( + claimResponse, + primaryTokenInfo, + tokenManagerMock, + ) + + expect(result).toEqual<Claim.Info>({ + status: 'done', + amounts: [ + {info: primaryTokenInfo, quantity: BigInt(2000000)}, + {info: tokenMocks.nftCryptoKitty.info, quantity: BigInt(44)}, + {info: tokenMocks.rnftWhatever.info, quantity: BigInt(410)}, + ], + txHash: 'tx_hash', + }) + }) + + it('should return a "queued" status with correct token amounts', async () => { + const claimResponse = claimFaucetResponses.claimTokens.success.queued + + tokenManagerMock.sync.mockResolvedValue( + new Map([ + [ + tokenMocks.nftCryptoKitty.info.id, + {record: tokenMocks.nftCryptoKitty.info}, + ], + [ + tokenMocks.rnftWhatever.info.id, + {record: tokenMocks.rnftWhatever.info}, + ], + ]), + ) + + const result = await asClaimToken( + claimResponse, + primaryTokenInfo, + tokenManagerMock, + ) + + expect(result).toEqual(claimApiMockResponses.claimTokens.processing) + }) + + it('should return an "accepted" status with correct token amounts', async () => { + const claimResponse = claimFaucetResponses.claimTokens.success.accepted + + tokenManagerMock.sync.mockResolvedValue( + new Map([ + [ + tokenMocks.nftCryptoKitty.info.id, + {record: tokenMocks.nftCryptoKitty.info}, + ], + [ + tokenMocks.rnftWhatever.info.id, + {record: tokenMocks.rnftWhatever.info}, + ], + ]), + ) + + const result = await asClaimToken( + claimResponse, + primaryTokenInfo, + tokenManagerMock, + ) + + expect(result).toEqual(claimApiMockResponses.claimTokens.accepted) + }) + + it('should filter out invalid tokens or not requested tokens', async () => { + const claimResponse = { + ...claimFaucetResponses.claimTokens.success.accepted, + lovelaces: null, + tokens: { + 'invalid.id': null, + 'dead.': null, + ...claimFaucetResponses.claimTokens.success.accepted.tokens, + }, + } + + tokenManagerMock.sync.mockResolvedValue( + new Map([ + [ + tokenMocks.nftCryptoKitty.info.id, + {record: tokenMocks.nftCryptoKitty.info}, + ], + [ + tokenMocks.rnftWhatever.info.id, + {record: tokenMocks.rnftWhatever.info}, + ], + ['invalid.', undefined], + ['dead.', {record: tokenMocks.rnftWhatever.info}], + ]), + ) + + const result = await asClaimToken( + claimResponse as any, + primaryTokenInfo, + tokenManagerMock, + ) + + expect(result).toEqual({ + status: 'accepted', + amounts: [ + {info: tokenMocks.nftCryptoKitty.info, quantity: BigInt(44)}, + {info: tokenMocks.rnftWhatever.info, quantity: BigInt(410)}, + ], + }) + }) +}) diff --git a/packages/claim/src/transformers.ts b/packages/claim/src/transformers.ts index 081c6d1ff8..1b3c9f8e3f 100644 --- a/packages/claim/src/transformers.ts +++ b/packages/claim/src/transformers.ts @@ -32,6 +32,13 @@ export const asClaimToken = async ( }) const amounts: Array<Portfolio.Token.Amount> = [] + if (ptQuantity > 0n) { + amounts.push({ + info: primaryTokenInfo, + quantity: ptQuantity, + }) + } + for (const [tokenId, cachedInfo] of infos.entries()) { if (!cachedInfo?.record || !ids.has(tokenId)) continue const quantity = tokens[tokenId] @@ -41,12 +48,6 @@ export const asClaimToken = async ( quantity: toBigInt(quantity, 0, true), }) } - if (ptQuantity > 0n) { - amounts.push({ - info: primaryTokenInfo, - quantity: ptQuantity, - }) - } if (status === 'claimed') { const claimed: Readonly<Claim.Info> = { diff --git a/packages/claim/src/translators/reactjs/hooks/useClaim.tsx b/packages/claim/src/translators/reactjs/hooks/useClaim.tsx new file mode 100644 index 0000000000..cf3032fdae --- /dev/null +++ b/packages/claim/src/translators/reactjs/hooks/useClaim.tsx @@ -0,0 +1,5 @@ +import * as React from 'react' + +import {ClaimContext} from '../provider/ClaimProvider' + +export const useClaim = () => React.useContext(ClaimContext) diff --git a/packages/claim/src/translators/reactjs/hooks/useClaimTokens.test.tsx b/packages/claim/src/translators/reactjs/hooks/useClaimTokens.test.tsx index e69de29bb2..eaed00a782 100644 --- a/packages/claim/src/translators/reactjs/hooks/useClaimTokens.test.tsx +++ b/packages/claim/src/translators/reactjs/hooks/useClaimTokens.test.tsx @@ -0,0 +1,57 @@ +import {QueryClient} from 'react-query' +import {renderHook, act} from '@testing-library/react-hooks' +import {queryClientFixture} from '@yoroi/common' +import {Claim, Scan} from '@yoroi/types' + +import { + claimApiMockResponses, + claimManagerMockInstances, +} from '../../../manager.mocks' +import {wrapperMaker} from '../../../fixtures/wrapperMaker' +import {useClaimTokens} from './useClaimTokens' + +describe('useClaimTokens', () => { + let queryClient: QueryClient + + beforeEach(() => { + jest.clearAllMocks() + queryClient = queryClientFixture() + }) + + afterEach(() => { + queryClient.clear() + }) + + const scanClaimAction: Scan.ActionClaim = { + action: 'claim', + code: 'code', + params: {}, + url: 'url', + } + + it('success', async () => { + const claimManagerMock: Claim.Manager = { + ...claimManagerMockInstances.processing, + claimTokens: jest + .fn() + .mockResolvedValue(claimApiMockResponses.claimTokens.processing), + } + + const wrapper = wrapperMaker({ + claimManager: claimManagerMock, + queryClient, + }) + + const {result, waitFor: waitForHook} = renderHook(() => useClaimTokens(), { + wrapper, + }) + + await act(async () => result.current.claimTokens(scanClaimAction)) + + await waitForHook(() => expect(result.current.isLoading).toBe(false)) + + expect(claimManagerMock.claimTokens).toHaveBeenCalledTimes(1) + expect(claimManagerMock.claimTokens).toHaveBeenCalledWith(scanClaimAction) + expect(result.current.isError).toBe(false) + }) +}) diff --git a/packages/claim/src/translators/reactjs/hooks/useClaimTokens.tsx b/packages/claim/src/translators/reactjs/hooks/useClaimTokens.tsx index ffab03accb..04db97d9da 100644 --- a/packages/claim/src/translators/reactjs/hooks/useClaimTokens.tsx +++ b/packages/claim/src/translators/reactjs/hooks/useClaimTokens.tsx @@ -2,7 +2,7 @@ import {Claim, Scan} from '@yoroi/types' import {useMutationWithInvalidations} from '@yoroi/common' import {UseMutationOptions} from 'react-query' -import {useClaim} from '../provider/ClaimProvider' +import {useClaim} from './useClaim' export const useClaimTokens = ( options: UseMutationOptions<Claim.Info, Error, Scan.ActionClaim> = {}, diff --git a/packages/claim/src/translators/reactjs/provider/ClaimProvider.test.tsx b/packages/claim/src/translators/reactjs/provider/ClaimProvider.test.tsx index 234f24b067..5d8bf46fdc 100644 --- a/packages/claim/src/translators/reactjs/provider/ClaimProvider.test.tsx +++ b/packages/claim/src/translators/reactjs/provider/ClaimProvider.test.tsx @@ -2,10 +2,10 @@ import {act, renderHook} from '@testing-library/react-hooks' import {queryClientFixture} from '@yoroi/common' import {QueryClient} from 'react-query' -import {useClaim} from './ClaimProvider' import {wrapperMaker} from '../../../fixtures/wrapperMaker' import {claimManagerMockInstances} from '../../../manager.mocks' import {defaultClaimState} from '../state/state' +import {useClaim} from '../hooks/useClaim' describe('ClaimProvider', () => { let queryClient: QueryClient diff --git a/packages/claim/src/translators/reactjs/provider/ClaimProvider.tsx b/packages/claim/src/translators/reactjs/provider/ClaimProvider.tsx index b4abf9c6fc..a3922a038d 100644 --- a/packages/claim/src/translators/reactjs/provider/ClaimProvider.tsx +++ b/packages/claim/src/translators/reactjs/provider/ClaimProvider.tsx @@ -1,4 +1,3 @@ -import {invalid} from '@yoroi/common' import {Claim, Scan} from '@yoroi/types' import * as React from 'react' @@ -21,7 +20,7 @@ const initialClaimProvider: ClaimProviderContext = { ...defaultClaimActions, ...claimManagerMockInstances.error, } -const ClaimContext = +export const ClaimContext = React.createContext<ClaimProviderContext>(initialClaimProvider) type ClaimProviderProps = React.PropsWithChildren<{ @@ -63,6 +62,3 @@ export const ClaimProvider = ({ <ClaimContext.Provider value={context}>{children}</ClaimContext.Provider> ) } -export const useClaim = () => - React.useContext(ClaimContext) ?? - invalid('useClaim: needs to be wrapped in a ClaimManagerProvider') diff --git a/packages/claim/src/translators/reactjs/state/state.mocks.ts b/packages/claim/src/translators/reactjs/state/state.mocks.ts index f72a089f54..18a697bd43 100644 --- a/packages/claim/src/translators/reactjs/state/state.mocks.ts +++ b/packages/claim/src/translators/reactjs/state/state.mocks.ts @@ -27,7 +27,7 @@ const withClaimTokenDone: ClaimState = { claimInfo: claimApiMockResponses.claimTokens.done, } as const -export const mocks = { +export const mocksState = { empty, withScanActionClaim, withClaimTokenAccepted, diff --git a/packages/claim/src/translators/reactjs/state/state.test.ts b/packages/claim/src/translators/reactjs/state/state.test.ts new file mode 100644 index 0000000000..959242ae2d --- /dev/null +++ b/packages/claim/src/translators/reactjs/state/state.test.ts @@ -0,0 +1,73 @@ +import {Scan} from '@yoroi/types' +import {claimApiMockResponses} from '../../../manager.mocks' +import { + claimReducer, + defaultClaimState, + ClaimActionType, + ClaimActionInfoChanged, + ClaimActionScanActionClaimChanged, + ClaimActionReset, +} from './state' + +describe('claimReducer', () => { + it('should return default state when no action matches', () => { + const initialState = defaultClaimState + const action = {type: 'unknown'} as any + const newState = claimReducer(initialState, action) + + expect(newState).toBe(initialState) + }) + + it('should handle ClaimInfoChanged action', () => { + const initialState = defaultClaimState + const action: ClaimActionInfoChanged = { + type: ClaimActionType.ClaimInfoChanged, + claimInfo: claimApiMockResponses.claimTokens.accepted, + } + + const newState = claimReducer(initialState, action) + + expect(newState.claimInfo).toEqual( + claimApiMockResponses.claimTokens.accepted, + ) + }) + + it('should handle ScanActionClaimChanged action', () => { + const initialState = defaultClaimState + const scanAction: Scan.ActionClaim = { + action: 'claim', + code: 'code', + params: {}, + url: 'https://example.com', + } + const action: ClaimActionScanActionClaimChanged = { + type: ClaimActionType.ScanActionClaimChanged, + scanActionClaim: scanAction, + } + + const newState = claimReducer(initialState, action) + + expect(newState.scanActionClaim).toEqual(scanAction) + }) + + it('should handle Reset action', () => { + const scanAction: Scan.ActionClaim = { + action: 'claim', + code: 'code', + params: {}, + url: 'https://example.com', + } + const populatedState = { + claimInfo: claimApiMockResponses.claimTokens.accepted, + scanActionClaim: scanAction, + } + const action: ClaimActionReset = { + type: ClaimActionType.Reset, + } + + const newState = claimReducer(populatedState, action) + + expect(newState.claimInfo).toBeUndefined() + expect(newState.scanActionClaim).toBeUndefined() + }) +}) diff --git a/packages/claim/src/translators/reactjs/state/state.ts b/packages/claim/src/translators/reactjs/state/state.ts index 3e2306267e..0cec6db978 100644 --- a/packages/claim/src/translators/reactjs/state/state.ts +++ b/packages/claim/src/translators/reactjs/state/state.ts @@ -19,12 +19,6 @@ export const defaultClaimState: ClaimState = { scanActionClaim: undefined, } as const -export const defaultClaimActions: ClaimActions = { - claimInfoChanged: () => invalid('missing init'), - scanActionClaimChanged: () => invalid('missing init'), - reset: () => invalid('missing init'), -} as const - export const claimReducer = ( state: ClaimState, action: ClaimAction, @@ -50,15 +44,28 @@ export type ClaimState = Readonly<{ scanActionClaim: Scan.ActionClaim | undefined }> +export type ClaimActionInfoChanged = { + type: ClaimActionType.ClaimInfoChanged + claimInfo: Claim.Info +} + +export type ClaimActionScanActionClaimChanged = { + type: ClaimActionType.ScanActionClaimChanged + scanActionClaim: Scan.ActionClaim +} + +export type ClaimActionReset = { + type: ClaimActionType.Reset +} + export type ClaimAction = - | { - type: ClaimActionType.ClaimInfoChanged - claimInfo: Claim.Info - } - | { - type: ClaimActionType.ScanActionClaimChanged - scanActionClaim: Scan.ActionClaim - } - | { - type: ClaimActionType.Reset - } + | ClaimActionInfoChanged + | ClaimActionScanActionClaimChanged + | ClaimActionReset + +/* istanbul ignore next */ +export const defaultClaimActions: ClaimActions = { + claimInfoChanged: () => invalid('missing init'), + scanActionClaimChanged: () => invalid('missing init'), + reset: () => invalid('missing init'), +} as const From e739d9db9de7890b8dbde16bad6922cf95c74d48 Mon Sep 17 00:00:00 2001 From: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> Date: Fri, 6 Sep 2024 21:35:52 +0100 Subject: [PATCH 4/6] refactor(wallet-mobile): claim module extracted --- apps/wallet-mobile/package.json | 1 + .../Claim/common/useClaimErrorResolver.tsx | 22 +-- .../useCases/ShowSuccessScreen.stories.tsx | 12 +- .../Claim/useCases/ShowSuccessScreen.tsx | 9 +- .../src/features/Scan/common/parsers.test.ts | 5 +- .../src/features/Scan/common/parsers.ts | 8 +- .../Scan/common/useScanErrorResolver.tsx | 5 +- .../Scan/common/useTriggerScanAction.tsx | 9 +- .../Transactions/TxHistoryNavigator.tsx | 9 +- apps/wallet-mobile/src/kernel/navigation.tsx | 5 +- .../Transactions/TxHistoryNavigator.json | 176 +++++++++--------- apps/wallet-mobile/tsconfig.json | 26 ++- packages/claim/src/index.ts | 1 + 13 files changed, 145 insertions(+), 143 deletions(-) diff --git a/apps/wallet-mobile/package.json b/apps/wallet-mobile/package.json index b3a3cd72ab..6b4fdc0281 100644 --- a/apps/wallet-mobile/package.json +++ b/apps/wallet-mobile/package.json @@ -125,6 +125,7 @@ "@types/bip39": "^3.0.0", "@types/pbkdf2": "^3.1.2", "@yoroi/api": "^1.5.2", + "@yoroi/claim": "1.0.0", "@yoroi/common": "^1.5.4", "@yoroi/exchange": "^2.1.1", "@yoroi/explorers": "^1.0.2", diff --git a/apps/wallet-mobile/src/features/Claim/common/useClaimErrorResolver.tsx b/apps/wallet-mobile/src/features/Claim/common/useClaimErrorResolver.tsx index b89de17493..4e30978c2f 100644 --- a/apps/wallet-mobile/src/features/Claim/common/useClaimErrorResolver.tsx +++ b/apps/wallet-mobile/src/features/Claim/common/useClaimErrorResolver.tsx @@ -1,12 +1,6 @@ +import {Claim} from '@yoroi/types' + import {useApiErrorResolver} from '../../../hooks/useApiErrorResolver' -import { - ClaimApiErrorsAlreadyClaimed, - ClaimApiErrorsExpired, - ClaimApiErrorsInvalidRequest, - ClaimApiErrorsNotFound, - ClaimApiErrorsRateLimited, - ClaimApiErrorsTooEarly, -} from '../../../../../../packages/claim/src/errors' import {useDialogs} from './useDialogs' export const useClaimErrorResolver = () => { @@ -14,12 +8,12 @@ export const useClaimErrorResolver = () => { const apiResolver = useApiErrorResolver() const resolver = (error: unknown) => { - if (error instanceof ClaimApiErrorsAlreadyClaimed) return dialogs.errorAlreadyClaimed - if (error instanceof ClaimApiErrorsExpired) return dialogs.errorExpired - if (error instanceof ClaimApiErrorsInvalidRequest) return dialogs.errorInvalidRequest - if (error instanceof ClaimApiErrorsNotFound) return dialogs.errorNotFound - if (error instanceof ClaimApiErrorsRateLimited) return dialogs.errorRateLimited - if (error instanceof ClaimApiErrorsTooEarly) return dialogs.errorTooEarly + if (error instanceof Claim.Api.Errors.AlreadyClaimed) return dialogs.errorAlreadyClaimed + if (error instanceof Claim.Api.Errors.Expired) return dialogs.errorExpired + if (error instanceof Claim.Api.Errors.InvalidRequest) return dialogs.errorInvalidRequest + if (error instanceof Claim.Api.Errors.NotFound) return dialogs.errorNotFound + if (error instanceof Claim.Api.Errors.RateLimited) return dialogs.errorRateLimited + if (error instanceof Claim.Api.Errors.TooEarly) return dialogs.errorTooEarly // falback to api errors return apiResolver(error) diff --git a/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.stories.tsx b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.stories.tsx index c57fae2725..f9de220027 100644 --- a/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.stories.tsx +++ b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.stories.tsx @@ -1,14 +1,12 @@ import {DecoratorFunction} from '@storybook/addons' import {storiesOf} from '@storybook/react-native' -import React from 'react' +import {claimManagerMockInstances, ClaimProvider, mocksState} from '@yoroi/claim' +import * as React from 'react' import {QueryClientProvider} from 'react-query' import {queryClientFixture} from '../../../kernel/fixtures/fixtures' import {mocks as walletMocks} from '../../../yoroi-wallets/mocks/wallet' import {WalletManagerProviderMock} from '../../../yoroi-wallets/mocks/WalletManagerProviderMock' -import {claimApiMockInstances} from '../../../../../../packages/claim/src/manager.mocks' -import {ClaimProvider} from '../../../../../../packages/claim/src/translators/reactjs/ClaimProvider' -import {mocks as claimMocks} from '../../../../../../packages/claim/src/translators/reactjs/state.mocks' import {ShowSuccessScreen} from './ShowSuccessScreen' const AppDecorator: DecoratorFunction<React.ReactNode> = (story) => { @@ -23,21 +21,21 @@ storiesOf('Claim ShowSuccessScreen', module) .addDecorator(AppDecorator) .add('processing', () => { return ( - <ClaimProvider claimApi={claimApiMockInstances.error} initialState={claimMocks.withClaimTokenProcessing}> + <ClaimProvider manager={claimManagerMockInstances.error} initialState={mocksState.withClaimTokenProcessing}> <ShowSuccessScreen /> </ClaimProvider> ) }) .add('accepted', () => { return ( - <ClaimProvider claimApi={claimApiMockInstances.error} initialState={claimMocks.withClaimTokenAccepted}> + <ClaimProvider manager={claimManagerMockInstances.error} initialState={mocksState.withClaimTokenAccepted}> <ShowSuccessScreen /> </ClaimProvider> ) }) .add('done', () => { return ( - <ClaimProvider claimApi={claimApiMockInstances.error} initialState={claimMocks.withClaimTokenDone}> + <ClaimProvider manager={claimManagerMockInstances.error} initialState={mocksState.withClaimTokenDone}> <ShowSuccessScreen /> </ClaimProvider> ) diff --git a/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx index 7ebf4a63d8..f81d57fffa 100644 --- a/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx +++ b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx @@ -1,7 +1,8 @@ +import {useClaim} from '@yoroi/claim' import {useExplorers} from '@yoroi/explorers' import {sortTokenAmountsByInfo} from '@yoroi/portfolio' import {useTheme} from '@yoroi/theme' -import {App, Portfolio} from '@yoroi/types' +import {App, Claim, Portfolio} from '@yoroi/types' import React from 'react' import {FlatList, Linking, Platform, StyleSheet, Text, TextProps, View, ViewProps} from 'react-native' import {SafeAreaView} from 'react-native-safe-area-context' @@ -18,8 +19,6 @@ import {useDialogs} from '../common/useDialogs' import {useNavigateTo} from '../common/useNavigateTo' import {useStrings} from '../common/useStrings' import {ClaimSuccessIllustration} from '../illustrations/ClaimSuccessIllustration' -import {useClaim} from '../../../../../../packages/claim/src/translators/reactjs/ClaimProvider' -import {ClaimStatus} from '../../../../../../packages/claim/src/types' export const ShowSuccessScreen = () => { const {styles} = useStyles() @@ -69,10 +68,10 @@ const Header = ({style, ...props}: ViewProps) => { const {styles} = useStyles() return <View style={[styles.header, style]} {...props} /> } -const Status = ({status, style, ...props}: TextProps & {status: ClaimStatus}) => { +const Status = ({status, style, ...props}: TextProps & {status: Claim.Status}) => { const {styles} = useStyles() const dialogs = useDialogs() - const dialog: Record<ClaimStatus, {message: string; title: string}> = { + const dialog: Record<Claim.Status, {message: string; title: string}> = { ['processing']: dialogs.processing, ['accepted']: dialogs.accepted, ['done']: dialogs.done, diff --git a/apps/wallet-mobile/src/features/Scan/common/parsers.test.ts b/apps/wallet-mobile/src/features/Scan/common/parsers.test.ts index b9ba0a0058..728937d77f 100644 --- a/apps/wallet-mobile/src/features/Scan/common/parsers.test.ts +++ b/apps/wallet-mobile/src/features/Scan/common/parsers.test.ts @@ -1,8 +1,7 @@ -import {Links} from '@yoroi/types' +import {Links, Scan} from '@yoroi/types' import {codeContent} from './mocks' import {parseScanAction} from './parsers' -import {ScanErrorUnknownContent} from './types' describe('parseScanAction', () => { it('should correctly parse a non-link address', () => { @@ -14,7 +13,7 @@ describe('parseScanAction', () => { }) it('should throw ScanErrorUnknownContent for invalid non-link content', () => { - expect(() => parseScanAction(codeContent.noLink.error.invalid)).toThrow(ScanErrorUnknownContent) + expect(() => parseScanAction(codeContent.noLink.error.invalid)).toThrow(Scan.Errors.UnknownContent) }) it('should throw SchemeNotImplemented for links not supporte like bitcoin', () => { diff --git a/apps/wallet-mobile/src/features/Scan/common/parsers.ts b/apps/wallet-mobile/src/features/Scan/common/parsers.ts index 4d2037ed02..0af18bdf37 100644 --- a/apps/wallet-mobile/src/features/Scan/common/parsers.ts +++ b/apps/wallet-mobile/src/features/Scan/common/parsers.ts @@ -1,14 +1,12 @@ import {linksCardanoModuleMaker} from '@yoroi/links' -import {Links} from '@yoroi/types' +import {Links, Scan} from '@yoroi/types' -import {ScanAction, ScanErrorUnknownContent} from './types' - -export const parseScanAction = (codeContent: string): ScanAction => { +export const parseScanAction = (codeContent: string): Scan.Action => { const isLink = codeContent.includes(':') // NOTE: if it is a string < 256 with valid characters, it'd be consider a Yoroi Receiver (wallet address | domain name) if (!isLink) { - if (codeContent.length > 255 || !nonProtocolRegex.test(codeContent)) throw new ScanErrorUnknownContent() + if (codeContent.length > 255 || !nonProtocolRegex.test(codeContent)) throw new Scan.Errors.UnknownContent() return { action: 'send-only-receiver', receiver: codeContent, diff --git a/apps/wallet-mobile/src/features/Scan/common/useScanErrorResolver.tsx b/apps/wallet-mobile/src/features/Scan/common/useScanErrorResolver.tsx index 99dead270a..9636f300ce 100644 --- a/apps/wallet-mobile/src/features/Scan/common/useScanErrorResolver.tsx +++ b/apps/wallet-mobile/src/features/Scan/common/useScanErrorResolver.tsx @@ -1,13 +1,12 @@ -import {Links} from '@yoroi/types' +import {Links, Scan} from '@yoroi/types' -import {ScanErrorUnknownContent} from './types' import {useDialogs} from './useDialogs' export const useScanErrorResolver = () => { const dialogs = useDialogs() const resolver = (error: unknown) => { - if (error instanceof ScanErrorUnknownContent) return dialogs.errorUnknownContent + if (error instanceof Scan.Errors.UnknownContent) return dialogs.errorUnknownContent if (error instanceof Links.Errors.ExtraParamsDenied) return dialogs.linksErrorExtraParamsDenied if (error instanceof Links.Errors.ForbiddenParamsProvided) return dialogs.linksErrorForbiddenParamsProvided if (error instanceof Links.Errors.RequiredParamsMissing) return dialogs.linksErrorRequiredParamsMissing diff --git a/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx b/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx index 759de02b7e..324a494f3b 100644 --- a/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx +++ b/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx @@ -1,20 +1,19 @@ +import {useClaim, useClaimTokens} from '@yoroi/claim' import {toBigInt} from '@yoroi/common' import {useTransfer} from '@yoroi/transfer' +import {Scan} from '@yoroi/types' import * as React from 'react' import {Alert} from 'react-native' import {useModal} from '../../../components/Modal/ModalContext' import {useClaimErrorResolver} from '../../../features/Claim/common/useClaimErrorResolver' import {useStrings as useStringsClaim} from '../../../features/Claim/common/useStrings' -import {useClaim} from '../../../../../../packages/claim/src/translators/reactjs/ClaimProvider' -import {useClaimTokens} from '../../../../../../packages/claim/src/translators/reactjs/useClaimTokens' import {AskConfirmation} from '../../../features/Claim/useCases/AskConfirmation' import {pastedFormatter} from '../../../yoroi-wallets/utils/amountUtils' import {useSelectedWallet} from '../../WalletManager/common/hooks/useSelectedWallet' -import {ScanAction, ScanFeature} from './types' import {useNavigateTo} from './useNavigateTo' -export const useTriggerScanAction = ({insideFeature}: {insideFeature: ScanFeature}) => { +export const useTriggerScanAction = ({insideFeature}: {insideFeature: Scan.Feature}) => { const { wallet: {portfolioPrimaryTokenInfo}, } = useSelectedWallet() @@ -45,7 +44,7 @@ export const useTriggerScanAction = ({insideFeature}: {insideFeature: ScanFeatur }) const stringsClaim = useStringsClaim() - const trigger = (scanAction: ScanAction) => { + const trigger = (scanAction: Scan.Action) => { switch (scanAction.action) { case 'send-single-pt': { if (insideFeature !== 'send') resetTransferState() diff --git a/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx b/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx index f286e9bb19..e186ab34a7 100644 --- a/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx +++ b/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx @@ -1,6 +1,7 @@ import {init} from '@emurgo/cross-csl-mobile' import {useNavigation} from '@react-navigation/native' import {createStackNavigator, StackNavigationOptions} from '@react-navigation/stack' +import {claimManagerMaker, ClaimProvider} from '@yoroi/claim' import {useAsyncStorage} from '@yoroi/common' import {exchangeApiMaker, exchangeManagerMaker, ExchangeProvider} from '@yoroi/exchange' import {resolverApiMaker, resolverManagerMaker, ResolverProvider, resolverStorageMaker} from '@yoroi/resolver' @@ -18,8 +19,6 @@ import { TxHistoryRouteNavigation, TxHistoryRoutes, } from '../../kernel/navigation' -import {claimApiMaker} from '../../../../../packages/claim/src/manager' -import {ClaimProvider} from '../../../../../packages/claim/src/translators/reactjs/ClaimProvider' import {ShowSuccessScreen} from '../Claim/useCases/ShowSuccessScreen' import {CreateExchangeOrderScreen} from '../Exchange/useCases/CreateExchangeOrderScreen/CreateExchangeOrderScreen' import {SelectProviderFromListScreen} from '../Exchange/useCases/SelectProviderFromListScreen/SelectProviderFromListScreen' @@ -79,8 +78,8 @@ export const TxHistoryNavigator = () => { }, [storage, wallet.id, wallet.isMainnet]) // claim - const claimApi = React.useMemo(() => { - return claimApiMaker({ + const claimManager = React.useMemo(() => { + return claimManagerMaker({ address: wallet.externalAddresses[0], primaryTokenInfo: wallet.portfolioPrimaryTokenInfo, tokenManager: wallet.networkManager.tokenManager, @@ -107,7 +106,7 @@ export const TxHistoryNavigator = () => { return ( <ReceiveProvider key={wallet.id}> <ResolverProvider resolverManager={resolverManager}> - <ClaimProvider key={wallet.id} claimApi={claimApi}> + <ClaimProvider key={wallet.id} manager={claimManager}> <ExchangeProvider key={wallet.id} manager={exchangeManager} diff --git a/apps/wallet-mobile/src/kernel/navigation.tsx b/apps/wallet-mobile/src/kernel/navigation.tsx index a14cb8afb8..4009822268 100644 --- a/apps/wallet-mobile/src/kernel/navigation.tsx +++ b/apps/wallet-mobile/src/kernel/navigation.tsx @@ -11,12 +11,11 @@ import {TransitionPresets} from '@react-navigation/stack' import {StackNavigationOptions, StackNavigationProp} from '@react-navigation/stack' import {isKeyOf} from '@yoroi/common' import {Atoms, ThemedPalette, useTheme} from '@yoroi/theme' -import {Chain, Portfolio} from '@yoroi/types' +import {Chain, Portfolio, Scan} from '@yoroi/types' import React from 'react' import {Dimensions, InteractionManager, Platform, TouchableOpacity, TouchableOpacityProps, View} from 'react-native' import {Icon} from '../components' -import {ScanFeature} from '../features/Scan/common/types' import {Routes as StakingGovernanceRoutes} from '../features/Staking/Governance/common/navigation' import {YoroiUnsignedTx} from '../yoroi-wallets/types' import {compareArrays} from '../yoroi-wallets/utils/utils' @@ -181,7 +180,7 @@ export type TxHistoryRoutes = { export type TxHistoryRouteNavigation = StackNavigationProp<TxHistoryRoutes> type ScanStartParams = Readonly<{ - insideFeature: ScanFeature + insideFeature: Scan.Feature }> export type ScanRoutes = { 'scan-start': ScanStartParams diff --git a/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json b/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json index 0597aae766..7d93064caa 100644 --- a/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json +++ b/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json @@ -4,14 +4,14 @@ "defaultMessage": "!!!Receive", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 418, + "line": 417, "column": 16, - "index": 15256 + "index": 15219 }, "end": { - "line": 421, + "line": 420, "column": 3, - "index": 15345 + "index": 15308 } }, { @@ -19,14 +19,14 @@ "defaultMessage": "!!!Address details", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 422, + "line": 421, "column": 32, - "index": 15379 + "index": 15342 }, "end": { - "line": 425, + "line": 424, "column": 3, - "index": 15492 + "index": 15455 } }, { @@ -34,14 +34,14 @@ "defaultMessage": "!!!Swap", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 426, + "line": 425, "column": 13, - "index": 15507 + "index": 15470 }, "end": { - "line": 429, + "line": 428, "column": 3, - "index": 15580 + "index": 15543 } }, { @@ -49,14 +49,14 @@ "defaultMessage": "!!!Swap from", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 430, + "line": 429, "column": 17, - "index": 15599 + "index": 15562 }, "end": { - "line": 433, + "line": 432, "column": 3, - "index": 15676 + "index": 15639 } }, { @@ -64,14 +64,14 @@ "defaultMessage": "!!!Swap to", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 434, + "line": 433, "column": 15, - "index": 15693 + "index": 15656 }, "end": { - "line": 437, + "line": 436, "column": 3, - "index": 15766 + "index": 15729 } }, { @@ -79,14 +79,14 @@ "defaultMessage": "!!!Slippage Tolerance", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 438, + "line": 437, "column": 21, - "index": 15789 + "index": 15752 }, "end": { - "line": 441, + "line": 440, "column": 3, - "index": 15884 + "index": 15847 } }, { @@ -94,14 +94,14 @@ "defaultMessage": "!!!Select pool", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 442, + "line": 441, "column": 14, - "index": 15900 + "index": 15863 }, "end": { - "line": 445, + "line": 444, "column": 3, - "index": 15981 + "index": 15944 } }, { @@ -109,14 +109,14 @@ "defaultMessage": "!!!Send", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 446, + "line": 445, "column": 13, - "index": 15996 + "index": 15959 }, "end": { - "line": 449, + "line": 448, "column": 3, - "index": 16076 + "index": 16039 } }, { @@ -124,14 +124,14 @@ "defaultMessage": "!!!Scan QR code address", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 450, + "line": 449, "column": 18, - "index": 16096 + "index": 16059 }, "end": { - "line": 453, + "line": 452, "column": 3, - "index": 16197 + "index": 16160 } }, { @@ -139,14 +139,14 @@ "defaultMessage": "!!!Select asset", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 454, + "line": 453, "column": 20, - "index": 16219 + "index": 16182 }, "end": { - "line": 457, + "line": 456, "column": 3, - "index": 16308 + "index": 16271 } }, { @@ -154,14 +154,14 @@ "defaultMessage": "!!!Assets added", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 458, + "line": 457, "column": 26, - "index": 16336 + "index": 16299 }, "end": { - "line": 461, + "line": 460, "column": 3, - "index": 16437 + "index": 16400 } }, { @@ -169,14 +169,14 @@ "defaultMessage": "!!!Edit amount", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 462, + "line": 461, "column": 19, - "index": 16458 + "index": 16421 }, "end": { - "line": 465, + "line": 464, "column": 3, - "index": 16551 + "index": 16514 } }, { @@ -184,14 +184,14 @@ "defaultMessage": "!!!Confirm", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 466, + "line": 465, "column": 16, - "index": 16569 + "index": 16532 }, "end": { - "line": 469, + "line": 468, "column": 3, - "index": 16655 + "index": 16618 } }, { @@ -199,14 +199,14 @@ "defaultMessage": "!!!Share this address to receive payments. To protect your privacy, new addresses are generated automatically once you use them.", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 470, + "line": 469, "column": 19, - "index": 16676 + "index": 16639 }, "end": { - "line": 476, + "line": 475, "column": 3, - "index": 16914 + "index": 16877 } }, { @@ -214,14 +214,14 @@ "defaultMessage": "!!!Confirm transaction", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 477, + "line": 476, "column": 27, - "index": 16943 + "index": 16906 }, "end": { - "line": 480, + "line": 479, "column": 3, - "index": 17036 + "index": 16999 } }, { @@ -229,14 +229,14 @@ "defaultMessage": "!!!Please scan a QR code", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 481, + "line": 480, "column": 13, - "index": 17051 + "index": 17014 }, "end": { - "line": 484, + "line": 483, "column": 3, - "index": 17126 + "index": 17089 } }, { @@ -244,14 +244,14 @@ "defaultMessage": "!!!Success", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 485, + "line": 484, "column": 25, - "index": 17153 + "index": 17116 }, "end": { - "line": 488, + "line": 487, "column": 3, - "index": 17227 + "index": 17190 } }, { @@ -259,14 +259,14 @@ "defaultMessage": "!!!Request specific amount", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 489, + "line": 488, "column": 18, - "index": 17247 + "index": 17210 }, "end": { - "line": 492, + "line": 491, "column": 3, - "index": 17361 + "index": 17324 } }, { @@ -274,14 +274,14 @@ "defaultMessage": "!!!Buy/Sell ADA", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 493, + "line": 492, "column": 28, - "index": 17391 + "index": 17354 }, "end": { - "line": 496, + "line": 495, "column": 3, - "index": 17487 + "index": 17450 } }, { @@ -289,14 +289,14 @@ "defaultMessage": "!!!Buy provider", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 497, + "line": 496, "column": 29, - "index": 17518 + "index": 17481 }, "end": { - "line": 500, + "line": 499, "column": 3, - "index": 17626 + "index": 17589 } }, { @@ -304,14 +304,14 @@ "defaultMessage": "!!!Sell provider", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 501, + "line": 500, "column": 30, - "index": 17658 + "index": 17621 }, "end": { - "line": 504, + "line": 503, "column": 3, - "index": 17768 + "index": 17731 } }, { @@ -319,14 +319,14 @@ "defaultMessage": "!!!Tx Details", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 505, + "line": 504, "column": 18, - "index": 17788 + "index": 17751 }, "end": { - "line": 508, + "line": 507, "column": 3, - "index": 17882 + "index": 17845 } } ] \ No newline at end of file diff --git a/apps/wallet-mobile/tsconfig.json b/apps/wallet-mobile/tsconfig.json index 680ebe3489..2a6a763398 100644 --- a/apps/wallet-mobile/tsconfig.json +++ b/apps/wallet-mobile/tsconfig.json @@ -3,15 +3,31 @@ "compilerOptions": { "jsx": "react", "allowJs": false, - "lib": ["es2019", "dom"], + "lib": [ + "es2019", + "dom" + ], "moduleResolution": "node", "strict": true, "noImplicitAny": true, - "types": ["react", "react-native", "jest", "node", "chai"], + "types": [ + "react", + "react-native", + "jest", + "node", + "chai" + ], "baseUrl": ".", "paths": { - "*.md": ["text-loader"] + "*.md": [ + "text-loader" + ] } }, - "include": ["./.d.ts", "react-native-svg-charts.d.ts", "./src", ".storybook/decorators", "../../packages/claim/src/api-faucet.mocks.ts", "../../packages/claim/src/manager.mocks.ts", "../../packages/claim/src/manager.tests.ts", "../../packages/claim/src/manager.ts", "../../packages/claim/src/translators/reactjs/ClaimProvider.tsx", "../../packages/claim/src/errors.ts", "../../packages/claim/src/translators/reactjs/state.mocks.ts", "../../packages/claim/src/translators/reactjs/state.ts", "../../packages/claim/src/transformers.ts", "../../packages/claim/src/types.ts", "../../packages/claim/src/translators/reactjs/useClaimTokens.tsx", "../../packages/claim/src/validators.ts"] -} + "include": [ + "./.d.ts", + "react-native-svg-charts.d.ts", + "./src", + ".storybook/decorators" + ] +} \ No newline at end of file diff --git a/packages/claim/src/index.ts b/packages/claim/src/index.ts index 79c970d0ff..c4710bbe41 100644 --- a/packages/claim/src/index.ts +++ b/packages/claim/src/index.ts @@ -6,6 +6,7 @@ export * from './manager.mocks' export * from './translators/reactjs/hooks/useClaim' export * from './translators/reactjs/hooks/useClaimTokens' export * from './translators/reactjs/provider/ClaimProvider' +export * from './translators/reactjs/state/state.mocks' export * from './transformers' export * from './validators' From 92e15e4e30a7f95d8609bcd17994cb8660701867 Mon Sep 17 00:00:00 2001 From: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> Date: Fri, 6 Sep 2024 21:42:17 +0100 Subject: [PATCH 5/6] Update packages/claim/package.json Signed-off-by: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> --- packages/claim/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/claim/package.json b/packages/claim/package.json index 6fbaf3ae02..e5df9f504e 100644 --- a/packages/claim/package.json +++ b/packages/claim/package.json @@ -143,7 +143,6 @@ }, "devDependencies": { "@commitlint/config-conventional": "^17.0.2", - "@emurgo/yoroi-lib": "^1.0.1", "@react-native-async-storage/async-storage": "^1.19.3", "@react-native-community/eslint-config": "^3.0.2", "@release-it/conventional-changelog": "^5.0.0", From 37dd4a32ed60fa9760be5613b0fc98272c381b14 Mon Sep 17 00:00:00 2001 From: Michal <miso.szorad@gmail.com> Date: Mon, 9 Sep 2024 09:56:57 +0100 Subject: [PATCH 6/6] fix(wallet-mobile): Update favicon URL --- .../wallet-mobile/src/features/Discover/common/helpers.ts | 2 +- .../src/legacy/Staking/StakingCenter/StakingCenter.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/wallet-mobile/src/features/Discover/common/helpers.ts b/apps/wallet-mobile/src/features/Discover/common/helpers.ts index d2aaa2625d..8a57b3b392 100644 --- a/apps/wallet-mobile/src/features/Discover/common/helpers.ts +++ b/apps/wallet-mobile/src/features/Discover/common/helpers.ts @@ -152,5 +152,5 @@ export const createDappConnector = (options: CreateDappConnectorOptions) => { export const getDappFallbackLogo = (website: string) => { const withoutProtocol = website.replace(/(^\w+:|^)\/\//, '') - return `https://api.faviconkit.com/${withoutProtocol}/144` + return `https://api.faviconkit.com/${withoutProtocol}/32` } diff --git a/apps/wallet-mobile/translations/messages/src/legacy/Staking/StakingCenter/StakingCenter.json b/apps/wallet-mobile/translations/messages/src/legacy/Staking/StakingCenter/StakingCenter.json index 505dc35da7..bfbee05540 100644 --- a/apps/wallet-mobile/translations/messages/src/legacy/Staking/StakingCenter/StakingCenter.json +++ b/apps/wallet-mobile/translations/messages/src/legacy/Staking/StakingCenter/StakingCenter.json @@ -6,12 +6,12 @@ "start": { "line": 127, "column": 9, - "index": 4466 + "index": 4454 }, "end": { "line": 130, "column": 3, - "index": 4574 + "index": 4562 } }, { @@ -21,12 +21,12 @@ "start": { "line": 131, "column": 11, - "index": 4587 + "index": 4575 }, "end": { "line": 134, "column": 3, - "index": 4753 + "index": 4741 } } ] \ No newline at end of file