diff --git a/.vscode/settings.json b/.vscode/settings.json index 6b2eddc6ac..4b8545f0a4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,13 @@ { "java.configuration.updateBuildConfiguration": "disabled", - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "jestrunner.debugOptions": { + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts", + "runtimeArgs": [ + "test", + "${fileBasename}", + "--no-cache", + "--color" + ] + }, } \ No newline at end of file diff --git a/apps/wallet-mobile/ios/Podfile.lock b/apps/wallet-mobile/ios/Podfile.lock index 5c9e32eaf9..b9cb45d978 100644 --- a/apps/wallet-mobile/ios/Podfile.lock +++ b/apps/wallet-mobile/ios/Podfile.lock @@ -495,10 +495,10 @@ PODS: - SentryPrivate (= 8.9.3) - SentryPrivate (8.9.3) - Yoga (1.14.0) - - ZXingObjC/Core (3.6.5) - - ZXingObjC/OneD (3.6.5): + - ZXingObjC/Core (3.6.9) + - ZXingObjC/OneD (3.6.9): - ZXingObjC/Core - - ZXingObjC/PDF417 (3.6.5): + - ZXingObjC/PDF417 (3.6.9): - ZXingObjC/Core DEPENDENCIES: @@ -858,7 +858,7 @@ SPEC CHECKSUMS: Sentry: 97161cac725da1ecbe77d1445bf8a61c1e5667f1 SentryPrivate: 9a76def09fb08f9501997b8df946e8097947b94f Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 - ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb + ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 PODFILE CHECKSUM: 31f344d67f1a9c35e34eb202e3cdfeb4907367e8 diff --git a/apps/wallet-mobile/package.json b/apps/wallet-mobile/package.json index e204d6e44a..1137499aa3 100644 --- a/apps/wallet-mobile/package.json +++ b/apps/wallet-mobile/package.json @@ -126,12 +126,13 @@ "@yoroi/common": "1.5.2", "@yoroi/exchange": "2.0.1", "@yoroi/links": "1.5.4", + "@yoroi/portfolio": "1.0.0", "@yoroi/resolver": "2.0.4", + "@yoroi/setup-wallet": "1.0.0", "@yoroi/staking": "1.5.1", "@yoroi/swap": "1.5.2", "@yoroi/theme": "^1.0.0", "@yoroi/transfer": "1.0.0", - "@yoroi/setup-wallet": "1.0.0", "add": "2.0.6", "assert": "^2.0.0", "axios": "^1.5.0", @@ -190,6 +191,7 @@ "react-native-webview": "^11.25.0", "react-query": "^3.39.3", "reselect": "^4.0.0", + "rxjs": "^7.8.1", "sentry-expo": "^7.0.1", "stream-browserify": "3.0.0", "tinycolor2": "1.4.2", diff --git a/apps/wallet-mobile/scripts/create-mocked-token-infos.js b/apps/wallet-mobile/scripts/create-mocked-token-infos.js new file mode 100644 index 0000000000..9bca7fa207 --- /dev/null +++ b/apps/wallet-mobile/scripts/create-mocked-token-infos.js @@ -0,0 +1,40 @@ +const nftCryptoKitty = { + decimals: 0, + ticker: 'CryptoKitty', + name: 'CryptoKitty #1234', + symbol: 'CK', + status: 'normal', + application: 'token', + tag: '', + reference: '0xabcdef1234567890.cryptokitty1234', + website: 'https://www.cryptokitties.co', + originalImage: 'https://cdn.example.com/ck-original1234.png', + id: '14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.43554259', + fingerprint: 'asset1s7nlt45cc82upqewvjtgu7g97l7eg483c6wu75', + nature: 'secondary', + type: 'nft', +} + +function generateTokenInfos(baseToken, count) { + const tokens = [] + for (let i = 0; i < count; i++) { + // Clone the base token object to create a new token + const id = `${baseToken.id.split('.')[0]}.${Buffer.from(String(i)).toString('hex')}` + const name = `${baseToken.name.split(' ')[0]} #${i}` + const reference = `${baseToken.reference.split('.')[0]}.${Buffer.from(String(i)).toString('hex')}` + const fingerprint = `asset${i}s7nlt45cc82upqewvjtgu7g97l7eg483c6wu${i}` + + const newToken = {...baseToken, id, name, reference, fingerprint} + + tokens.push(newToken) + } + return tokens +} + +const generatedTokens = generateTokenInfos(nftCryptoKitty, 50) + +const apiResponseTokenInfos = generatedTokens.reduce((acc, token, index) => { + acc[token.id] = [200, token, `hash${index + 1}`, 3600] + return acc +}, {}) + diff --git a/apps/wallet-mobile/src/AppNavigator.tsx b/apps/wallet-mobile/src/AppNavigator.tsx index cfdfb3a24d..d97f976e4b 100644 --- a/apps/wallet-mobile/src/AppNavigator.tsx +++ b/apps/wallet-mobile/src/AppNavigator.tsx @@ -19,6 +19,7 @@ import {ModalScreen} from './components/Modal/ModalScreen' import {AgreementChangedNavigator, InitializationNavigator} from './features/Initialization' import {LegalAgreement, useLegalAgreement} from './features/Initialization/common' import {useDeepLinkWatcher} from './features/Links/common/useDeepLinkWatcher' +import {PortfolioScreen} from './features/Portfolio/useCases/PortfolioScreen' import {AddWalletNavigator} from './features/SetupWallet/SetupWalletNavigator' import {CONFIG} from './legacy/config' import {DeveloperScreen} from './legacy/DeveloperScreen' @@ -160,6 +161,8 @@ export const AppNavigator = () => { + + )} @@ -248,9 +251,9 @@ type FirstAction = 'auth-with-pin' | 'auth-with-os' | 'request-new-pin' | 'first const getFirstAction = ( isAuthOsSupported: boolean, authSetting: AuthSetting, - agreement: LegalAgreement | undefined, + legalAgreement: LegalAgreement | undefined | null, ): FirstAction => { - const hasAccepted = agreement?.latestAcceptedAgreementsDate === CONFIG.AGREEMENT_DATE + const hasAccepted = legalAgreement?.latestAcceptedAgreementsDate === CONFIG.AGREEMENT_DATE if (isString(authSetting) && !hasAccepted) return 'show-agreement-changed-notice' @@ -264,7 +267,7 @@ const getFirstAction = ( const useFirstAction = () => { const authSetting = useAuthSetting() const isAuthOsSupported = useIsAuthOsSupported() - const terms = useLegalAgreement() + const legalAgreement = useLegalAgreement() - return getFirstAction(isAuthOsSupported, authSetting, terms) + return getFirstAction(isAuthOsSupported, authSetting, legalAgreement) } diff --git a/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioBalanceManager.ts b/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioBalanceManager.ts new file mode 100644 index 0000000000..faa177bea4 --- /dev/null +++ b/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioBalanceManager.ts @@ -0,0 +1,73 @@ +import {mountMMKVStorage, observableStorageMaker} from '@yoroi/common' +import {createPrimaryTokenInfo, portfolioBalanceManagerMaker, portfolioBalanceStorageMaker} from '@yoroi/portfolio' +import {Portfolio} from '@yoroi/types' +import * as React from 'react' + +import {YoroiWallet} from '../../../yoroi-wallets/cardano/types' + +export const usePortfolioBalanceManager = ({ + tokenManager, + walletId, +}: { + tokenManager: Portfolio.Manager.Token + walletId: YoroiWallet['id'] +}) => { + return React.useMemo(() => { + const balanceStorageMounted = mountMMKVStorage({path: `balance/${walletId}/`}) + const primaryBreakdownStorageMounted = mountMMKVStorage({ + path: `/primary-breakdown/${walletId}/`, + }) + + const balanceStorage = portfolioBalanceStorageMaker({ + balanceStorage: observableStorageMaker(balanceStorageMounted), + primaryBreakdownStorage: observableStorageMaker(primaryBreakdownStorageMounted), + }) + + const balanceManager = portfolioBalanceManagerMaker({ + tokenManager, + storage: balanceStorage, + primaryToken: { + info: primaryTokenInfo, + discovery: { + counters: { + items: 0, + supply: 0n, + totalItems: 0, + }, + id: primaryTokenInfo.id, + originalMetadata: { + filteredMintMetadatum: null, + referenceDatum: null, + tokenRegistry: null, + }, + properties: {}, + source: { + decimals: Portfolio.Token.Source.Metadata, + name: Portfolio.Token.Source.Metadata, + ticker: Portfolio.Token.Source.Metadata, + symbol: Portfolio.Token.Source.Metadata, + image: Portfolio.Token.Source.Metadata, + }, + }, + }, + sourceId: walletId, + }) + + balanceManager.hydrate() + return { + balanceManager, + balanceStorage, + } + }, [tokenManager, walletId]) +} + +const primaryTokenInfo = createPrimaryTokenInfo({ + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '$', + reference: '', + tag: '', + website: '', + originalImage: '', +}) diff --git a/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioTokenManager.ts b/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioTokenManager.ts new file mode 100644 index 0000000000..6286ddfcaa --- /dev/null +++ b/apps/wallet-mobile/src/features/Portfolio/common/usePortfolioTokenManager.ts @@ -0,0 +1,27 @@ +import {mountMMKVStorage, observableStorageMaker} from '@yoroi/common' +import {portfolioApiMaker, portfolioTokenManagerMaker, portfolioTokenStorageMaker} from '@yoroi/portfolio' +import {Chain, Portfolio} from '@yoroi/types' +import * as React from 'react' + +export const usePortfolioTokenManager = ({network}: {network: Chain.Network}) => { + return React.useMemo(() => { + const tokenDiscoveryStorageMounted = mountMMKVStorage({path: `${network}/token-discovery/`}) + const tokenInfoStorageMounted = mountMMKVStorage({path: `${network}/token-info/`}) + + const tokenStorage = portfolioTokenStorageMaker({ + tokenDiscoveryStorage: observableStorageMaker(tokenDiscoveryStorageMounted), + tokenInfoStorage: observableStorageMaker(tokenInfoStorageMounted), + }) + const api = portfolioApiMaker({ + network, + }) + + const tokenManager = portfolioTokenManagerMaker({ + api, + storage: tokenStorage, + }) + + tokenManager.hydrate({sourceId: 'initial'}) + return {tokenManager, tokenStorage} + }, [network]) +} diff --git a/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx new file mode 100644 index 0000000000..b8d7b9a2ff --- /dev/null +++ b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioScreen.tsx @@ -0,0 +1,127 @@ +import {useNavigation} from '@react-navigation/native' +import {useObserver} from '@yoroi/common' +import {Chain} from '@yoroi/types' +import * as React from 'react' +import {Text, View} from 'react-native' +import {FlatList} from 'react-native-gesture-handler' +import {SafeAreaView} from 'react-native-safe-area-context' + +import {Button, Spacer} from '../../../components' +import {useSelectedWallet} from '../../WalletManager/Context' +import {usePortfolioBalanceManager} from '../common/usePortfolioBalanceManager' +import {usePortfolioTokenManager} from '../common/usePortfolioTokenManager' + +export const PortfolioScreen = () => { + const navigation = useNavigation() + const wallet = useSelectedWallet() + const {tokenManager, tokenStorage} = usePortfolioTokenManager({network: Chain.Network.Main}) + const {balanceManager: bmW1, balanceStorage: bs1} = usePortfolioBalanceManager({ + tokenManager, + walletId: wallet.id, + }) + + // wallet 2 for testing + const {balanceManager: bmW2, balanceStorage: bs2} = usePortfolioBalanceManager({ + tokenManager, + walletId: 'wallet-2', + }) + const {data: balancesW2, isPending: isPendingW2} = useObserver({ + observable: bmW2.observable, + executor: () => bmW2.getBalances().all, + }) + // end of wallet 2 + + const {data: balances, isPending} = useObserver({ + observable: bmW1.observable, + executor: () => bmW1.getBalances().all, + }) + const opacity = isPending || isPendingW2 ? 0.5 : 1 + + const handleOnSync = () => { + bmW1.sync({ + primaryBalance: { + balance: 1n, + lockedInBuiltTxs: 2n, + minRequiredByTokens: 0n, + records: [], + }, + secondaryBalances: new Map([ + ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.34', {balance: 2n, lockedInBuiltTxs: 0n}], + ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.35', {balance: 3n, lockedInBuiltTxs: 0n}], + ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.36', {balance: 4n, lockedInBuiltTxs: 0n}], + ]), + }) + + bmW2.sync({ + primaryBalance: { + balance: 2n, + lockedInBuiltTxs: 3n, + minRequiredByTokens: 0n, + records: [], + }, + secondaryBalances: new Map([ + ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.3130', {balance: 222n, lockedInBuiltTxs: 0n}], + ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.35', {balance: 223n, lockedInBuiltTxs: 0n}], + ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.3131', {balance: 224n, lockedInBuiltTxs: 0n}], + ['14696a4676909f4e3cb1f2e60e2e08e5abed70caf5c02699be971139.3132', {balance: 224n, lockedInBuiltTxs: 0n}], + ]), + }) + } + + const handleOnReset = () => { + bs1.clear() + bs2.clear() + tokenStorage.clear() + navigation.goBack() + } + + return ( + + + Portfolio playground + + + +