diff --git a/examples/example-react-native-app/App.tsx b/examples/example-react-native-app/App.tsx index 37a1b7dda..ca426f777 100644 --- a/examples/example-react-native-app/App.tsx +++ b/examples/example-react-native-app/App.tsx @@ -1,14 +1,63 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; import {ConnectionProvider} from '@solana/wallet-adapter-react'; -import {clusterApiUrl} from '@solana/web3.js'; +import {clusterApiUrl, PublicKey, PublicKeyInitData} from '@solana/web3.js'; import React, {Suspense} from 'react'; -import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native'; +import { + ActivityIndicator, + AppState, + SafeAreaView, + StyleSheet, + View, +} from 'react-native'; import {Provider as PaperProvider} from 'react-native-paper'; +import {Cache, SWRConfig} from 'swr'; import SnackbarProvider from './components/SnackbarProvider'; import MainScreen from './screens/MainScreen'; const DEVNET_ENDPOINT = /*#__PURE__*/ clusterApiUrl('devnet'); +function cacheReviver(key: string, value: any) { + if (key === 'publicKey') { + return new PublicKey(value as PublicKeyInitData); + } else { + return value; + } +} + +const STORAGE_KEY = 'app-cache'; +let initialCacheFetchPromise: Promise; +let initialCacheFetchResult: any; +function asyncStorageProvider() { + if (initialCacheFetchPromise == null) { + initialCacheFetchPromise = AsyncStorage.getItem(STORAGE_KEY).then( + result => { + initialCacheFetchResult = result; + }, + ); + throw initialCacheFetchPromise; + } + let storedAppCache; + try { + storedAppCache = JSON.parse(initialCacheFetchResult, cacheReviver); + } catch {} + const map = new Map(storedAppCache || []); + initialCacheFetchResult = undefined; + function persistCache() { + const appCache = JSON.stringify(Array.from(map.entries())); + AsyncStorage.setItem(STORAGE_KEY, appCache); + } + AppState.addEventListener('change', state => { + if (state !== 'active') { + persistCache(); + } + }); + AppState.addEventListener('memoryWarning', () => { + persistCache(); + }); + return map as Cache; +} + export default function App() { return ( @@ -24,7 +73,9 @@ export default function App() { /> }> - + + + diff --git a/examples/example-react-native-app/utils/useAuthorization.tsx b/examples/example-react-native-app/utils/useAuthorization.tsx index 6eb875cc5..33a321625 100644 --- a/examples/example-react-native-app/utils/useAuthorization.tsx +++ b/examples/example-react-native-app/utils/useAuthorization.tsx @@ -1,8 +1,8 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; import {PublicKey} from '@solana/web3.js'; import { AuthorizationResult, AuthorizeAPI, + AuthToken, Base64EncodedAddress, DeauthorizeAPI, ReauthorizeAPI, @@ -13,45 +13,23 @@ import useSWR from 'swr'; type Account = Readonly<{ address: Base64EncodedAddress; + authToken: AuthToken; publicKey: PublicKey; }>; const STORAGE_KEY = 'cachedAuthorization'; -function getDataFromAuthorizationResult( - authorizationResult: AuthorizationResult, -) { - return { - account: getAccountFromAuthorizationResult(authorizationResult), - authorization: authorizationResult, - }; -} - function getAccountFromAuthorizationResult( authorizationResult: AuthorizationResult, ): Account { const address = authorizationResult.addresses[0]; // TODO(#44): support multiple addresses return { address, + authToken: authorizationResult.auth_token, publicKey: getPublicKeyFromAddress(address), }; } -async function authorizationFetcher(storageKey: string) { - try { - const serializedValue = await AsyncStorage.getItem(storageKey); - if (!serializedValue) { - return null; - } - const authorization = JSON.parse(serializedValue) as AuthorizationResult; - return getDataFromAuthorizationResult(authorization); - } catch { - // Presume the data in storage is corrupt and erase it. - await AsyncStorage.removeItem(STORAGE_KEY); - return null; - } -} - function getPublicKeyFromAddress(address: Base64EncodedAddress): PublicKey { const publicKeyByteArray = toUint8Array(address); return new PublicKey(publicKeyByteArray); @@ -62,60 +40,37 @@ export const APP_IDENTITY = { }; export default function useAuthorization() { - const {data, mutate} = useSWR(STORAGE_KEY, authorizationFetcher, { - suspense: true, - }); - const setAuthorization = useCallback( - (authorizationResult: AuthorizationResult | null) => { - mutate( - async () => { - if (authorizationResult) { - await AsyncStorage.setItem( - STORAGE_KEY, - JSON.stringify(authorizationResult), - ); - return getDataFromAuthorizationResult(authorizationResult); - } else { - await AsyncStorage.removeItem(STORAGE_KEY); - return null; - } - }, - { - optimisticData: authorizationResult - ? getDataFromAuthorizationResult(authorizationResult) - : null, - }, - ); - }, - [mutate], + const {data: account, mutate} = useSWR( + STORAGE_KEY, ); const authorizeSession = useCallback( async (wallet: AuthorizeAPI & ReauthorizeAPI) => { - const authorizationResult = await (data + const authorizationResult = await (account ? wallet.reauthorize({ - auth_token: data.authorization.auth_token, + auth_token: account.authToken, }) : wallet.authorize({ cluster: 'devnet', identity: APP_IDENTITY, })); - setAuthorization(authorizationResult); - return getAccountFromAuthorizationResult(authorizationResult); + return await mutate( + getAccountFromAuthorizationResult(authorizationResult), + ); }, - [data, setAuthorization], + [account, mutate], ); const deauthorizeSession = useCallback( async (wallet: DeauthorizeAPI) => { - if (data?.authorization.auth_token == null) { + if (account?.authToken == null) { return; } - await wallet.deauthorize({auth_token: data.authorization.auth_token}); - setAuthorization(null); + await wallet.deauthorize({auth_token: account?.authToken}); + mutate(null); }, - [data?.authorization.auth_token, setAuthorization], + [account, mutate], ); return { - account: data?.account ?? null, + account: account ?? null, authorizeSession, deauthorizeSession, };