react-native-fpay 0.3.13 → 0.3.14

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.
Files changed (34) hide show
  1. package/lib/module/FountainPayProvider.js +2 -2
  2. package/lib/module/FountainPayProvider.js.map +1 -1
  3. package/lib/module/core/api/client.js +1 -1
  4. package/lib/module/core/api/index.js +3 -3
  5. package/lib/module/core/api/index.js.map +1 -1
  6. package/lib/module/ui/modals/FPPaymentRequestModal.js +0 -1
  7. package/lib/module/ui/modals/FPPaymentRequestModal.js.map +1 -1
  8. package/lib/module/ui/modals/FPShell.js +0 -1
  9. package/lib/module/ui/modals/FPShell.js.map +1 -1
  10. package/lib/module/ui/screens/ResultScreen.js +321 -0
  11. package/lib/module/ui/screens/ResultScreen.js.map +1 -0
  12. package/lib/module/ui/screens/SendScreen.js +30 -134
  13. package/lib/module/ui/screens/SendScreen.js.map +1 -1
  14. package/lib/module/ui/theme/index.js +1 -1
  15. package/lib/typescript/src/core/api/index.d.ts +6 -2
  16. package/lib/typescript/src/core/api/index.d.ts.map +1 -1
  17. package/lib/typescript/src/ui/components/OtpInput/Styles.d.ts +21 -21
  18. package/lib/typescript/src/ui/modals/FPPaymentRequestModal.d.ts.map +1 -1
  19. package/lib/typescript/src/ui/modals/FPShell.d.ts.map +1 -1
  20. package/lib/typescript/src/ui/screens/ResultScreen.d.ts +8 -0
  21. package/lib/typescript/src/ui/screens/ResultScreen.d.ts.map +1 -0
  22. package/lib/typescript/src/ui/screens/SendScreen.d.ts.map +1 -1
  23. package/lib/typescript/src/ui/screens/styles.d.ts +82 -82
  24. package/lib/typescript/src/ui/screens/sub/sendPayment/BluetoothSubScreen.d.ts +22 -22
  25. package/lib/typescript/src/ui/screens/sub/sendPayment/ProximitySubScreen.d.ts +22 -22
  26. package/package.json +1 -1
  27. package/src/FountainPayProvider.tsx +2 -2
  28. package/src/core/api/client.ts +1 -1
  29. package/src/core/api/index.ts +3 -3
  30. package/src/ui/modals/FPPaymentRequestModal.tsx +1 -5
  31. package/src/ui/modals/FPShell.tsx +0 -1
  32. package/src/ui/screens/ResultScreen.tsx +291 -0
  33. package/src/ui/screens/SendScreen.tsx +37 -171
  34. 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,15 @@
3
3
  // Shows channel selection, then opens the
4
4
  // relevant sub-screen in the same modal sheet
5
5
  // ─────────────────────────────────────────────
6
- import React, { useState } from 'react';
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
13
  TextInput,
15
14
  } from 'react-native';
16
- import { C, R, S, F, shadow } from '../theme';
17
- import { FPButton } from '../components/FPButton';
18
15
  import { TransferSubScreen } from './sub/sendPayment/TransferSubScreen';
19
16
  import { NQRSubScreen } from './sub/sendPayment/NQRSubScreen';
20
17
  import { ProximitySubScreen } from './sub/sendPayment/ProximitySubScreen';
@@ -35,21 +32,12 @@ import styled from 'styled-components/native';
35
32
  import Ionicons from 'react-native-vector-icons/Ionicons';
36
33
  import Svg, { Path } from 'react-native-svg';
37
34
  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
35
  import ConfirmScreen from '../components/ConfirmScreen';
49
36
  import LoadingAnimation from '../components/LoadingAnimation';
50
37
  import { getFPStore } from '../../store/FPStore';
38
+ import { ResultScreen } from './ResultScreen';
51
39
 
52
- const { width, height } = Dimensions.get('window');
40
+ const { height } = Dimensions.get('window');
53
41
 
54
42
  type Channel = 'TRANSFER' | 'QRCODE' | 'NFC' | 'BTH' | 'PXTR';
55
43
 
@@ -150,143 +138,6 @@ const ActionLabel = styled(Text)`
150
138
  line-height: 14px;
151
139
  `;
152
140
 
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
141
 
291
142
  const BluetoothIcon = ({ color = '#111', size = 22 }) => (
292
143
  <Svg
@@ -464,15 +315,11 @@ const SendScreen = ({
464
315
  onError,
465
316
  }: Props) => {
466
317
  const [channel, setChannel] = useState<Channel>('TRANSFER');
467
- const [transactionAmount, setTransactionAmount] = useState<string>('');
468
318
  const [loading, setLoading] = useState<boolean>(false);
469
- const [showAmountModal, setShowAmountModal] = useState<boolean>(false);
470
- const [showConfirmationModal, setShowConfirmationModal] =
471
- useState<boolean>(false);
319
+ const [showConfirmationModal, setShowConfirmationModal] = useState<boolean>(false);
320
+ const [completedTx, setCompletedTx] = useState<FPTransaction | null>(null);
472
321
 
473
- const [transactionPayload, setTransactionPayload] = useState<
474
- FPSendPaymentRequest | FPSendWalletPaymentRequest
475
- >({} as FPSendPaymentRequest | FPSendWalletPaymentRequest);
322
+ const [transactionPayload, setTransactionPayload] = useState< FPSendPaymentRequest | FPSendWalletPaymentRequest>({} as FPSendPaymentRequest | FPSendWalletPaymentRequest);
476
323
 
477
324
  const formatted = `${currency} ${amount.toLocaleString('en-NG', { minimumFractionDigits: 2 })}`;
478
325
 
@@ -488,26 +335,31 @@ const SendScreen = ({
488
335
  setShowConfirmationModal(true);
489
336
  };
490
337
 
338
+ const handleResultClose = () => {
339
+ setCompletedTx(null);
340
+ onClose(); // close the entire shell
341
+ };
342
+
491
343
  const handleProcessingTransaction = async (temptId: string) => {
492
344
  console.log('Got in here for processing');
493
345
  try {
494
346
  setLoading(true);
495
347
  setShowConfirmationModal(false);
496
- let response: FPTransactionResponse | null = null;
348
+ let response: any = null;
497
349
  switch (transactionPayload?.channel) {
498
350
  case 'transfer':
499
351
  case 'bluetooth':
500
352
  case 'proximity':
501
353
  if ('isBank' in transactionPayload && transactionPayload.isBank) {
502
- response = await transferAPI.sendToBank(
354
+ response = (await transferAPI.sendToBank(
503
355
  transactionPayload as FPSendPaymentRequest,
504
356
  temptId
505
- );
357
+ )) || null;
506
358
  } else {
507
- response = await transferAPI.sendToWallet(
359
+ response = (await transferAPI.sendToWallet(
508
360
  transactionPayload as FPSendWalletPaymentRequest,
509
361
  temptId
510
- );
362
+ )) || null;
511
363
  }
512
364
  break;
513
365
  case 'nqr':
@@ -534,15 +386,19 @@ const SendScreen = ({
534
386
  onError?.({ message: 'Invalid channel' } as FPError);
535
387
  break;
536
388
  }
537
-
538
- if (response?.status) {
539
- onError?.({
540
- message: response?.message || 'Transaction failed',
541
- } as FPError);
542
- return;
389
+
390
+ if (!response?.status) {
391
+ // Show failed result screen
392
+ setCompletedTx({
393
+ ...(response?.payload ?? {}),
394
+ status: 'failed',
395
+ } as FPTransaction);
396
+ return; // ← don't call onError — let result screen handle it
543
397
  }
544
398
 
545
- onPaymentSent?.(response?.payload as FPTransaction);
399
+ // Success — show result screen, notify host app
400
+ setCompletedTx(response.payload as FPTransaction);
401
+ onPaymentSent?.(response.payload as FPTransaction);
546
402
  } catch (error) {
547
403
  onError?.(error as FPError);
548
404
  } finally {
@@ -550,6 +406,16 @@ const SendScreen = ({
550
406
  }
551
407
  };
552
408
 
409
+ if (completedTx) {
410
+ console.log("Completed Tx: ", completedTx)
411
+ return (
412
+ <ResultScreen
413
+ transaction={{...transactionPayload, ...completedTx}}
414
+ onClose={handleResultClose}
415
+ />
416
+ );
417
+ }
418
+
553
419
  return (
554
420
  <Container>
555
421
  {loading && <LoadingAnimation />}
@@ -1,6 +1,6 @@
1
1
  export const C = {
2
2
  ink: '#0B1D35',
3
- brand: '#0052CC',
3
+ brand: '#003333',
4
4
  brandLight: '#E8F0FE',
5
5
  green: '#00875A',
6
6
  greenLight: '#E3FCEF',