Skip to content

Commit 7c93737

Browse files
authored
feature(wallet-mobile): new tx review for collateral (#3683)
1 parent 4542ca9 commit 7c93737

File tree

25 files changed

+1045
-538
lines changed

25 files changed

+1045
-538
lines changed

apps/wallet-mobile/.storybook/storybook.requires.js

-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/wallet-mobile/src/YoroiApp.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {ErrorBoundary} from './components/ErrorBoundary/ErrorBoundary'
1515
import {AuthProvider} from './features/Auth/AuthProvider'
1616
import {BrowserProvider} from './features/Discover/common/BrowserProvider'
1717
import {PortfolioTokenActivityProvider} from './features/Portfolio/common/PortfolioTokenActivityProvider'
18+
import {ReviewTxProvider} from './features/ReviewTx/common/ReviewTxProvider'
1819
import {CurrencyProvider} from './features/Settings/Currency/CurrencyContext'
1920
import {AutomaticWalletOpenerProvider} from './features/WalletManager/context/AutomaticWalletOpeningProvider'
2021
import {WalletManagerProvider} from './features/WalletManager/context/WalletManagerProvider'
@@ -65,7 +66,9 @@ const Yoroi = () => {
6566
<PoolTransitionProvider>
6667
<BrowserProvider>
6768
<AutomaticWalletOpenerProvider>
68-
<InitApp />
69+
<ReviewTxProvider>
70+
<InitApp />
71+
</ReviewTxProvider>
6972
</AutomaticWalletOpenerProvider>
7073
</BrowserProvider>
7174
</PoolTransitionProvider>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import {castDraft, produce} from 'immer'
2+
import _ from 'lodash'
3+
import React from 'react'
4+
5+
import {YoroiSignedTx, YoroiUnsignedTx} from '../../../yoroi-wallets/types/yoroi'
6+
7+
export const useReviewTx = () => React.useContext(ReviewTxContext)
8+
9+
export const ReviewTxProvider = ({
10+
children,
11+
initialState,
12+
}: {
13+
children: React.ReactNode
14+
initialState?: Partial<ReviewTxState>
15+
}) => {
16+
const [state, dispatch] = React.useReducer(reviewTxReducer, {
17+
...defaultState,
18+
...initialState,
19+
})
20+
21+
const actions = React.useRef<ReviewTxActions>({
22+
unsignedTxChanged: (unsignedTx: ReviewTxState['unsignedTx']) =>
23+
dispatch({type: ReviewTxActionType.UnsignedTxChanged, unsignedTx}),
24+
cborChanged: (cbor: ReviewTxState['cbor']) => dispatch({type: ReviewTxActionType.CborChanged, cbor}),
25+
operationsChanged: (operations: ReviewTxState['operations']) =>
26+
dispatch({type: ReviewTxActionType.OperationsChanged, operations}),
27+
onSuccessChanged: (onSuccess: ReviewTxState['onSuccess']) =>
28+
dispatch({type: ReviewTxActionType.OnSuccessChanged, onSuccess}),
29+
onErrorChanged: (onError: ReviewTxState['onError']) => dispatch({type: ReviewTxActionType.OnErrorChanged, onError}),
30+
}).current
31+
32+
const context = React.useMemo(
33+
() => ({
34+
...state,
35+
...actions,
36+
}),
37+
[state, actions],
38+
)
39+
40+
return <ReviewTxContext.Provider value={context}>{children}</ReviewTxContext.Provider>
41+
}
42+
43+
const reviewTxReducer = (state: ReviewTxState, action: ReviewTxAction) => {
44+
return produce(state, (draft) => {
45+
switch (action.type) {
46+
case ReviewTxActionType.UnsignedTxChanged:
47+
draft.unsignedTx = castDraft(action.unsignedTx)
48+
break
49+
50+
case ReviewTxActionType.CborChanged:
51+
draft.cbor = action.cbor
52+
break
53+
54+
case ReviewTxActionType.OperationsChanged:
55+
draft.operations = action.operations
56+
break
57+
58+
case ReviewTxActionType.OnSuccessChanged:
59+
draft.onSuccess = action.onSuccess
60+
break
61+
62+
case ReviewTxActionType.OnErrorChanged:
63+
draft.onError = action.onError
64+
break
65+
66+
default:
67+
throw new Error('[ReviewTxContext] invalid action')
68+
}
69+
})
70+
}
71+
72+
type ReviewTxAction =
73+
| {
74+
type: ReviewTxActionType.UnsignedTxChanged
75+
unsignedTx: ReviewTxState['unsignedTx']
76+
}
77+
| {
78+
type: ReviewTxActionType.CborChanged
79+
cbor: ReviewTxState['cbor']
80+
}
81+
| {
82+
type: ReviewTxActionType.OperationsChanged
83+
operations: ReviewTxState['operations']
84+
}
85+
| {
86+
type: ReviewTxActionType.OnSuccessChanged
87+
onSuccess: ReviewTxState['onSuccess']
88+
}
89+
| {
90+
type: ReviewTxActionType.OnErrorChanged
91+
onError: ReviewTxState['onError']
92+
}
93+
94+
export type ReviewTxState = {
95+
unsignedTx: YoroiUnsignedTx | null
96+
cbor: string | null
97+
operations: Array<React.ReactNode> | null
98+
onSuccess: ((signedTx: YoroiSignedTx) => void) | null
99+
onError: (() => void) | null
100+
}
101+
102+
type ReviewTxActions = {
103+
unsignedTxChanged: (unsignedTx: ReviewTxState['unsignedTx']) => void
104+
cborChanged: (cbor: ReviewTxState['cbor']) => void
105+
operationsChanged: (operations: ReviewTxState['operations']) => void
106+
onSuccessChanged: (onSuccess: ReviewTxState['onSuccess']) => void
107+
onErrorChanged: (onError: ReviewTxState['onError']) => void
108+
}
109+
110+
const defaultState: ReviewTxState = Object.freeze({
111+
unsignedTx: null,
112+
cbor: null,
113+
operations: null,
114+
onSuccess: null,
115+
onError: null,
116+
})
117+
118+
function missingInit() {
119+
console.error('[ReviewTxContext] missing initialization')
120+
}
121+
122+
const initialReviewTxContext: ReviewTxContext = {
123+
...defaultState,
124+
unsignedTxChanged: missingInit,
125+
cborChanged: missingInit,
126+
operationsChanged: missingInit,
127+
onSuccessChanged: missingInit,
128+
onErrorChanged: missingInit,
129+
}
130+
131+
enum ReviewTxActionType {
132+
UnsignedTxChanged = 'unsignedTxChanged',
133+
CborChanged = 'cborChanged',
134+
OperationsChanged = 'operationsChanged',
135+
OnSuccessChanged = 'onSuccessChanged',
136+
OnErrorChanged = 'onErrorChanged',
137+
}
138+
139+
type ReviewTxContext = ReviewTxState & ReviewTxActions
140+
141+
const ReviewTxContext = React.createContext<ReviewTxContext>(initialReviewTxContext)

apps/wallet-mobile/src/features/ReviewTx/common/hooks/useOnConfirm.tsx

+8-8
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export const useOnConfirm = ({
1515
onError,
1616
onNotSupportedCIP1694,
1717
}: {
18-
onSuccess: (txId: YoroiSignedTx) => void
19-
onError: () => void
18+
onSuccess?: ((txId: YoroiSignedTx) => void) | null
19+
onError?: (() => void) | null
2020
cbor?: string
2121
unsignedTx?: YoroiUnsignedTx
2222
onNotSupportedCIP1694?: () => void
@@ -34,7 +34,7 @@ export const useOnConfirm = ({
3434
<ConfirmTxWithHwModal
3535
onCancel={closeModal}
3636
unsignedTx={unsignedTx}
37-
onSuccess={(signedTx) => onSuccess(signedTx)}
37+
onSuccess={(signedTx) => onSuccess?.(signedTx)}
3838
onNotSupportedCIP1694={onNotSupportedCIP1694}
3939
/>,
4040
400,
@@ -47,8 +47,8 @@ export const useOnConfirm = ({
4747
strings.signTransaction,
4848
<ConfirmTxWithSpendingPasswordModal
4949
unsignedTx={unsignedTx}
50-
onSuccess={(signedTx) => onSuccess(signedTx)}
51-
onError={onError}
50+
onSuccess={(signedTx) => onSuccess?.(signedTx)}
51+
onError={onError ?? undefined}
5252
/>,
5353
)
5454
return
@@ -59,13 +59,13 @@ export const useOnConfirm = ({
5959
strings.signTransaction,
6060
<ConfirmTxWithOsModal
6161
unsignedTx={unsignedTx}
62-
onSuccess={(signedTx) => onSuccess(signedTx)}
63-
onError={onError}
62+
onSuccess={(signedTx) => onSuccess?.(signedTx)}
63+
onError={onError ?? undefined}
6464
/>,
6565
)
6666
return
6767
}
6868
}
6969

70-
return {onConfirm}
70+
return {onConfirm} as const
7171
}

apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Overview/OverviewTab.tsx

+30-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ import {CopiableText} from '../../../common/CopiableText'
1616
import {Divider} from '../../../common/Divider'
1717
import {useAddressType} from '../../../common/hooks/useAddressType'
1818
import {useStrings} from '../../../common/hooks/useStrings'
19+
import {ReviewTxState} from '../../../common/ReviewTxProvider'
1920
import {TokenItem} from '../../../common/TokenItem'
2021
import {FormattedOutputs, FormattedTx} from '../../../common/types'
2122

22-
export const OverviewTab = ({tx}: {tx: FormattedTx}) => {
23+
export const OverviewTab = ({tx, operations}: {tx: FormattedTx; operations: ReviewTxState['operations']}) => {
2324
const {styles} = useStyles()
2425

2526
const notOwnedOutputs = React.useMemo(() => tx.outputs.filter((output) => !output.ownAddress), [tx.outputs])
@@ -34,6 +35,8 @@ export const OverviewTab = ({tx}: {tx: FormattedTx}) => {
3435
<Divider verticalSpace="lg" />
3536

3637
<SenderSection tx={tx} notOwnedOutputs={notOwnedOutputs} ownedOutputs={ownedOutputs} />
38+
39+
<OperationsSection operations={operations} />
3740
</View>
3841
)
3942
}
@@ -192,6 +195,32 @@ const ReceiverSection = ({notOwnedOutputs}: {notOwnedOutputs: FormattedOutputs})
192195
)
193196
}
194197

198+
const OperationsSection = ({operations}: {operations: ReviewTxState['operations']}) => {
199+
if (operations === null || (Array.isArray(operations) && operations.length === 0)) return null
200+
201+
return (
202+
<View>
203+
<Divider verticalSpace="lg" />
204+
205+
<Accordion label="Operations">
206+
<Space height="lg" />
207+
208+
{operations.map((operation, index) => {
209+
if (index === 0) return operation
210+
211+
return (
212+
<>
213+
<Space height="sm" />
214+
215+
{operation}
216+
</>
217+
)
218+
})}
219+
</Accordion>
220+
</View>
221+
)
222+
}
223+
195224
const useStyles = () => {
196225
const {atoms, color} = useTheme()
197226
const styles = StyleSheet.create({

apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx

+9-8
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import {FlatList, ScrollView, StyleSheet, Text, TouchableOpacity, TouchableOpaci
55

66
import {Button} from '../../../../components/Button/Button'
77
import {SafeArea} from '../../../../components/SafeArea'
8-
import {ReviewTxRoutes, useUnsafeParams} from '../../../../kernel/navigation'
98
import {useFormattedTx} from '../../common/hooks/useFormattedTx'
109
import {useOnConfirm} from '../../common/hooks/useOnConfirm'
1110
import {useStrings} from '../../common/hooks/useStrings'
1211
import {useTxBody} from '../../common/hooks/useTxBody'
12+
import {useReviewTx} from '../../common/ReviewTxProvider'
1313
import {OverviewTab} from './Overview/OverviewTab'
1414
import {UTxOsTab} from './UTxOs/UTxOsTab'
1515

@@ -18,20 +18,21 @@ const MaterialTab = createMaterialTopTabNavigator()
1818
export const ReviewTxScreen = () => {
1919
const {styles} = useStyles()
2020
const strings = useStrings()
21+
const {unsignedTx, operations, onSuccess, onError} = useReviewTx()
22+
23+
if (unsignedTx === null) throw new Error('ReviewTxScreen: missing unsignedTx')
2124

22-
// TODO: move this to a context
23-
const params = useUnsafeParams<ReviewTxRoutes['review-tx']>()
2425
const {onConfirm} = useOnConfirm({
25-
unsignedTx: params.unsignedTx,
26-
onSuccess: params.onSuccess,
27-
onError: params.onError,
26+
unsignedTx,
27+
onSuccess,
28+
onError,
2829
})
2930

3031
// TODO: add cbor arguments
31-
const txBody = useTxBody({unsignedTx: params.unsignedTx})
32+
const txBody = useTxBody({unsignedTx})
3233
const formatedTx = useFormattedTx(txBody)
3334

34-
const OverViewTabMemo = React.memo(() => <OverviewTab tx={formatedTx} />)
35+
const OverViewTabMemo = React.memo(() => <OverviewTab tx={formatedTx} operations={operations} />)
3536
const UTxOsTabMemo = React.memo(() => <UTxOsTab tx={formatedTx} />)
3637

3738
return (

apps/wallet-mobile/src/features/Send/useCases/ListAmountsToSend/ListAmountsToSendScreen.tsx

+7-10
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {useWalletNavigation} from '../../../../kernel/navigation'
2222
import {useSaveMemo} from '../../../../yoroi-wallets/hooks'
2323
import {YoroiEntry, YoroiSignedTx} from '../../../../yoroi-wallets/types/yoroi'
2424
import {TokenAmountItem} from '../../../Portfolio/common/TokenAmountItem/TokenAmountItem'
25+
import {useReviewTx} from '../../../ReviewTx/common/ReviewTxProvider'
2526
import {useSearch} from '../../../Search/SearchContext'
2627
import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet'
2728
import {useNavigateTo, useOverridePreviousSendTxRoute} from '../../common/navigation'
@@ -38,21 +39,15 @@ export const ListAmountsToSendScreen = () => {
3839
const navigation = useNavigation()
3940
const {track} = useMetrics()
4041
const {wallet} = useSelectedWallet()
42+
const {unsignedTxChanged, onSuccessChanged, onErrorChanged} = useReviewTx()
4143

4244
useOverridePreviousSendTxRoute('send-start-tx')
4345

4446
useLayoutEffect(() => {
4547
navigation.setOptions({headerLeft: () => <ListAmountsNavigateBackButton />})
4648
}, [navigation])
4749

48-
const {
49-
memo,
50-
targets,
51-
selectedTargetIndex,
52-
tokenSelectedChanged,
53-
amountRemoved,
54-
unsignedTxChanged: yoroiUnsignedTxChanged,
55-
} = useTransfer()
50+
const {memo, targets, selectedTargetIndex, tokenSelectedChanged, amountRemoved} = useTransfer()
5651
const {saveMemo} = useSaveMemo({wallet})
5752
const {amounts} = targets[selectedTargetIndex].entry
5853
const selectedTokensCounter = Object.keys(amounts).length
@@ -108,8 +103,10 @@ export const ListAmountsToSendScreen = () => {
108103
// NOTE: update on multi target support
109104
createUnsignedTx([toYoroiEntry(targets[selectedTargetIndex].entry)], {
110105
onSuccess: (yoroiUnsignedTx) => {
111-
yoroiUnsignedTxChanged(yoroiUnsignedTx)
112-
navigateToTxReview({unsignedTx: yoroiUnsignedTx, onSuccess, onError})
106+
unsignedTxChanged(yoroiUnsignedTx)
107+
onSuccessChanged(onSuccess)
108+
onErrorChanged(onError)
109+
navigateToTxReview()
113110
},
114111
})
115112
}

0 commit comments

Comments
 (0)