react-native-fpay 0.4.30 → 0.4.33
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 +5 -0
- package/lib/module/FountainPayProvider.js.map +1 -1
- package/lib/module/core/api/index.js +59 -0
- package/lib/module/core/api/index.js.map +1 -1
- package/lib/module/core/types/index.js +32 -0
- package/lib/module/core/types/index.js.map +1 -1
- package/lib/module/engine/FPEngine.js +9 -0
- package/lib/module/engine/FPEngine.js.map +1 -1
- package/lib/module/ui/components/ConfirmScreen.js +43 -51
- package/lib/module/ui/components/ConfirmScreen.js.map +1 -1
- package/lib/module/ui/components/RecurringToggle.js +94 -0
- package/lib/module/ui/components/RecurringToggle.js.map +1 -0
- package/lib/module/ui/modals/FPShell.js +19 -0
- package/lib/module/ui/modals/FPShell.js.map +1 -1
- package/lib/module/ui/screens/BillsScreen.js +187 -0
- package/lib/module/ui/screens/BillsScreen.js.map +1 -0
- package/lib/module/ui/screens/ResultScreen.js +113 -28
- package/lib/module/ui/screens/ResultScreen.js.map +1 -1
- package/lib/module/ui/screens/SendScreen.js +54 -6
- package/lib/module/ui/screens/SendScreen.js.map +1 -1
- package/lib/module/ui/screens/sub/billPayment/AirtimeScreen.js +257 -0
- package/lib/module/ui/screens/sub/billPayment/AirtimeScreen.js.map +1 -0
- package/lib/module/ui/screens/sub/billPayment/CableScreen.js +264 -0
- package/lib/module/ui/screens/sub/billPayment/CableScreen.js.map +1 -0
- package/lib/module/ui/screens/sub/billPayment/DataScreen.js +273 -0
- package/lib/module/ui/screens/sub/billPayment/DataScreen.js.map +1 -0
- package/lib/module/ui/screens/sub/billPayment/ElectricityScreen.js +337 -0
- package/lib/module/ui/screens/sub/billPayment/ElectricityScreen.js.map +1 -0
- package/lib/typescript/src/FountainPayProvider.d.ts.map +1 -1
- package/lib/typescript/src/core/api/index.d.ts +52 -63
- package/lib/typescript/src/core/api/index.d.ts.map +1 -1
- package/lib/typescript/src/core/types/index.d.ts +146 -0
- package/lib/typescript/src/core/types/index.d.ts.map +1 -1
- package/lib/typescript/src/engine/FPEngine.d.ts +4 -2
- package/lib/typescript/src/engine/FPEngine.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/ui/components/ConfirmScreen.d.ts +25 -4
- package/lib/typescript/src/ui/components/ConfirmScreen.d.ts.map +1 -1
- package/lib/typescript/src/ui/components/RecurringToggle.d.ts +7 -0
- package/lib/typescript/src/ui/components/RecurringToggle.d.ts.map +1 -0
- package/lib/typescript/src/ui/modals/FPShell.d.ts.map +1 -1
- package/lib/typescript/src/ui/screens/BillsScreen.d.ts +10 -0
- package/lib/typescript/src/ui/screens/BillsScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/ResultScreen.d.ts +20 -3
- package/lib/typescript/src/ui/screens/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/ui/screens/SendScreen.d.ts.map +1 -1
- package/lib/typescript/src/ui/screens/sub/billPayment/AirtimeScreen.d.ts +15 -0
- package/lib/typescript/src/ui/screens/sub/billPayment/AirtimeScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/sub/billPayment/CableScreen.d.ts +14 -0
- package/lib/typescript/src/ui/screens/sub/billPayment/CableScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/sub/billPayment/DataScreen.d.ts +14 -0
- package/lib/typescript/src/ui/screens/sub/billPayment/DataScreen.d.ts.map +1 -0
- package/lib/typescript/src/ui/screens/sub/billPayment/ElectricityScreen.d.ts +16 -0
- package/lib/typescript/src/ui/screens/sub/billPayment/ElectricityScreen.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/FountainPayProvider.tsx +7 -0
- package/src/core/api/index.ts +149 -27
- package/src/core/types/index.ts +181 -0
- package/src/engine/FPEngine.ts +12 -1
- package/src/index.ts +9 -1
- package/src/ui/components/ConfirmScreen.tsx +47 -54
- package/src/ui/components/RecurringToggle.tsx +106 -0
- package/src/ui/modals/FPShell.tsx +26 -3
- package/src/ui/screens/BillsScreen.tsx +198 -0
- package/src/ui/screens/ResultScreen.tsx +129 -28
- package/src/ui/screens/SendScreen.tsx +43 -6
- package/src/ui/screens/sub/billPayment/AirtimeScreen.tsx +252 -0
- package/src/ui/screens/sub/billPayment/CableScreen.tsx +274 -0
- package/src/ui/screens/sub/billPayment/DataScreen.tsx +263 -0
- package/src/ui/screens/sub/billPayment/ElectricityScreen.tsx +344 -0
|
@@ -14,15 +14,6 @@ import {
|
|
|
14
14
|
import styled from 'styled-components/native';
|
|
15
15
|
import OTPInputs from './OtpInput';
|
|
16
16
|
import InLoading from './LoadingAnimation/InLoading';
|
|
17
|
-
import type {
|
|
18
|
-
FPInternalTransferRecipient,
|
|
19
|
-
FPSendPaymentRequest,
|
|
20
|
-
FPSendWalletPaymentRequest,
|
|
21
|
-
FPTransferRecipient,
|
|
22
|
-
} from '../../core/types';
|
|
23
|
-
import { transferAPI } from '../../core/api';
|
|
24
|
-
|
|
25
|
-
const { width } = Dimensions.get('window');
|
|
26
17
|
|
|
27
18
|
// ─── Styled Components ───────────────────────────────────────────────────────
|
|
28
19
|
|
|
@@ -178,11 +169,31 @@ const ProcessingRow = styled.View`
|
|
|
178
169
|
align-items: center;
|
|
179
170
|
`;
|
|
180
171
|
|
|
172
|
+
const ErrorText = styled(Text)`
|
|
173
|
+
color: #de350b;
|
|
174
|
+
font-size: 13px;
|
|
175
|
+
font-weight: 600;
|
|
176
|
+
text-align: center;
|
|
177
|
+
margin-top: 10px;
|
|
178
|
+
`;
|
|
179
|
+
|
|
181
180
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
182
181
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Generalized props — ConfirmScreen no longer knows what a "transaction" or
|
|
184
|
+
* "recipient" is. The caller (SendScreen, BillsScreen, etc.) supplies:
|
|
185
|
+
* - summaryRows: what to show in the detail card
|
|
186
|
+
* - amount/currency: for the amount display
|
|
187
|
+
* - subtitle: the warning/info line under the header
|
|
188
|
+
* - validate: the PIN-check call to make (different endpoint per domain)
|
|
189
|
+
* - onContinue: called with the temp_id once PIN validation succeeds
|
|
190
|
+
*/
|
|
191
|
+
interface ConfirmScreenProps {
|
|
192
|
+
summaryRows: { label: string; value: string }[];
|
|
193
|
+
amount: number;
|
|
194
|
+
currency?: string;
|
|
195
|
+
subtitle?: string;
|
|
196
|
+
validate: (pin: string) => Promise<{ status: boolean; message: string; payload?: { temp_id: string } }>;
|
|
186
197
|
onContinue: (tempId: string) => void;
|
|
187
198
|
}
|
|
188
199
|
|
|
@@ -248,9 +259,12 @@ const DetailRow: React.FC<{ label: string; value: string }> = ({
|
|
|
248
259
|
|
|
249
260
|
// ─── Main Component ──────────────────────────────────────────────────────────
|
|
250
261
|
|
|
251
|
-
const ConfirmScreen: React.FC<
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
const ConfirmScreen: React.FC<ConfirmScreenProps> = ({
|
|
263
|
+
summaryRows,
|
|
264
|
+
amount,
|
|
265
|
+
currency,
|
|
266
|
+
subtitle,
|
|
267
|
+
validate,
|
|
254
268
|
onContinue,
|
|
255
269
|
}) => {
|
|
256
270
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
|
@@ -263,24 +277,19 @@ const ConfirmScreen: React.FC<TransferReminderScreenProps> = ({
|
|
|
263
277
|
const [errText, setErrText] = useState<string>('');
|
|
264
278
|
const [otp, setOtp] = useState<string>('');
|
|
265
279
|
|
|
266
|
-
const isWalletTransfer = !transaction?.isBank;
|
|
267
|
-
const recipientName = transaction?.recipient.accountName ?? '';
|
|
268
|
-
const accountNumber = transaction?.recipient.accountNumber ?? '';
|
|
269
|
-
const bankName = !isWalletTransfer
|
|
270
|
-
? (transaction?.recipient as any)?.bankName
|
|
271
|
-
: undefined;
|
|
272
|
-
|
|
273
280
|
const handleContinue = async () => {
|
|
274
281
|
setProcessing(true);
|
|
282
|
+
setErrText('');
|
|
275
283
|
try {
|
|
276
|
-
const response
|
|
284
|
+
const response = await validate(otp);
|
|
277
285
|
console.log('Validate: ', response);
|
|
278
|
-
if (!response.status) {
|
|
286
|
+
if (!response.status || !response.payload?.temp_id) {
|
|
279
287
|
Vibration.vibrate(100);
|
|
280
288
|
setErrText(response.message);
|
|
281
289
|
return;
|
|
282
290
|
}
|
|
283
291
|
console.log('Continue: ', response.payload.temp_id);
|
|
292
|
+
|
|
284
293
|
onContinue(response.payload.temp_id);
|
|
285
294
|
} catch (error: any) {
|
|
286
295
|
setErrText(error.message);
|
|
@@ -324,8 +333,8 @@ const ConfirmScreen: React.FC<TransferReminderScreenProps> = ({
|
|
|
324
333
|
]).start();
|
|
325
334
|
}, []);
|
|
326
335
|
|
|
327
|
-
const formattedAmount = `${
|
|
328
|
-
const amountWords = getAmountInWords(Number(
|
|
336
|
+
const formattedAmount = `${currency ?? '₦'}${Number(amount || 0).toFixed(2)}`;
|
|
337
|
+
const amountWords = getAmountInWords(Number(amount || 0));
|
|
329
338
|
|
|
330
339
|
return (
|
|
331
340
|
<Container>
|
|
@@ -348,8 +357,8 @@ const ConfirmScreen: React.FC<TransferReminderScreenProps> = ({
|
|
|
348
357
|
>
|
|
349
358
|
<Header>
|
|
350
359
|
<SubTitle>
|
|
351
|
-
|
|
352
|
-
|
|
360
|
+
{subtitle ??
|
|
361
|
+
'Double check the details before you proceed. Please note that successful transactions cannot be reversed.'}
|
|
353
362
|
</SubTitle>
|
|
354
363
|
</Header>
|
|
355
364
|
</Animated.View>
|
|
@@ -363,28 +372,9 @@ const ConfirmScreen: React.FC<TransferReminderScreenProps> = ({
|
|
|
363
372
|
>
|
|
364
373
|
<Card>
|
|
365
374
|
<CardTitle>Transaction Details</CardTitle>
|
|
366
|
-
{
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
{transaction?.channel === 'transfer' && (
|
|
370
|
-
<>
|
|
371
|
-
<Row>
|
|
372
|
-
<RowLabel>Bank</RowLabel>
|
|
373
|
-
<RowValue>{bankName}</RowValue>
|
|
374
|
-
</Row>
|
|
375
|
-
<Row>
|
|
376
|
-
<RowLabel>Account No.</RowLabel>
|
|
377
|
-
<RowValue>{accountNumber}</RowValue>
|
|
378
|
-
</Row>
|
|
379
|
-
</>
|
|
380
|
-
)}
|
|
381
|
-
</>
|
|
382
|
-
) : (
|
|
383
|
-
<>
|
|
384
|
-
<DetailRow label="Name" value={recipientName} />
|
|
385
|
-
<DetailRow label="Account Number" value={accountNumber} />
|
|
386
|
-
</>
|
|
387
|
-
)}
|
|
375
|
+
{summaryRows.map((row) => (
|
|
376
|
+
<DetailRow key={row.label} label={row.label} value={row.value} />
|
|
377
|
+
))}
|
|
388
378
|
|
|
389
379
|
<AmountRow>
|
|
390
380
|
<AmountLabelContainer>
|
|
@@ -400,9 +390,12 @@ const ConfirmScreen: React.FC<TransferReminderScreenProps> = ({
|
|
|
400
390
|
</Card>
|
|
401
391
|
|
|
402
392
|
{!processing && (
|
|
403
|
-
|
|
404
|
-
<
|
|
405
|
-
|
|
393
|
+
<>
|
|
394
|
+
<AmountRow>
|
|
395
|
+
<OTPInputs count={4} callBack={setOtp} />
|
|
396
|
+
</AmountRow>
|
|
397
|
+
{!!errText && <ErrorText>{errText}</ErrorText>}
|
|
398
|
+
</>
|
|
406
399
|
)}
|
|
407
400
|
</Animated.View>
|
|
408
401
|
</View>
|
|
@@ -432,4 +425,4 @@ const ConfirmScreen: React.FC<TransferReminderScreenProps> = ({
|
|
|
432
425
|
);
|
|
433
426
|
};
|
|
434
427
|
|
|
435
|
-
export default ConfirmScreen;
|
|
428
|
+
export default ConfirmScreen;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// components/RecurringToggle.tsx
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { Switch } from 'react-native';
|
|
4
|
+
import styled from 'styled-components/native';
|
|
5
|
+
import type { SubscriptionFreq } from '../../core/types';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
onToggle: (enabled: boolean, frequency: SubscriptionFreq) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Wrapper = styled.View`
|
|
12
|
+
background-color: #f5f3ff;
|
|
13
|
+
border-radius: 14px;
|
|
14
|
+
padding: 14px 16px;
|
|
15
|
+
margin-top: 12px;
|
|
16
|
+
border-width: 1px;
|
|
17
|
+
border-color: #ede9fe;
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const TopRow = styled.View`
|
|
21
|
+
flex-direction: row;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: space-between;
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const TextBlock = styled.View`flex: 1; margin-right: 12px;`;
|
|
27
|
+
|
|
28
|
+
const ToggleLabel = styled.Text`
|
|
29
|
+
font-size: 14px;
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
color: #4f46e5;
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const ToggleSubtext = styled.Text`
|
|
35
|
+
font-size: 12px;
|
|
36
|
+
color: #7c6fcd;
|
|
37
|
+
margin-top: 2px;
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const FreqRow = styled.View`
|
|
41
|
+
flex-direction: row;
|
|
42
|
+
gap: 8px;
|
|
43
|
+
margin-top: 12px;
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const FreqBtn = styled.TouchableOpacity<{ selected?: boolean }>`
|
|
47
|
+
flex: 1;
|
|
48
|
+
padding: 8px 4px;
|
|
49
|
+
border-radius: 8px;
|
|
50
|
+
align-items: center;
|
|
51
|
+
background-color: ${p => p.selected ? '#4f46e5' : '#fff'};
|
|
52
|
+
border-width: 1px;
|
|
53
|
+
border-color: ${p => p.selected ? '#4f46e5' : '#ddd6fe'};
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const FreqText = styled.Text<{ selected?: boolean }>`
|
|
57
|
+
font-size: 11px;
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
color: ${p => p.selected ? '#fff' : '#7c6fcd'};
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const FREQUENCIES: SubscriptionFreq[] = ['DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'];
|
|
63
|
+
|
|
64
|
+
export default function RecurringToggle({ onToggle }: Props) {
|
|
65
|
+
const [enabled, setEnabled] = useState(false);
|
|
66
|
+
const [frequency, setFrequency] = useState<SubscriptionFreq>('MONTHLY');
|
|
67
|
+
|
|
68
|
+
const handleSwitch = (val: boolean) => {
|
|
69
|
+
setEnabled(val);
|
|
70
|
+
onToggle(val, frequency);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleFreq = (freq: SubscriptionFreq) => {
|
|
74
|
+
setFrequency(freq);
|
|
75
|
+
if (enabled) onToggle(true, freq);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Wrapper>
|
|
80
|
+
<TopRow>
|
|
81
|
+
<TextBlock>
|
|
82
|
+
<ToggleLabel>🔁 Make this recurring</ToggleLabel>
|
|
83
|
+
<ToggleSubtext>Auto-pay this on a schedule</ToggleSubtext>
|
|
84
|
+
</TextBlock>
|
|
85
|
+
<Switch
|
|
86
|
+
value={enabled}
|
|
87
|
+
onValueChange={handleSwitch}
|
|
88
|
+
trackColor={{ false: '#ddd', true: '#818cf8' }}
|
|
89
|
+
thumbColor={enabled ? '#4f46e5' : '#f4f3f4'}
|
|
90
|
+
/>
|
|
91
|
+
</TopRow>
|
|
92
|
+
|
|
93
|
+
{enabled && (
|
|
94
|
+
<FreqRow>
|
|
95
|
+
{FREQUENCIES.map(freq => (
|
|
96
|
+
<FreqBtn key={freq} selected={frequency === freq} onPress={() => handleFreq(freq)}>
|
|
97
|
+
<FreqText selected={frequency === freq}>
|
|
98
|
+
{freq[0] + freq.slice(1).toLowerCase()}
|
|
99
|
+
</FreqText>
|
|
100
|
+
</FreqBtn>
|
|
101
|
+
))}
|
|
102
|
+
</FreqRow>
|
|
103
|
+
)}
|
|
104
|
+
</Wrapper>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -16,23 +16,25 @@ import {
|
|
|
16
16
|
import { _onEvent } from '../../engine/FPEngine';
|
|
17
17
|
import SendScreen from '../screens/SendScreen';
|
|
18
18
|
import ReceiveScreen from '../screens/ReceiveScreen';
|
|
19
|
+
import BillsScreen from '../screens/BillsScreen';
|
|
19
20
|
import FPValidateBvn from '../modals/FPValidateBvn';
|
|
20
21
|
import FPCreatePin from '../modals/FPCreatePin';
|
|
21
22
|
import { FPPaymentRequestModal } from './FPPaymentRequestModal';
|
|
22
23
|
import { FPEngine } from '../../engine/FPEngine';
|
|
23
24
|
import { C, S, F } from '../theme';
|
|
24
|
-
import type { FPCurrency, FPUserInfo } from '../../core/types';
|
|
25
|
+
import type { FPCurrency, FPUserInfo, FPBillCategory } from '../../core/types';
|
|
25
26
|
import { useFPStore } from '../../store/FPStore';
|
|
26
27
|
|
|
27
28
|
const { width: SCREEN_W, height: SCREEN_H } = Dimensions.get('window');
|
|
28
29
|
|
|
29
|
-
type SheetMode = 'send' | 'receive' | null;
|
|
30
|
+
type SheetMode = 'send' | 'receive' | 'bills' | null;
|
|
30
31
|
|
|
31
32
|
interface SheetState {
|
|
32
33
|
user?: FPUserInfo;
|
|
33
34
|
mode: SheetMode;
|
|
34
35
|
amount?: number;
|
|
35
36
|
currency?: FPCurrency;
|
|
37
|
+
category?: FPBillCategory;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export function FPShell() {
|
|
@@ -83,11 +85,23 @@ export function FPShell() {
|
|
|
83
85
|
currency: data?.currency,
|
|
84
86
|
});
|
|
85
87
|
});
|
|
88
|
+
const unsubBills = _onEvent('show_bills', (d: unknown) => {
|
|
89
|
+
const data = d as any;
|
|
90
|
+
open({
|
|
91
|
+
mode: 'bills',
|
|
92
|
+
user: data?.user,
|
|
93
|
+
amount: data?.amount,
|
|
94
|
+
category: data?.category,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
86
97
|
const unsubClose = _onEvent('close_send', () => close());
|
|
98
|
+
const unsubCloseBills = _onEvent('close_bills', () => close());
|
|
87
99
|
return () => {
|
|
88
100
|
unsubSend();
|
|
89
101
|
unsubReceive();
|
|
102
|
+
unsubBills();
|
|
90
103
|
unsubClose();
|
|
104
|
+
unsubCloseBills();
|
|
91
105
|
};
|
|
92
106
|
}, [open, close]);
|
|
93
107
|
|
|
@@ -134,6 +148,15 @@ export function FPShell() {
|
|
|
134
148
|
onError={FPEngine.getCallbacks().onError}
|
|
135
149
|
/>
|
|
136
150
|
)}
|
|
151
|
+
{sheet.mode === 'bills' && sheet.category && (
|
|
152
|
+
<BillsScreen
|
|
153
|
+
category={sheet.category}
|
|
154
|
+
amount={sheet.amount ?? 0}
|
|
155
|
+
user={user ?? null}
|
|
156
|
+
onClose={close}
|
|
157
|
+
onError={FPEngine.getCallbacks().onError}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
137
160
|
</View>
|
|
138
161
|
</Animated.View>
|
|
139
162
|
)}
|
|
@@ -211,4 +234,4 @@ const st = StyleSheet.create({
|
|
|
211
234
|
flex: 1,
|
|
212
235
|
backgroundColor: C.surface,
|
|
213
236
|
},
|
|
214
|
-
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────
|
|
2
|
+
// Bills Screen
|
|
3
|
+
// Renders the category-specific sub-screen
|
|
4
|
+
// (Airtime / Data / Electricity / Cable), then
|
|
5
|
+
// runs the same Confirm (PIN) → Loading →
|
|
6
|
+
// Result flow SendScreen uses, generalized.
|
|
7
|
+
// ─────────────────────────────────────────────
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import { View, TouchableOpacity, Text } from 'react-native';
|
|
10
|
+
import styled from 'styled-components/native';
|
|
11
|
+
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
12
|
+
import AirtimeSubScreen from './sub/billPayment/AirtimeScreen';
|
|
13
|
+
import DataSubScreen from './sub/billPayment/DataScreen';
|
|
14
|
+
import ElectricitySubScreen from './sub/billPayment/ElectricityScreen';
|
|
15
|
+
import CableSubScreen from './sub/billPayment/CableScreen';
|
|
16
|
+
import ConfirmScreen from '../components/ConfirmScreen';
|
|
17
|
+
import { ResultScreen } from './ResultScreen';
|
|
18
|
+
import LoadingAnimation from '../components/LoadingAnimation';
|
|
19
|
+
import { billsAPI } from '../../core/api';
|
|
20
|
+
import { getFPStore } from '../../store/FPStore';
|
|
21
|
+
import { C, F, S } from '../theme';
|
|
22
|
+
import type {
|
|
23
|
+
FPBillCategory,
|
|
24
|
+
FPBillPurchaseRequest,
|
|
25
|
+
FPBillTransaction,
|
|
26
|
+
FPCallbacks,
|
|
27
|
+
FPError,
|
|
28
|
+
FPUserInfo,
|
|
29
|
+
FPSummaryRow,
|
|
30
|
+
} from '../../core/types';
|
|
31
|
+
|
|
32
|
+
const Container = styled(View)`
|
|
33
|
+
flex: 1;
|
|
34
|
+
background-color: ${C.white};
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const Header = styled(View)`
|
|
38
|
+
flex-direction: row;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: space-between;
|
|
41
|
+
padding: ${S.md}px ${S.lg}px;
|
|
42
|
+
margin-top: 30px;
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const HeaderTitle = styled(Text)`
|
|
46
|
+
font-size: ${F.lg}px;
|
|
47
|
+
font-weight: 800;
|
|
48
|
+
color: ${C.ink};
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const CloseBtn = styled(TouchableOpacity)`
|
|
52
|
+
width: 36px;
|
|
53
|
+
height: 36px;
|
|
54
|
+
border-radius: 18px;
|
|
55
|
+
background-color: ${C.surface};
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const CATEGORY_TITLES: Record<FPBillCategory, string> = {
|
|
61
|
+
AIRTIME: 'Buy Airtime',
|
|
62
|
+
DATA: 'Buy Data',
|
|
63
|
+
ELECTRICITY: 'Pay Electricity Bill',
|
|
64
|
+
CABLE: 'Pay TV Subscription',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
interface Props extends Pick<FPCallbacks, 'onError'> {
|
|
68
|
+
category: FPBillCategory;
|
|
69
|
+
amount: number;
|
|
70
|
+
user: FPUserInfo | null;
|
|
71
|
+
onClose: () => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type FlowStage = 'FORM' | 'CONFIRM' | 'RESULT';
|
|
75
|
+
|
|
76
|
+
export default function BillsScreen({ category, amount, user, onClose, onError }: Props) {
|
|
77
|
+
const [stage, setStage] = useState<FlowStage>('FORM');
|
|
78
|
+
const [loading, setLoading] = useState(false);
|
|
79
|
+
const [pendingPayload, setPendingPayload] = useState<FPBillPurchaseRequest | null>(null);
|
|
80
|
+
const [pendingSummaryRows, setPendingSummaryRows] = useState<FPSummaryRow[]>([]);
|
|
81
|
+
const [completedTx, setCompletedTx] = useState<FPBillTransaction | null>(null);
|
|
82
|
+
|
|
83
|
+
const handleFormContinue = (payload: FPBillPurchaseRequest, summaryRows: FPSummaryRow[]) => {
|
|
84
|
+
setPendingPayload(payload);
|
|
85
|
+
setPendingSummaryRows(summaryRows);
|
|
86
|
+
setStage('CONFIRM');
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handleResultClose = () => {
|
|
90
|
+
setCompletedTx(null);
|
|
91
|
+
setStage('FORM');
|
|
92
|
+
onClose();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handlePinAuthorized = async (tempId: string) => {
|
|
96
|
+
if (!pendingPayload) return;
|
|
97
|
+
setLoading(true);
|
|
98
|
+
try {
|
|
99
|
+
let response;
|
|
100
|
+
switch (pendingPayload.category) {
|
|
101
|
+
case 'AIRTIME':
|
|
102
|
+
response = await billsAPI.purchaseAirtime(pendingPayload, tempId);
|
|
103
|
+
break;
|
|
104
|
+
case 'DATA':
|
|
105
|
+
response = await billsAPI.purchaseData(pendingPayload, tempId);
|
|
106
|
+
break;
|
|
107
|
+
case 'ELECTRICITY':
|
|
108
|
+
response = await billsAPI.purchaseElectricity(pendingPayload, tempId);
|
|
109
|
+
break;
|
|
110
|
+
case 'CABLE':
|
|
111
|
+
response = await billsAPI.purchaseCable(pendingPayload, tempId);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!response?.status) {
|
|
116
|
+
onError?.({ code: 'PURCHASE_FAILED', message: response?.message ?? 'Purchase failed' } as FPError);
|
|
117
|
+
// Still show the result screen — ResultScreen polls status itself
|
|
118
|
+
// and renders the failed state, same pattern SendScreen uses.
|
|
119
|
+
}
|
|
120
|
+
setCompletedTx(response?.payload ?? null);
|
|
121
|
+
setStage('RESULT');
|
|
122
|
+
} catch (error) {
|
|
123
|
+
onError?.(error as FPError);
|
|
124
|
+
} finally {
|
|
125
|
+
setLoading(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const renderForm = () => {
|
|
130
|
+
switch (category) {
|
|
131
|
+
case 'AIRTIME':
|
|
132
|
+
return <AirtimeSubScreen amount={amount} onProcessTransaction={handleFormContinue} onError={onError} />;
|
|
133
|
+
case 'DATA':
|
|
134
|
+
return <DataSubScreen onProcessTransaction={handleFormContinue} onError={onError} />;
|
|
135
|
+
case 'ELECTRICITY':
|
|
136
|
+
return (
|
|
137
|
+
<ElectricitySubScreen
|
|
138
|
+
amount={amount}
|
|
139
|
+
user={user}
|
|
140
|
+
onProcessTransaction={handleFormContinue}
|
|
141
|
+
onError={onError}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
case 'CABLE':
|
|
145
|
+
return <CableSubScreen onProcessTransaction={handleFormContinue} onError={onError} />;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (stage === 'RESULT' && completedTx) {
|
|
150
|
+
return (
|
|
151
|
+
<ResultScreen
|
|
152
|
+
reference={completedTx.reference}
|
|
153
|
+
summaryRows={pendingSummaryRows}
|
|
154
|
+
allowRecurring={false}
|
|
155
|
+
statusFetcher={billsAPI.status}
|
|
156
|
+
onClose={handleResultClose}
|
|
157
|
+
/>
|
|
158
|
+
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<Container>
|
|
164
|
+
{loading && <LoadingAnimation text="Processing..." />}
|
|
165
|
+
|
|
166
|
+
{stage === 'FORM' && (
|
|
167
|
+
<>
|
|
168
|
+
<Header>
|
|
169
|
+
<HeaderTitle>{CATEGORY_TITLES[category]}</HeaderTitle>
|
|
170
|
+
<CloseBtn onPress={onClose}>
|
|
171
|
+
<Ionicons name="close" size={20} color={C.ink} />
|
|
172
|
+
</CloseBtn>
|
|
173
|
+
</Header>
|
|
174
|
+
{renderForm()}
|
|
175
|
+
</>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{stage === 'CONFIRM' && pendingPayload && (
|
|
179
|
+
<>
|
|
180
|
+
<Header>
|
|
181
|
+
<HeaderTitle>Confirm Purchase</HeaderTitle>
|
|
182
|
+
<CloseBtn onPress={() => setStage('FORM')}>
|
|
183
|
+
<Ionicons name="close" size={20} color={C.ink} />
|
|
184
|
+
</CloseBtn>
|
|
185
|
+
</Header>
|
|
186
|
+
<ConfirmScreen
|
|
187
|
+
amount={pendingPayload.amountInKobo / 100}
|
|
188
|
+
currency="₦"
|
|
189
|
+
subtitle="Double check the details before you proceed. Please note that successful bill payments cannot be reversed."
|
|
190
|
+
summaryRows={pendingSummaryRows}
|
|
191
|
+
validate={(pin: string) => billsAPI.validateBillPin(pin, getFPStore().psspId || '')}
|
|
192
|
+
onContinue={handlePinAuthorized}
|
|
193
|
+
/>
|
|
194
|
+
</>
|
|
195
|
+
)}
|
|
196
|
+
</Container>
|
|
197
|
+
);
|
|
198
|
+
}
|