react-native-fpay 0.3.13 → 0.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/module/FountainPayProvider.js +2 -2
- package/lib/module/FountainPayProvider.js.map +1 -1
- package/lib/module/core/api/client.js +1 -1
- package/lib/module/core/api/index.js +3 -3
- package/lib/module/core/api/index.js.map +1 -1
- package/lib/module/ui/modals/FPPaymentRequestModal.js +0 -1
- package/lib/module/ui/modals/FPPaymentRequestModal.js.map +1 -1
- package/lib/module/ui/modals/FPShell.js +0 -1
- package/lib/module/ui/modals/FPShell.js.map +1 -1
- package/lib/module/ui/screens/ResultScreen.js +321 -0
- package/lib/module/ui/screens/ResultScreen.js.map +1 -0
- package/lib/module/ui/screens/SendScreen.js +30 -134
- package/lib/module/ui/screens/SendScreen.js.map +1 -1
- package/lib/module/ui/theme/index.js +1 -1
- package/lib/typescript/src/core/api/index.d.ts +6 -2
- package/lib/typescript/src/core/api/index.d.ts.map +1 -1
- package/lib/typescript/src/ui/components/OtpInput/Styles.d.ts +21 -21
- package/lib/typescript/src/ui/modals/FPPaymentRequestModal.d.ts.map +1 -1
- package/lib/typescript/src/ui/modals/FPShell.d.ts.map +1 -1
- package/lib/typescript/src/ui/screens/ResultScreen.d.ts +8 -0
- package/lib/typescript/src/ui/screens/ResultScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/SendScreen.d.ts.map +1 -1
- package/lib/typescript/src/ui/screens/styles.d.ts +82 -82
- package/lib/typescript/src/ui/screens/sub/sendPayment/BluetoothSubScreen.d.ts +22 -22
- package/lib/typescript/src/ui/screens/sub/sendPayment/ProximitySubScreen.d.ts +22 -22
- package/package.json +1 -1
- package/src/FountainPayProvider.tsx +2 -2
- package/src/core/api/client.ts +1 -1
- package/src/core/api/index.ts +3 -3
- package/src/ui/modals/FPPaymentRequestModal.tsx +1 -5
- package/src/ui/modals/FPShell.tsx +1 -6
- package/src/ui/screens/ResultScreen.tsx +291 -0
- package/src/ui/screens/SendScreen.tsx +37 -173
- package/src/ui/theme/index.ts +1 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Animated, Easing, TouchableOpacity } from 'react-native';
|
|
3
|
+
import Svg, { Path, Circle } from 'react-native-svg';
|
|
4
|
+
import styled from 'styled-components/native';
|
|
5
|
+
import { C, F, R, S } from '../theme';
|
|
6
|
+
import type { FPTransaction } from '../../core/types';
|
|
7
|
+
import { transferAPI } from '../../core/api';
|
|
8
|
+
|
|
9
|
+
// ── Icons ─────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const SuccessIcon = () => (
|
|
12
|
+
<Svg width={64} height={64} viewBox="0 0 64 64" fill="none">
|
|
13
|
+
<Circle cx="32" cy="32" r="32" fill="#00875A" />
|
|
14
|
+
<Path d="M20 33L28 41L44 24" stroke="#fff" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
15
|
+
</Svg>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const FailedIcon = () => (
|
|
19
|
+
<Svg width={64} height={64} viewBox="0 0 64 64" fill="none">
|
|
20
|
+
<Circle cx="32" cy="32" r="32" fill="#DE350B" />
|
|
21
|
+
<Path d="M22 22L42 42M42 22L22 42" stroke="#fff" strokeWidth="3.5" strokeLinecap="round" />
|
|
22
|
+
</Svg>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// ── Styled Components ─────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const Container = styled(Animated.View)`
|
|
28
|
+
flex: 1;
|
|
29
|
+
background-color: ${C.white};
|
|
30
|
+
padding: ${S.xxl}px ${S.lg}px ${S.xl}px;
|
|
31
|
+
align-items: center;
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const IconWrap = styled(Animated.View)`
|
|
35
|
+
margin-bottom: ${S.lg}px;
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const IconBg = styled.View<{ bg: string }>`
|
|
39
|
+
width: 112px;
|
|
40
|
+
height: 112px;
|
|
41
|
+
border-radius: 56px;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
align-items: center;
|
|
44
|
+
background-color: ${({ bg }) => bg};
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const StatusText = styled.Text<{ color: string }>`
|
|
48
|
+
font-size: ${F.xl}px;
|
|
49
|
+
font-weight: 800;
|
|
50
|
+
margin-bottom: ${S.sm}px;
|
|
51
|
+
text-align: center;
|
|
52
|
+
color: ${({ color }) => color};
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const Amount = styled.Text`
|
|
56
|
+
font-size: ${F.hero}px;
|
|
57
|
+
font-weight: 900;
|
|
58
|
+
color: ${C.ink};
|
|
59
|
+
margin-bottom: ${S.xl}px;
|
|
60
|
+
letter-spacing: -1px;
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const Card = styled.View`
|
|
64
|
+
width: 100%;
|
|
65
|
+
background-color: ${C.surface};
|
|
66
|
+
border-radius: ${R.xl}px;
|
|
67
|
+
padding: ${S.md}px ${S.lg}px;
|
|
68
|
+
margin-bottom: ${S.xl}px;
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
const RowWrap = styled.View`
|
|
72
|
+
flex-direction: row;
|
|
73
|
+
justify-content: space-between;
|
|
74
|
+
align-items: center;
|
|
75
|
+
padding: ${S.sm}px 0;
|
|
76
|
+
border-bottom-width: 1px;
|
|
77
|
+
border-bottom-color: ${C.border};
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
const RowLabel = styled.Text`
|
|
81
|
+
font-size: ${F.sm}px;
|
|
82
|
+
color: ${C.muted};
|
|
83
|
+
font-weight: 500;
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
const RowValue = styled.Text`
|
|
87
|
+
font-size: ${F.sm}px;
|
|
88
|
+
color: ${C.ink};
|
|
89
|
+
font-weight: 700;
|
|
90
|
+
max-width: 60%;
|
|
91
|
+
text-align: right;
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
const Footer = styled.View`
|
|
95
|
+
flex-direction: row;
|
|
96
|
+
align-items: center;
|
|
97
|
+
margin-top: auto;
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
const CloseBtn = styled(TouchableOpacity)`
|
|
101
|
+
flex: 1;
|
|
102
|
+
background-color: ${C.brand};
|
|
103
|
+
border-radius: ${R.full}px;
|
|
104
|
+
padding: ${S.md}px 0;
|
|
105
|
+
align-items: center;
|
|
106
|
+
margin-left: ${S.md}px;
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
const CloseBtnText = styled.Text`
|
|
110
|
+
color: ${C.white};
|
|
111
|
+
font-weight: 800;
|
|
112
|
+
font-size: ${F.md}px;
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
const RingWrap = styled.View`
|
|
116
|
+
width: 48px;
|
|
117
|
+
height: 48px;
|
|
118
|
+
justify-content: center;
|
|
119
|
+
align-items: center;
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
const RingLabel = styled.Text`
|
|
123
|
+
position: absolute;
|
|
124
|
+
font-size: 8px;
|
|
125
|
+
color: ${C.muted};
|
|
126
|
+
text-align: center;
|
|
127
|
+
font-weight: 600;
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
// ── Countdown ────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
const COUNTDOWN_SECONDS = 5;
|
|
133
|
+
const RING_SIZE = 48;
|
|
134
|
+
const STROKE_WIDTH = 3;
|
|
135
|
+
const RADIUS = (RING_SIZE - STROKE_WIDTH) / 2;
|
|
136
|
+
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
|
137
|
+
|
|
138
|
+
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
139
|
+
|
|
140
|
+
function CountdownRing({ duration }: { duration: number }) {
|
|
141
|
+
const progress = useRef(new Animated.Value(0)).current;
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
Animated.timing(progress, {
|
|
145
|
+
toValue: 1,
|
|
146
|
+
duration,
|
|
147
|
+
easing: Easing.linear,
|
|
148
|
+
useNativeDriver: false,
|
|
149
|
+
}).start();
|
|
150
|
+
}, [duration, progress]);
|
|
151
|
+
|
|
152
|
+
const strokeDashoffset = progress.interpolate({
|
|
153
|
+
inputRange: [0, 1],
|
|
154
|
+
outputRange: [0, CIRCUMFERENCE],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<RingWrap>
|
|
159
|
+
<Svg width={RING_SIZE} height={RING_SIZE}>
|
|
160
|
+
<Circle
|
|
161
|
+
cx={RING_SIZE / 2}
|
|
162
|
+
cy={RING_SIZE / 2}
|
|
163
|
+
r={RADIUS}
|
|
164
|
+
stroke={C.border}
|
|
165
|
+
strokeWidth={STROKE_WIDTH}
|
|
166
|
+
fill="none"
|
|
167
|
+
/>
|
|
168
|
+
<AnimatedCircle
|
|
169
|
+
cx={RING_SIZE / 2}
|
|
170
|
+
cy={RING_SIZE / 2}
|
|
171
|
+
r={RADIUS}
|
|
172
|
+
stroke={C.brand}
|
|
173
|
+
strokeWidth={STROKE_WIDTH}
|
|
174
|
+
fill="none"
|
|
175
|
+
strokeDasharray={`${CIRCUMFERENCE} ${CIRCUMFERENCE}`}
|
|
176
|
+
strokeDashoffset={strokeDashoffset}
|
|
177
|
+
strokeLinecap="round"
|
|
178
|
+
rotation="-90"
|
|
179
|
+
origin={`${RING_SIZE / 2}, ${RING_SIZE / 2}`}
|
|
180
|
+
/>
|
|
181
|
+
</Svg>
|
|
182
|
+
<RingLabel>{`Auto\nclose`}</RingLabel>
|
|
183
|
+
</RingWrap>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Row ──────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
function Row({ label, value }: { label: string; value: string }) {
|
|
190
|
+
return (
|
|
191
|
+
<RowWrap>
|
|
192
|
+
<RowLabel>{label}</RowLabel>
|
|
193
|
+
<RowValue>{value}</RowValue>
|
|
194
|
+
</RowWrap>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Main ─────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
interface Props {
|
|
201
|
+
transaction: FPTransaction | any;
|
|
202
|
+
onClose: () => void;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function ResultScreen({ transaction, onClose }: Props) {
|
|
206
|
+
const [transactionDetail, setTransactionDetail] = useState<any>(null)
|
|
207
|
+
const slideAnim = useRef(new Animated.Value(60)).current;
|
|
208
|
+
const opacAnim = useRef(new Animated.Value(0)).current;
|
|
209
|
+
const scaleAnim = useRef(new Animated.Value(0.5)).current;
|
|
210
|
+
|
|
211
|
+
const isSuccess = transactionDetail?.status.toLowerCase() === 'successful';
|
|
212
|
+
const accentColor = isSuccess ? C.green : C.red;
|
|
213
|
+
const bgColor = isSuccess ? '#E3FCEF' : '#FFEBE6';
|
|
214
|
+
|
|
215
|
+
const formatted = `${transactionDetail?.currency || 'NGN'} ${Number(transactionDetail?.amount).toLocaleString('en-NG', {
|
|
216
|
+
minimumFractionDigits: 2,
|
|
217
|
+
})}`;
|
|
218
|
+
|
|
219
|
+
const recipient = transaction?.recipient as any;
|
|
220
|
+
const recipientName = recipient?.accountName ?? recipient?.name ?? '—';
|
|
221
|
+
|
|
222
|
+
const channelLabel: Record<string, string> = {
|
|
223
|
+
transfer: 'Bank Transfer',
|
|
224
|
+
bluetooth: 'Bluetooth',
|
|
225
|
+
proximity: 'Nearby',
|
|
226
|
+
nqr: 'QR Code',
|
|
227
|
+
nfc: 'NFC Tap',
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const dateStr = transactionDetail?.createdAt
|
|
231
|
+
? new Date(transactionDetail?.createdAt).toLocaleString('en-NG')
|
|
232
|
+
: '—';
|
|
233
|
+
|
|
234
|
+
const loadTransaction = async()=>{
|
|
235
|
+
const response = await transferAPI.status(transaction.reference) as any;
|
|
236
|
+
console.log("Transaction payload: ", response);
|
|
237
|
+
if(response.status){
|
|
238
|
+
setTransactionDetail(response.payload);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
Animated.parallel([
|
|
244
|
+
Animated.spring(scaleAnim, { toValue: 1, useNativeDriver: true }),
|
|
245
|
+
Animated.timing(opacAnim, { toValue: 1, duration: 300, useNativeDriver: true }),
|
|
246
|
+
Animated.timing(slideAnim, { toValue: 0, duration: 350, useNativeDriver: true }),
|
|
247
|
+
]).start();
|
|
248
|
+
}, [scaleAnim, opacAnim, slideAnim]);
|
|
249
|
+
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
const timer = setTimeout(onClose, COUNTDOWN_SECONDS * 1000);
|
|
252
|
+
return () => clearTimeout(timer);
|
|
253
|
+
}, [onClose]);
|
|
254
|
+
|
|
255
|
+
useEffect(()=>{
|
|
256
|
+
if(transaction){
|
|
257
|
+
loadTransaction();
|
|
258
|
+
}
|
|
259
|
+
}, [transaction])
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<Container style={{ opacity: opacAnim, transform: [{ translateY: slideAnim }] }}>
|
|
263
|
+
<IconWrap style={{ transform: [{ scale: scaleAnim }] }}>
|
|
264
|
+
<IconBg bg={bgColor}>
|
|
265
|
+
{isSuccess ? <SuccessIcon /> : <FailedIcon />}
|
|
266
|
+
</IconBg>
|
|
267
|
+
</IconWrap>
|
|
268
|
+
|
|
269
|
+
<StatusText color={accentColor}>
|
|
270
|
+
{isSuccess ? 'Payment Successful' : 'Payment Failed'}
|
|
271
|
+
</StatusText>
|
|
272
|
+
|
|
273
|
+
<Amount>{formatted}</Amount>
|
|
274
|
+
|
|
275
|
+
<Card>
|
|
276
|
+
<Row label="To" value={recipientName} />
|
|
277
|
+
<Row label="Channel" value={channelLabel[transactionDetail?.channel] ?? transaction?.channel} />
|
|
278
|
+
<Row label="Reference" value={transactionDetail?.reference} />
|
|
279
|
+
<Row label="Date" value={dateStr} />
|
|
280
|
+
<Row label="Status" value={transactionDetail?.status?.toUpperCase()} />
|
|
281
|
+
</Card>
|
|
282
|
+
|
|
283
|
+
<Footer>
|
|
284
|
+
<CountdownRing duration={COUNTDOWN_SECONDS * 1000} />
|
|
285
|
+
<CloseBtn onPress={loadTransaction} activeOpacity={0.8}>
|
|
286
|
+
<CloseBtnText>Close</CloseBtnText>
|
|
287
|
+
</CloseBtn>
|
|
288
|
+
</Footer>
|
|
289
|
+
</Container>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
@@ -3,18 +3,14 @@
|
|
|
3
3
|
// Shows channel selection, then opens the
|
|
4
4
|
// relevant sub-screen in the same modal sheet
|
|
5
5
|
// ─────────────────────────────────────────────
|
|
6
|
-
import
|
|
6
|
+
import { useState } from 'react';
|
|
7
7
|
import {
|
|
8
8
|
View,
|
|
9
9
|
Text,
|
|
10
10
|
TouchableOpacity,
|
|
11
|
-
StyleSheet,
|
|
12
11
|
ScrollView,
|
|
13
12
|
Dimensions,
|
|
14
|
-
TextInput,
|
|
15
13
|
} from 'react-native';
|
|
16
|
-
import { C, R, S, F, shadow } from '../theme';
|
|
17
|
-
import { FPButton } from '../components/FPButton';
|
|
18
14
|
import { TransferSubScreen } from './sub/sendPayment/TransferSubScreen';
|
|
19
15
|
import { NQRSubScreen } from './sub/sendPayment/NQRSubScreen';
|
|
20
16
|
import { ProximitySubScreen } from './sub/sendPayment/ProximitySubScreen';
|
|
@@ -29,27 +25,17 @@ import type {
|
|
|
29
25
|
FPUserInfo,
|
|
30
26
|
FPTransferRecipient,
|
|
31
27
|
FPSendWalletPaymentRequest,
|
|
32
|
-
FPTransactionResponse,
|
|
33
28
|
} from '../../core/types';
|
|
34
29
|
import styled from 'styled-components/native';
|
|
35
30
|
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
36
31
|
import Svg, { Path } from 'react-native-svg';
|
|
37
32
|
import { nfcAPI, nqrAPI, transferAPI } from '../../core/api';
|
|
38
|
-
import {
|
|
39
|
-
ButtonContainer,
|
|
40
|
-
ContentContainer,
|
|
41
|
-
CTAButton,
|
|
42
|
-
CTAText,
|
|
43
|
-
InputBox,
|
|
44
|
-
InputContainer,
|
|
45
|
-
StyledTextInput,
|
|
46
|
-
} from './styles';
|
|
47
|
-
import OTPInputs from '../components/OtpInput';
|
|
48
33
|
import ConfirmScreen from '../components/ConfirmScreen';
|
|
49
34
|
import LoadingAnimation from '../components/LoadingAnimation';
|
|
50
35
|
import { getFPStore } from '../../store/FPStore';
|
|
36
|
+
import { ResultScreen } from './ResultScreen';
|
|
51
37
|
|
|
52
|
-
const {
|
|
38
|
+
const { height } = Dimensions.get('window');
|
|
53
39
|
|
|
54
40
|
type Channel = 'TRANSFER' | 'QRCODE' | 'NFC' | 'BTH' | 'PXTR';
|
|
55
41
|
|
|
@@ -150,143 +136,6 @@ const ActionLabel = styled(Text)`
|
|
|
150
136
|
line-height: 14px;
|
|
151
137
|
`;
|
|
152
138
|
|
|
153
|
-
const SearchContainer = styled(View)`
|
|
154
|
-
flex-direction: row;
|
|
155
|
-
align-items: center;
|
|
156
|
-
background-color: #f9fafb;
|
|
157
|
-
border-radius: 12px;
|
|
158
|
-
padding: 14px 16px;
|
|
159
|
-
margin-bottom: 24px;
|
|
160
|
-
border: 1px solid #e5e7eb;
|
|
161
|
-
`;
|
|
162
|
-
|
|
163
|
-
const SearchInput = styled(TextInput)`
|
|
164
|
-
flex: 1;
|
|
165
|
-
margin-left: 10px;
|
|
166
|
-
font-size: 14px;
|
|
167
|
-
color: #1f2937;
|
|
168
|
-
padding: 0;
|
|
169
|
-
`;
|
|
170
|
-
|
|
171
|
-
const AddContactButton = styled.TouchableOpacity`
|
|
172
|
-
align-items: center;
|
|
173
|
-
margin-right: 20px;
|
|
174
|
-
|
|
175
|
-
flex-direction: row;
|
|
176
|
-
gap: 20px;
|
|
177
|
-
`;
|
|
178
|
-
|
|
179
|
-
const Icon = styled(Text)<{ size?: number; color?: string }>`
|
|
180
|
-
font-size: ${(props) => props.size || 24}px;
|
|
181
|
-
color: ${(props) => props.color || '#000000'};
|
|
182
|
-
`;
|
|
183
|
-
|
|
184
|
-
const AddCircle = styled.View`
|
|
185
|
-
width: 44px;
|
|
186
|
-
height: 44px;
|
|
187
|
-
border-radius: 32px;
|
|
188
|
-
background-color: #fff;
|
|
189
|
-
border-width: 2px;
|
|
190
|
-
border-color: #e0e0e0;
|
|
191
|
-
border-style: dashed;
|
|
192
|
-
justify-content: center;
|
|
193
|
-
align-items: center;
|
|
194
|
-
margin-bottom: 8px;
|
|
195
|
-
`;
|
|
196
|
-
|
|
197
|
-
const PrxyIconContainer = styled(View)<{ color: string }>`
|
|
198
|
-
width: 44px;
|
|
199
|
-
height: 44px;
|
|
200
|
-
border-radius: 100%;
|
|
201
|
-
background-color: ${(props) => props.color};
|
|
202
|
-
justify-content: center;
|
|
203
|
-
align-items: center;
|
|
204
|
-
elevation: 3;
|
|
205
|
-
shadow-color: #000;
|
|
206
|
-
shadow-offset: 0px 2px;
|
|
207
|
-
shadow-opacity: 0.12;
|
|
208
|
-
shadow-radius: 4px;
|
|
209
|
-
`;
|
|
210
|
-
|
|
211
|
-
const ContactName = styled.Text`
|
|
212
|
-
font-size: 12px;
|
|
213
|
-
color: #000;
|
|
214
|
-
`;
|
|
215
|
-
|
|
216
|
-
const ScanIcon = ({ color = '#111', size = 22 }) => (
|
|
217
|
-
<Svg
|
|
218
|
-
width={size}
|
|
219
|
-
height={size}
|
|
220
|
-
strokeWidth="0.9"
|
|
221
|
-
viewBox="0 0 24 24"
|
|
222
|
-
fill="none"
|
|
223
|
-
color={color}
|
|
224
|
-
>
|
|
225
|
-
<Path
|
|
226
|
-
d="M6 3H3V6"
|
|
227
|
-
stroke="#000000"
|
|
228
|
-
strokeWidth="0.9"
|
|
229
|
-
strokeLinecap="round"
|
|
230
|
-
strokeLinejoin="round"
|
|
231
|
-
/>
|
|
232
|
-
<Path
|
|
233
|
-
d="M2 12H12L22 12"
|
|
234
|
-
stroke="#000000"
|
|
235
|
-
strokeWidth="0.9"
|
|
236
|
-
strokeLinecap="round"
|
|
237
|
-
strokeLinejoin="round"
|
|
238
|
-
/>
|
|
239
|
-
<Path
|
|
240
|
-
d="M9 19V17V15"
|
|
241
|
-
stroke="#000000"
|
|
242
|
-
strokeWidth="0.9"
|
|
243
|
-
strokeLinecap="round"
|
|
244
|
-
strokeLinejoin="round"
|
|
245
|
-
/>
|
|
246
|
-
<Path
|
|
247
|
-
d="M12 16V15.5V15"
|
|
248
|
-
stroke="#000000"
|
|
249
|
-
strokeWidth="0.9"
|
|
250
|
-
strokeLinecap="round"
|
|
251
|
-
strokeLinejoin="round"
|
|
252
|
-
/>
|
|
253
|
-
<Path
|
|
254
|
-
d="M15 17V16V15"
|
|
255
|
-
stroke="#000000"
|
|
256
|
-
strokeWidth="0.9"
|
|
257
|
-
strokeLinecap="round"
|
|
258
|
-
strokeLinejoin="round"
|
|
259
|
-
/>
|
|
260
|
-
<Path
|
|
261
|
-
d="M12 21V19.5V18"
|
|
262
|
-
stroke="#000000"
|
|
263
|
-
strokeWidth="0.9"
|
|
264
|
-
strokeLinecap="round"
|
|
265
|
-
strokeLinejoin="round"
|
|
266
|
-
/>
|
|
267
|
-
<Path
|
|
268
|
-
d="M18 3H21V6"
|
|
269
|
-
stroke="#000000"
|
|
270
|
-
strokeWidth="0.9"
|
|
271
|
-
strokeLinecap="round"
|
|
272
|
-
strokeLinejoin="round"
|
|
273
|
-
/>
|
|
274
|
-
<Path
|
|
275
|
-
d="M6 21H3V18"
|
|
276
|
-
stroke="#000000"
|
|
277
|
-
strokeWidth="0.9"
|
|
278
|
-
strokeLinecap="round"
|
|
279
|
-
strokeLinejoin="round"
|
|
280
|
-
/>
|
|
281
|
-
<Path
|
|
282
|
-
d="M18 21H21V18"
|
|
283
|
-
stroke="#000000"
|
|
284
|
-
strokeWidth="0.9"
|
|
285
|
-
strokeLinecap="round"
|
|
286
|
-
strokeLinejoin="round"
|
|
287
|
-
/>
|
|
288
|
-
</Svg>
|
|
289
|
-
);
|
|
290
139
|
|
|
291
140
|
const BluetoothIcon = ({ color = '#111', size = 22 }) => (
|
|
292
141
|
<Svg
|
|
@@ -464,15 +313,11 @@ const SendScreen = ({
|
|
|
464
313
|
onError,
|
|
465
314
|
}: Props) => {
|
|
466
315
|
const [channel, setChannel] = useState<Channel>('TRANSFER');
|
|
467
|
-
const [transactionAmount, setTransactionAmount] = useState<string>('');
|
|
468
316
|
const [loading, setLoading] = useState<boolean>(false);
|
|
469
|
-
const [
|
|
470
|
-
const [
|
|
471
|
-
useState<boolean>(false);
|
|
317
|
+
const [showConfirmationModal, setShowConfirmationModal] = useState<boolean>(false);
|
|
318
|
+
const [completedTx, setCompletedTx] = useState<FPTransaction | null>(null);
|
|
472
319
|
|
|
473
|
-
const [transactionPayload, setTransactionPayload] = useState<
|
|
474
|
-
FPSendPaymentRequest | FPSendWalletPaymentRequest
|
|
475
|
-
>({} as FPSendPaymentRequest | FPSendWalletPaymentRequest);
|
|
320
|
+
const [transactionPayload, setTransactionPayload] = useState< FPSendPaymentRequest | FPSendWalletPaymentRequest>({} as FPSendPaymentRequest | FPSendWalletPaymentRequest);
|
|
476
321
|
|
|
477
322
|
const formatted = `${currency} ${amount.toLocaleString('en-NG', { minimumFractionDigits: 2 })}`;
|
|
478
323
|
|
|
@@ -488,26 +333,31 @@ const SendScreen = ({
|
|
|
488
333
|
setShowConfirmationModal(true);
|
|
489
334
|
};
|
|
490
335
|
|
|
336
|
+
const handleResultClose = () => {
|
|
337
|
+
setCompletedTx(null);
|
|
338
|
+
onClose(); // close the entire shell
|
|
339
|
+
};
|
|
340
|
+
|
|
491
341
|
const handleProcessingTransaction = async (temptId: string) => {
|
|
492
342
|
console.log('Got in here for processing');
|
|
493
343
|
try {
|
|
494
344
|
setLoading(true);
|
|
495
345
|
setShowConfirmationModal(false);
|
|
496
|
-
let response:
|
|
346
|
+
let response: any = null;
|
|
497
347
|
switch (transactionPayload?.channel) {
|
|
498
348
|
case 'transfer':
|
|
499
349
|
case 'bluetooth':
|
|
500
350
|
case 'proximity':
|
|
501
351
|
if ('isBank' in transactionPayload && transactionPayload.isBank) {
|
|
502
|
-
response = await transferAPI.sendToBank(
|
|
352
|
+
response = (await transferAPI.sendToBank(
|
|
503
353
|
transactionPayload as FPSendPaymentRequest,
|
|
504
354
|
temptId
|
|
505
|
-
);
|
|
355
|
+
)) || null;
|
|
506
356
|
} else {
|
|
507
|
-
response = await transferAPI.sendToWallet(
|
|
357
|
+
response = (await transferAPI.sendToWallet(
|
|
508
358
|
transactionPayload as FPSendWalletPaymentRequest,
|
|
509
359
|
temptId
|
|
510
|
-
);
|
|
360
|
+
)) || null;
|
|
511
361
|
}
|
|
512
362
|
break;
|
|
513
363
|
case 'nqr':
|
|
@@ -534,15 +384,19 @@ const SendScreen = ({
|
|
|
534
384
|
onError?.({ message: 'Invalid channel' } as FPError);
|
|
535
385
|
break;
|
|
536
386
|
}
|
|
537
|
-
|
|
538
|
-
if (response?.status) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
387
|
+
|
|
388
|
+
if (!response?.status) {
|
|
389
|
+
// Show failed result screen
|
|
390
|
+
setCompletedTx({
|
|
391
|
+
...(response?.payload ?? {}),
|
|
392
|
+
status: 'failed',
|
|
393
|
+
} as FPTransaction);
|
|
394
|
+
return; // ← don't call onError — let result screen handle it
|
|
543
395
|
}
|
|
544
396
|
|
|
545
|
-
|
|
397
|
+
// Success — show result screen, notify host app
|
|
398
|
+
setCompletedTx(response.payload as FPTransaction);
|
|
399
|
+
onPaymentSent?.(response.payload as FPTransaction);
|
|
546
400
|
} catch (error) {
|
|
547
401
|
onError?.(error as FPError);
|
|
548
402
|
} finally {
|
|
@@ -550,6 +404,16 @@ const SendScreen = ({
|
|
|
550
404
|
}
|
|
551
405
|
};
|
|
552
406
|
|
|
407
|
+
if (completedTx) {
|
|
408
|
+
console.log("Completed Tx: ", completedTx)
|
|
409
|
+
return (
|
|
410
|
+
<ResultScreen
|
|
411
|
+
transaction={{...transactionPayload, ...completedTx}}
|
|
412
|
+
onClose={handleResultClose}
|
|
413
|
+
/>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
553
417
|
return (
|
|
554
418
|
<Container>
|
|
555
419
|
{loading && <LoadingAnimation />}
|