react-native-fpay 0.4.29 → 0.4.31

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 (79) hide show
  1. package/lib/module/FountainPayProvider.js +5 -0
  2. package/lib/module/FountainPayProvider.js.map +1 -1
  3. package/lib/module/core/api/index.js +59 -0
  4. package/lib/module/core/api/index.js.map +1 -1
  5. package/lib/module/core/types/index.js +32 -0
  6. package/lib/module/core/types/index.js.map +1 -1
  7. package/lib/module/engine/FPEngine.js +9 -0
  8. package/lib/module/engine/FPEngine.js.map +1 -1
  9. package/lib/module/hooks/useLocation.js +66 -0
  10. package/lib/module/hooks/useLocation.js.map +1 -0
  11. package/lib/module/ui/components/ConfirmScreen.js +43 -51
  12. package/lib/module/ui/components/ConfirmScreen.js.map +1 -1
  13. package/lib/module/ui/components/RecurringToggle.js +94 -0
  14. package/lib/module/ui/components/RecurringToggle.js.map +1 -0
  15. package/lib/module/ui/modals/FPShell.js +19 -0
  16. package/lib/module/ui/modals/FPShell.js.map +1 -1
  17. package/lib/module/ui/screens/BillsScreen.js +186 -0
  18. package/lib/module/ui/screens/BillsScreen.js.map +1 -0
  19. package/lib/module/ui/screens/ResultScreen.js +113 -28
  20. package/lib/module/ui/screens/ResultScreen.js.map +1 -1
  21. package/lib/module/ui/screens/SendScreen.js +85 -16
  22. package/lib/module/ui/screens/SendScreen.js.map +1 -1
  23. package/lib/module/ui/screens/sub/billPayment/AirtimeScreen.js +257 -0
  24. package/lib/module/ui/screens/sub/billPayment/AirtimeScreen.js.map +1 -0
  25. package/lib/module/ui/screens/sub/billPayment/CableScreen.js +264 -0
  26. package/lib/module/ui/screens/sub/billPayment/CableScreen.js.map +1 -0
  27. package/lib/module/ui/screens/sub/billPayment/DataScreen.js +273 -0
  28. package/lib/module/ui/screens/sub/billPayment/DataScreen.js.map +1 -0
  29. package/lib/module/ui/screens/sub/billPayment/ElectricityScreen.js +337 -0
  30. package/lib/module/ui/screens/sub/billPayment/ElectricityScreen.js.map +1 -0
  31. package/lib/module/ui/screens/sub/sendPayment/TransferSubScreen.js +1 -1
  32. package/lib/module/ui/screens/sub/sendPayment/TransferSubScreen.js.map +1 -1
  33. package/lib/typescript/src/FountainPayProvider.d.ts.map +1 -1
  34. package/lib/typescript/src/core/api/index.d.ts +52 -63
  35. package/lib/typescript/src/core/api/index.d.ts.map +1 -1
  36. package/lib/typescript/src/core/types/index.d.ts +159 -6
  37. package/lib/typescript/src/core/types/index.d.ts.map +1 -1
  38. package/lib/typescript/src/engine/FPEngine.d.ts +4 -2
  39. package/lib/typescript/src/engine/FPEngine.d.ts.map +1 -1
  40. package/lib/typescript/src/hooks/useLocation.d.ts +12 -0
  41. package/lib/typescript/src/hooks/useLocation.d.ts.map +1 -0
  42. package/lib/typescript/src/index.d.ts +1 -1
  43. package/lib/typescript/src/index.d.ts.map +1 -1
  44. package/lib/typescript/src/ui/components/ConfirmScreen.d.ts +25 -4
  45. package/lib/typescript/src/ui/components/ConfirmScreen.d.ts.map +1 -1
  46. package/lib/typescript/src/ui/components/RecurringToggle.d.ts +7 -0
  47. package/lib/typescript/src/ui/components/RecurringToggle.d.ts.map +1 -0
  48. package/lib/typescript/src/ui/modals/FPShell.d.ts.map +1 -1
  49. package/lib/typescript/src/ui/screens/BillsScreen.d.ts +10 -0
  50. package/lib/typescript/src/ui/screens/BillsScreen.d.ts.map +1 -0
  51. package/lib/typescript/src/ui/screens/ResultScreen.d.ts +20 -3
  52. package/lib/typescript/src/ui/screens/ResultScreen.d.ts.map +1 -1
  53. package/lib/typescript/src/ui/screens/SendScreen.d.ts.map +1 -1
  54. package/lib/typescript/src/ui/screens/sub/billPayment/AirtimeScreen.d.ts +15 -0
  55. package/lib/typescript/src/ui/screens/sub/billPayment/AirtimeScreen.d.ts.map +1 -0
  56. package/lib/typescript/src/ui/screens/sub/billPayment/CableScreen.d.ts +14 -0
  57. package/lib/typescript/src/ui/screens/sub/billPayment/CableScreen.d.ts.map +1 -0
  58. package/lib/typescript/src/ui/screens/sub/billPayment/DataScreen.d.ts +14 -0
  59. package/lib/typescript/src/ui/screens/sub/billPayment/DataScreen.d.ts.map +1 -0
  60. package/lib/typescript/src/ui/screens/sub/billPayment/ElectricityScreen.d.ts +16 -0
  61. package/lib/typescript/src/ui/screens/sub/billPayment/ElectricityScreen.d.ts.map +1 -0
  62. package/package.json +2 -2
  63. package/src/FountainPayProvider.tsx +7 -0
  64. package/src/core/api/index.ts +149 -27
  65. package/src/core/types/index.ts +194 -11
  66. package/src/engine/FPEngine.ts +12 -1
  67. package/src/hooks/useLocation.ts +81 -0
  68. package/src/index.ts +9 -1
  69. package/src/ui/components/ConfirmScreen.tsx +47 -54
  70. package/src/ui/components/RecurringToggle.tsx +106 -0
  71. package/src/ui/modals/FPShell.tsx +26 -3
  72. package/src/ui/screens/BillsScreen.tsx +197 -0
  73. package/src/ui/screens/ResultScreen.tsx +129 -28
  74. package/src/ui/screens/SendScreen.tsx +124 -68
  75. package/src/ui/screens/sub/billPayment/AirtimeScreen.tsx +252 -0
  76. package/src/ui/screens/sub/billPayment/CableScreen.tsx +274 -0
  77. package/src/ui/screens/sub/billPayment/DataScreen.tsx +263 -0
  78. package/src/ui/screens/sub/billPayment/ElectricityScreen.tsx +344 -0
  79. package/src/ui/screens/sub/sendPayment/TransferSubScreen.tsx +1 -1
@@ -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
- interface TransferReminderScreenProps {
184
- userId: string;
185
- transaction: any;
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<TransferReminderScreenProps> = ({
252
- userId,
253
- transaction,
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: any = await transferAPI.validateTransfer(otp, userId, transaction?.recipient.id);
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 = `${transaction?.currency ?? '₦'}${Number(transaction?.amount || 0).toFixed(2)}`;
328
- const amountWords = getAmountInWords(Number(transaction?.amount || 0));
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
- Double check the transfer details before you proceed. Please
352
- note that successful transfers cannot be reversed.
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
- {transaction?.isBank ? (
367
- <>
368
- <DetailRow label="Name" value={recipientName} />
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
- <AmountRow>
404
- <OTPInputs count={4} callBack={setOtp} />
405
- </AmountRow>
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,197 @@
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
+ `;
43
+
44
+ const HeaderTitle = styled(Text)`
45
+ font-size: ${F.lg}px;
46
+ font-weight: 800;
47
+ color: ${C.ink};
48
+ `;
49
+
50
+ const CloseBtn = styled(TouchableOpacity)`
51
+ width: 36px;
52
+ height: 36px;
53
+ border-radius: 18px;
54
+ background-color: ${C.surface};
55
+ align-items: center;
56
+ justify-content: center;
57
+ `;
58
+
59
+ const CATEGORY_TITLES: Record<FPBillCategory, string> = {
60
+ AIRTIME: 'Buy Airtime',
61
+ DATA: 'Buy Data',
62
+ ELECTRICITY: 'Pay Electricity Bill',
63
+ CABLE: 'Pay TV Subscription',
64
+ };
65
+
66
+ interface Props extends Pick<FPCallbacks, 'onError'> {
67
+ category: FPBillCategory;
68
+ amount: number;
69
+ user: FPUserInfo | null;
70
+ onClose: () => void;
71
+ }
72
+
73
+ type FlowStage = 'FORM' | 'CONFIRM' | 'RESULT';
74
+
75
+ export default function BillsScreen({ category, amount, user, onClose, onError }: Props) {
76
+ const [stage, setStage] = useState<FlowStage>('FORM');
77
+ const [loading, setLoading] = useState(false);
78
+ const [pendingPayload, setPendingPayload] = useState<FPBillPurchaseRequest | null>(null);
79
+ const [pendingSummaryRows, setPendingSummaryRows] = useState<FPSummaryRow[]>([]);
80
+ const [completedTx, setCompletedTx] = useState<FPBillTransaction | null>(null);
81
+
82
+ const handleFormContinue = (payload: FPBillPurchaseRequest, summaryRows: FPSummaryRow[]) => {
83
+ setPendingPayload(payload);
84
+ setPendingSummaryRows(summaryRows);
85
+ setStage('CONFIRM');
86
+ };
87
+
88
+ const handleResultClose = () => {
89
+ setCompletedTx(null);
90
+ setStage('FORM');
91
+ onClose();
92
+ };
93
+
94
+ const handlePinAuthorized = async (tempId: string) => {
95
+ if (!pendingPayload) return;
96
+ setLoading(true);
97
+ try {
98
+ let response;
99
+ switch (pendingPayload.category) {
100
+ case 'AIRTIME':
101
+ response = await billsAPI.purchaseAirtime(pendingPayload, tempId);
102
+ break;
103
+ case 'DATA':
104
+ response = await billsAPI.purchaseData(pendingPayload, tempId);
105
+ break;
106
+ case 'ELECTRICITY':
107
+ response = await billsAPI.purchaseElectricity(pendingPayload, tempId);
108
+ break;
109
+ case 'CABLE':
110
+ response = await billsAPI.purchaseCable(pendingPayload, tempId);
111
+ break;
112
+ }
113
+
114
+ if (!response?.status) {
115
+ onError?.({ code: 'PURCHASE_FAILED', message: response?.message ?? 'Purchase failed' } as FPError);
116
+ // Still show the result screen — ResultScreen polls status itself
117
+ // and renders the failed state, same pattern SendScreen uses.
118
+ }
119
+ setCompletedTx(response?.payload ?? null);
120
+ setStage('RESULT');
121
+ } catch (error) {
122
+ onError?.(error as FPError);
123
+ } finally {
124
+ setLoading(false);
125
+ }
126
+ };
127
+
128
+ const renderForm = () => {
129
+ switch (category) {
130
+ case 'AIRTIME':
131
+ return <AirtimeSubScreen amount={amount} onProcessTransaction={handleFormContinue} onError={onError} />;
132
+ case 'DATA':
133
+ return <DataSubScreen onProcessTransaction={handleFormContinue} onError={onError} />;
134
+ case 'ELECTRICITY':
135
+ return (
136
+ <ElectricitySubScreen
137
+ amount={amount}
138
+ user={user}
139
+ onProcessTransaction={handleFormContinue}
140
+ onError={onError}
141
+ />
142
+ );
143
+ case 'CABLE':
144
+ return <CableSubScreen onProcessTransaction={handleFormContinue} onError={onError} />;
145
+ }
146
+ };
147
+
148
+ if (stage === 'RESULT' && completedTx) {
149
+ return (
150
+ <ResultScreen
151
+ reference={completedTx.reference}
152
+ summaryRows={pendingSummaryRows}
153
+ allowRecurring={false}
154
+ statusFetcher={billsAPI.status}
155
+ onClose={handleResultClose}
156
+ />
157
+
158
+ );
159
+ }
160
+
161
+ return (
162
+ <Container>
163
+ {loading && <LoadingAnimation text="Processing..." />}
164
+
165
+ {stage === 'FORM' && (
166
+ <>
167
+ <Header>
168
+ <HeaderTitle>{CATEGORY_TITLES[category]}</HeaderTitle>
169
+ <CloseBtn onPress={onClose}>
170
+ <Ionicons name="close" size={20} color={C.ink} />
171
+ </CloseBtn>
172
+ </Header>
173
+ {renderForm()}
174
+ </>
175
+ )}
176
+
177
+ {stage === 'CONFIRM' && pendingPayload && (
178
+ <>
179
+ <Header>
180
+ <HeaderTitle>Confirm Purchase</HeaderTitle>
181
+ <CloseBtn onPress={() => setStage('FORM')}>
182
+ <Ionicons name="close" size={20} color={C.ink} />
183
+ </CloseBtn>
184
+ </Header>
185
+ <ConfirmScreen
186
+ amount={pendingPayload.amountInKobo / 100}
187
+ currency="₦"
188
+ subtitle="Double check the details before you proceed. Please note that successful bill payments cannot be reversed."
189
+ summaryRows={pendingSummaryRows}
190
+ validate={(pin: string) => billsAPI.validateBillPin(pin, getFPStore().psspId || '')}
191
+ onContinue={handlePinAuthorized}
192
+ />
193
+ </>
194
+ )}
195
+ </Container>
196
+ );
197
+ }