react-native-fpay 0.2.4 → 0.2.7

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 (39) hide show
  1. package/lib/module/ui/modals/FPShell.js +120 -76
  2. package/lib/module/ui/modals/FPShell.js.map +1 -1
  3. package/lib/module/ui/screens/ReceiveScreen.js +0 -2
  4. package/lib/module/ui/screens/ReceiveScreen.js.map +1 -1
  5. package/lib/module/ui/screens/SendScreen.js +454 -168
  6. package/lib/module/ui/screens/SendScreen.js.map +1 -1
  7. package/lib/module/ui/screens/sub/BluetoothSubScreen.js +0 -1
  8. package/lib/module/ui/screens/sub/BluetoothSubScreen.js.map +1 -1
  9. package/lib/module/ui/screens/sub/NFCSubScreen.js +2 -7
  10. package/lib/module/ui/screens/sub/NFCSubScreen.js.map +1 -1
  11. package/lib/module/ui/screens/sub/NQRSubScreen.js +0 -5
  12. package/lib/module/ui/screens/sub/NQRSubScreen.js.map +1 -1
  13. package/lib/module/ui/screens/sub/ProximitySubScreen.js.map +1 -1
  14. package/lib/module/ui/screens/sub/TransferSubScreen.js +1 -9
  15. package/lib/module/ui/screens/sub/TransferSubScreen.js.map +1 -1
  16. package/lib/typescript/src/ui/modals/FPShell.d.ts.map +1 -1
  17. package/lib/typescript/src/ui/screens/SendScreen.d.ts +2 -2
  18. package/lib/typescript/src/ui/screens/SendScreen.d.ts.map +1 -1
  19. package/lib/typescript/src/ui/screens/sub/BluetoothSubScreen.d.ts +1 -2
  20. package/lib/typescript/src/ui/screens/sub/BluetoothSubScreen.d.ts.map +1 -1
  21. package/lib/typescript/src/ui/screens/sub/NFCSubScreen.d.ts +1 -2
  22. package/lib/typescript/src/ui/screens/sub/NFCSubScreen.d.ts.map +1 -1
  23. package/lib/typescript/src/ui/screens/sub/NQRSubScreen.d.ts +1 -2
  24. package/lib/typescript/src/ui/screens/sub/NQRSubScreen.d.ts.map +1 -1
  25. package/lib/typescript/src/ui/screens/sub/ProximitySubScreen.d.ts +0 -1
  26. package/lib/typescript/src/ui/screens/sub/ProximitySubScreen.d.ts.map +1 -1
  27. package/lib/typescript/src/ui/screens/sub/TransferSubScreen.d.ts +1 -2
  28. package/lib/typescript/src/ui/screens/sub/TransferSubScreen.d.ts.map +1 -1
  29. package/package.json +2 -2
  30. package/src/ui/modals/FPShell.tsx +149 -49
  31. package/src/ui/screens/ReceiveScreen.tsx +2 -2
  32. package/src/ui/screens/SendScreen.tsx +266 -53
  33. package/src/ui/screens/sub/BluetoothSubScreen.tsx +6 -3
  34. package/src/ui/screens/sub/NFCSubScreen.tsx +7 -4
  35. package/src/ui/screens/sub/NQRSubScreen.tsx +6 -5
  36. package/src/ui/screens/sub/ProximitySubScreen.tsx +5 -3
  37. package/src/ui/screens/sub/TransferSubScreen.tsx +6 -5
  38. package/android/src/main/java/com/fpay/FpayModule.kt +0 -15
  39. package/android/src/main/java/com/fpay/FpayPackage.kt +0 -31
@@ -5,16 +5,16 @@
5
5
  // renders the appropriate full-screen sheet.
6
6
  // ─────────────────────────────────────────────
7
7
  import React, { useEffect, useState, useRef } from 'react';
8
- import { Modal, View, Text, TouchableOpacity, StyleSheet, Animated, Dimensions, SafeAreaView } from 'react-native';
8
+ import { Modal, View, Text, TouchableOpacity, StyleSheet, Animated, Dimensions, SafeAreaView, StatusBar, Platform } from 'react-native';
9
9
  import { _onEvent } from '../../engine/FPEngine';
10
- import { SendScreen } from '../screens/SendScreen';
10
+ import SendScreen from '../screens/SendScreen';
11
11
  import { ReceiveScreen } from '../screens/ReceiveScreen';
12
12
  import { FPPaymentRequestModal } from './FPPaymentRequestModal';
13
13
  import { FPEngine } from '../../engine/FPEngine';
14
14
  import { C, S, F, shadow } from '../theme';
15
15
  import type { FPCurrency } from '../../core/types';
16
16
 
17
- const { height: SCREEN_H } = Dimensions.get('window');
17
+ const { width: SCREEN_W, height: SCREEN_H } = Dimensions.get('window');
18
18
 
19
19
  type SheetMode = 'send' | 'receive' | null;
20
20
 
@@ -26,81 +26,181 @@ interface SheetState {
26
26
 
27
27
  export function FPShell() {
28
28
  const [sheet, setSheet] = useState<SheetState>({ mode: null });
29
+ const [visible, setVisible] = useState(false);
29
30
  const slide = useRef(new Animated.Value(SCREEN_H)).current;
30
31
 
31
32
  const open = (state: SheetState) => {
32
33
  setSheet(state);
33
- Animated.spring(slide, { toValue: 0, useNativeDriver: true, tension: 65, friction: 11 }).start();
34
+ setVisible(true);
35
+ Animated.spring(slide, {
36
+ toValue: 0,
37
+ useNativeDriver: true,
38
+ tension: 60,
39
+ friction: 12,
40
+ }).start();
34
41
  };
35
42
 
36
43
  const close = () => {
37
- Animated.timing(slide, { toValue: SCREEN_H, duration: 260, useNativeDriver: true }).start(() => {
44
+ Animated.timing(slide, {
45
+ toValue: SCREEN_H,
46
+ duration: 280,
47
+ useNativeDriver: true,
48
+ }).start(() => {
49
+ setVisible(false);
38
50
  setSheet({ mode: null });
39
51
  });
40
52
  };
41
53
 
42
54
  useEffect(() => {
43
- const unsubSend = _onEvent('show_send', (d: unknown) => { const data = d as any; open({ mode: 'send', amount: data?.amount, currency: data?.currency }); });
44
- const unsubReceive = _onEvent('show_receive', (d: unknown) => { const data = d as any; open({ mode: 'receive', amount: data?.amount, currency: data?.currency }); });
55
+ const unsubSend = _onEvent('show_send', (d: unknown) => {
56
+ const data = d as any;
57
+ open({ mode: 'send', amount: data?.amount, currency: data?.currency });
58
+ });
59
+ const unsubReceive = _onEvent('show_receive', (d: unknown) => {
60
+ const data = d as any;
61
+ open({ mode: 'receive', amount: data?.amount, currency: data?.currency });
62
+ });
45
63
  return () => { unsubSend(); unsubReceive(); };
46
64
  }, []);
47
65
 
66
+ const title = sheet.mode === 'send' ? 'Send Money' : 'Receive Money';
67
+ const accentColor = sheet.mode === 'send' ? C.brand : C.green;
68
+
69
+
48
70
  return (
49
71
  <>
50
72
  {/* Always-on BT payment request modal */}
51
73
  <FPPaymentRequestModal />
52
74
 
53
- {/* Send / Receive sheet */}
54
- <Modal visible={sheet.mode !== null} transparent animationType="none" statusBarTranslucent onRequestClose={close}>
55
- <TouchableOpacity style={st.scrim} activeOpacity={1} onPress={close} />
56
- <Animated.View style={[st.sheet, { transform: [{ translateY: slide }] }]}>
57
- <SafeAreaView style={{ flex: 1 }}>
58
- {/* Handle + header */}
59
- <View style={st.handle} />
75
+ {visible && (
76
+ <Animated.View
77
+ style={[st.fullscreen, { transform: [{ translateY: slide }] }]}
78
+ >
79
+ <StatusBar barStyle="dark-content" backgroundColor={C.white} />
80
+
81
+ <SafeAreaView style={st.safeArea}>
82
+ {/* ── Header ───────────────────────────────────────── */}
60
83
  <View style={st.header}>
61
- <Text style={st.headerTitle}>
62
- {sheet.mode === 'send' ? 'Send Money' : 'Receive Money'}
63
- </Text>
64
- <TouchableOpacity onPress={close} style={st.closeBtn}>
65
- <Text style={st.closeText}>Done</Text>
84
+ {/* Close button — left side */}
85
+ <TouchableOpacity onPress={close} style={st.closeBtn} activeOpacity={0.7} hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}>
86
+ <View style={st.closeIcon}>
87
+ <View style={[st.closeLine, { transform: [{ rotate: '45deg' }] }]} />
88
+ <View style={[st.closeLine, { transform: [{ rotate: '-45deg' }], position: 'absolute' }]} />
89
+ </View>
66
90
  </TouchableOpacity>
91
+
92
+ {/* Title — center */}
93
+ <View style={st.titleWrap}>
94
+ <Text style={st.title}>{title}</Text>
95
+ </View>
96
+
97
+ {/* Accent dot — right side (balances the close button visually) */}
98
+ <View style={[st.dot, { backgroundColor: accentColor }]} />
67
99
  </View>
68
100
 
69
- {/* Read user + callbacks fresh at render time — not at mount time.
70
- This ensures they're populated even if initializeSDK ran after
71
- the shell mounted (which is always the case). */}
72
- {sheet.mode === 'send' && (
73
- <SendScreen
74
- amount={sheet.amount ?? 0}
75
- currency={sheet.currency ?? 'NGN'}
76
- onClose={close}
77
- onPaymentSent={(tx) => { FPEngine.getCallbacks().onPaymentSent?.(tx); close(); }}
78
- onError={FPEngine.getCallbacks().onError}
79
- />
80
- )}
81
- {sheet.mode === 'receive' && (
82
- <ReceiveScreen
83
- amount={sheet.amount}
84
- currency={sheet.currency}
85
- user={FPEngine.getUser() ?? undefined}
86
- onClose={close}
87
- onPaymentReceived={FPEngine.getCallbacks().onPaymentReceived}
88
- onError={FPEngine.getCallbacks().onError}
89
- />
90
- )}
101
+ {/* ── Divider ───────────────────────────────────────── */}
102
+ <View style={st.divider} />
103
+
104
+ {/* ── Content ───────────────────────────────────────── */}
105
+ <View style={st.content}>
106
+ {sheet.mode === 'send' && (
107
+ <SendScreen
108
+ amount={sheet.amount ?? 0}
109
+ currency={sheet.currency ?? 'NGN'}
110
+ onClose={close}
111
+ onPaymentSent={(tx) => {
112
+ FPEngine.getCallbacks().onPaymentSent?.(tx);
113
+ close();
114
+ }}
115
+ onError={FPEngine.getCallbacks().onError}
116
+ />
117
+ )}
118
+ {sheet.mode === 'receive' && (
119
+ <ReceiveScreen
120
+ amount={sheet.amount}
121
+ currency={sheet.currency}
122
+ user={FPEngine.getUser() ?? undefined}
123
+ onClose={close}
124
+ onPaymentReceived={FPEngine.getCallbacks().onPaymentReceived}
125
+ onError={FPEngine.getCallbacks().onError}
126
+ />
127
+ )}
128
+ </View>
91
129
  </SafeAreaView>
92
130
  </Animated.View>
93
- </Modal>
131
+ )}
94
132
  </>
95
133
  );
96
134
  }
97
135
 
98
136
  const st = StyleSheet.create({
99
- scrim: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(11,29,53,0.55)' },
100
- sheet: { position: 'absolute', bottom: 0, left: 0, right: 0, height: SCREEN_H * 0.92, backgroundColor: C.white, borderTopLeftRadius: 24, borderTopRightRadius: 24, ...shadow.lg },
101
- handle: { width: 40, height: 4, borderRadius: 2, backgroundColor: '#DFE1E6', alignSelf: 'center', marginTop: S.sm },
102
- header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: S.lg, paddingVertical: S.md, borderBottomWidth: 1, borderBottomColor: '#DFE1E6' },
103
- headerTitle: { fontSize: F.lg, fontWeight: '800', color: C.ink },
104
- closeBtn: { paddingHorizontal: S.sm, paddingVertical: 4 },
105
- closeText: { fontSize: F.md, color: '#0052CC', fontWeight: '700' },
137
+ fullscreen: {
138
+ position: 'absolute',
139
+ top: 0,
140
+ left: 0,
141
+ width: SCREEN_W,
142
+ height: SCREEN_H,
143
+ backgroundColor: C.white,
144
+ zIndex: 9999,
145
+ elevation: 99,
146
+ },
147
+ safeArea: {
148
+ flex: 1,
149
+ backgroundColor: C.white,
150
+ },
151
+ header: {
152
+ flexDirection: 'row',
153
+ alignItems: 'center',
154
+ justifyContent: 'space-between',
155
+ paddingHorizontal: S.lg,
156
+ paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) + S.sm : S.md,
157
+ paddingBottom: S.md,
158
+ backgroundColor: C.white,
159
+ },
160
+ closeBtn: {
161
+ width: 36,
162
+ height: 36,
163
+ borderRadius: 18,
164
+ backgroundColor: C.surface,
165
+ justifyContent: 'center',
166
+ alignItems: 'center',
167
+ },
168
+ closeIcon: {
169
+ width: 14,
170
+ height: 14,
171
+ justifyContent: 'center',
172
+ alignItems: 'center',
173
+ },
174
+ closeLine: {
175
+ width: 14,
176
+ height: 2,
177
+ borderRadius: 1,
178
+ backgroundColor: C.ink,
179
+ },
180
+ titleWrap: {
181
+ flex: 1,
182
+ alignItems: 'center',
183
+ },
184
+ title: {
185
+ fontSize: F.lg,
186
+ fontWeight: '800',
187
+ color: C.ink,
188
+ letterSpacing: -0.3,
189
+ },
190
+ dot: {
191
+ width: 8,
192
+ height: 8,
193
+ borderRadius: 4,
194
+ marginRight: 14, // visually balances the close button width
195
+ },
196
+ divider: {
197
+ height: 1,
198
+ backgroundColor: C.border,
199
+ marginHorizontal: 0,
200
+ },
201
+ content: {
202
+ flex: 1,
203
+ backgroundColor: C.surface,
204
+ },
106
205
  });
206
+
@@ -30,8 +30,8 @@ export function ReceiveScreen({ amount, currency = 'NGN', user, onClose, onPayme
30
30
  bankCode: user?.bankCode ?? '',
31
31
  };
32
32
 
33
- if (channel === 'nqr') return <NQRSubScreen mode="receive" amount={amount} currency={currency} onBack={() => setChannel(null)} onDone={onClose} onError={onError} />;
34
- if (channel === 'nfc') return <NFCSubScreen mode="receive" myWallet={myWallet} onBack={() => setChannel(null)} onDone={onClose} onError={onError} />;
33
+ if (channel === 'nqr') return <NQRSubScreen mode="receive" amount={amount} currency={currency} onDone={onClose} onError={onError} />;
34
+ if (channel === 'nfc') return <NFCSubScreen mode="receive" myWallet={myWallet} onDone={onClose} onError={onError} />;
35
35
 
36
36
  // Transfer — just show account details inline
37
37
  if (channel === 'transfer') return (
@@ -4,7 +4,7 @@
4
4
  // relevant sub-screen in the same modal sheet
5
5
  // ─────────────────────────────────────────────
6
6
  import React, { useState } from 'react';
7
- import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
7
+ import { View, Text, TouchableOpacity, StyleSheet, ScrollView, Dimensions, TextInput } from 'react-native';
8
8
  import { C, R, S, F, shadow } from '../theme';
9
9
  import { FPButton } from '../components/FPButton';
10
10
  import { TransferSubScreen } from './sub/TransferSubScreen';
@@ -13,15 +13,20 @@ import { ProximitySubScreen } from './sub/ProximitySubScreen';
13
13
  import { BluetoothSubScreen } from './sub/BluetoothSubScreen';
14
14
  import { NFCSubScreen } from './sub/NFCSubScreen';
15
15
  import type { FPCurrency, FPCallbacks } from '../../core/types';
16
+ import styled from 'styled-components/native';
17
+ import Ionicons from 'react-native-vector-icons/Ionicons';
18
+ import Svg, { Path } from 'react-native-svg';
16
19
 
17
- type Channel = 'transfer' | 'nqr' | 'proximity' | 'bluetooth' | 'nfc';
20
+ const { width, height } = Dimensions.get('window');
21
+
22
+ type Channel = 'TRANSFER' | 'QRCODE' | 'NFC' | 'BTH' | 'PXTR';
18
23
 
19
24
  const CHANNELS: { id: Channel; emoji: string; title: string; desc: string }[] = [
20
- { id: 'transfer', emoji: '🏦', title: 'Bank Transfer', desc: 'Send to any Nigerian bank account' },
21
- { id: 'nqr', emoji: '⬛', title: 'NQR Scan', desc: 'Scan a merchant or person QR code' },
22
- { id: 'proximity', emoji: '📡', title: 'Nearby', desc: 'People within 100m of you' },
23
- { id: 'bluetooth', emoji: '📶', title: 'Bluetooth', desc: 'Pair wirelessly and send' },
24
- { id: 'nfc', emoji: '📲', title: 'NFC Tap', desc: 'Tap another phone to pay' },
25
+ { id: 'TRANSFER', emoji: '🏦', title: 'Bank Transfer', desc: 'Send to any Nigerian bank account' },
26
+ { id: 'QRCODE', emoji: '⬛', title: 'NQR Scan', desc: 'Scan a merchant or person QR code' },
27
+ { id: 'PXTR', emoji: '📡', title: 'Nearby', desc: 'People within 100m of you' },
28
+ { id: 'BTH', emoji: '📶', title: 'Bluetooth', desc: 'Pair wirelessly and send' },
29
+ { id: 'NFC', emoji: '📲', title: 'NFC Tap', desc: 'Tap another phone to pay' },
25
30
  ];
26
31
 
27
32
  interface Props extends FPCallbacks {
@@ -30,57 +35,265 @@ interface Props extends FPCallbacks {
30
35
  onClose: () => void;
31
36
  }
32
37
 
33
- export function SendScreen({ amount, currency, onClose, onPaymentSent, onError }: Props) {
34
- const [channel, setChannel] = useState<Channel | null>(null);
38
+
39
+ const Container = styled(View)`
40
+ flex: 1;
41
+ background-color: #000000;
42
+ `;
43
+
44
+
45
+ // Bottom Sheet Styles
46
+ const BottomSheet = styled(View)`
47
+ position: absolute;
48
+ bottom: 0;
49
+ left: 0;
50
+ right: 0;
51
+ background-color: #ffffff;
52
+ border-top-left-radius: 24px;
53
+ border-top-right-radius: 24px;
54
+ padding: 20px 20px 32px;
55
+ elevation: 20;
56
+ shadow-color: #000;
57
+ shadow-offset: 0px -4px;
58
+ shadow-opacity: 0.15;
59
+ shadow-radius: 12px;
60
+ max-height: ${height * 0.9}px;
61
+ `;
62
+
63
+ const SheetHandle = styled(View)`
64
+ width: 40px;
65
+ height: 4px;
66
+ background-color: #d1d5db;
67
+ border-radius: 2px;
68
+ align-self: center;
69
+ margin-bottom: 20px;
70
+ `;
71
+
72
+ const SheetScrollView = styled(ScrollView)`
73
+ flex: 1;
74
+ `;
75
+
76
+ const SectionTitle = styled(Text)`
77
+ font-size: 18px;
78
+ font-weight: 700;
79
+ color: #1f2937;
80
+ margin-bottom: 16px;
81
+ `;
82
+
83
+ const ActionsRow = styled(View)`
84
+ flex-direction: row;
85
+ gap: 16px;
86
+ margin-bottom: 24px;
87
+ `;
88
+
89
+ const ActionButton = styled(TouchableOpacity)`
90
+ align-items: center;
91
+ gap: 8px;
92
+ flex: 1;
93
+ `;
94
+
95
+ const ActionIconContainer = styled(View)<{ color: string }>`
96
+ width: 56px;
97
+ height: 56px;
98
+ border-radius: 100%;
99
+ background-color: ${props => props.color};
100
+ justify-content: center;
101
+ align-items: center;
102
+ elevation: 3;
103
+ shadow-color: #000;
104
+ shadow-offset: 0px 2px;
105
+ shadow-opacity: 0.12;
106
+ shadow-radius: 4px;
107
+ `;
108
+
109
+
110
+
111
+ const ActionLabel = styled(Text)`
112
+ font-size: 11px;
113
+ color: #374151;
114
+ text-align: center;
115
+ line-height: 14px;
116
+ `;
117
+
118
+ const SearchContainer = styled(View)`
119
+ flex-direction: row;
120
+ align-items: center;
121
+ background-color: #f9fafb;
122
+ border-radius: 12px;
123
+ padding: 14px 16px;
124
+ margin-bottom: 24px;
125
+ border: 1px solid #e5e7eb;
126
+ `;
127
+
128
+ const SearchInput = styled(TextInput)`
129
+ flex: 1;
130
+ margin-left: 10px;
131
+ font-size: 14px;
132
+ color: #1f2937;
133
+ padding: 0;
134
+ `;
135
+
136
+ const AddContactButton = styled.TouchableOpacity`
137
+ align-items: center;
138
+ margin-right: 20px;
139
+
140
+ flex-direction: row;
141
+ gap: 20px;
142
+
143
+ `;
144
+
145
+ const Icon = styled(Text)<{ size?: number; color?: string }>`
146
+ font-size: ${props => props.size || 24}px;
147
+ color: ${props => props.color || '#000000'};
148
+ `;
149
+
150
+ const AddCircle = styled.View`
151
+ width: 44px;
152
+ height: 44px;
153
+ border-radius: 32px;
154
+ background-color: #fff;
155
+ border-width: 2px;
156
+ border-color: #e0e0e0;
157
+ border-style: dashed;
158
+ justify-content: center;
159
+ align-items: center;
160
+ margin-bottom: 8px;
161
+ `;
162
+
163
+ const PrxyIconContainer = styled(View)<{ color: string }>`
164
+ width: 44px;
165
+ height: 44px;
166
+ border-radius: 100%;
167
+ background-color: ${props => props.color};
168
+ justify-content: center;
169
+ align-items: center;
170
+ elevation: 3;
171
+ shadow-color: #000;
172
+ shadow-offset: 0px 2px;
173
+ shadow-opacity: 0.12;
174
+ shadow-radius: 4px;
175
+ `;
176
+
177
+ const ContactName = styled.Text`
178
+ font-size: 12px;
179
+ color: #000;
180
+ `;
181
+
182
+ const ScanIcon = ({ color = "#111", size = 22 }) => (
183
+ <Svg width={size} height={size} strokeWidth="0.9" viewBox="0 0 24 24" fill="none" color={color}>
184
+ <Path d="M6 3H3V6" stroke="#000000" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
185
+ <Path d="M2 12H12L22 12" stroke="#000000" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
186
+ <Path d="M9 19V17V15" stroke="#000000" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
187
+ <Path d="M12 16V15.5V15" stroke="#000000" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
188
+ <Path d="M15 17V16V15" stroke="#000000" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
189
+ <Path d="M12 21V19.5V18" stroke="#000000" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
190
+ <Path d="M18 3H21V6" stroke="#000000" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
191
+ <Path d="M6 21H3V18" stroke="#000000" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
192
+ <Path d="M18 21H21V18" stroke="#000000" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
193
+ </Svg>
194
+ );
195
+
196
+ const BluetoothIcon =({ color = "#111", size = 22 })=>(
197
+ <Svg width={size} height={size} strokeWidth="0.9" viewBox="0 0 24 24" fill="none" color={color}>
198
+ <Path d="M6 19.0007C3.57111 17.1763 2 14.2716 2 11C2 5.47715 6.47715 1 12 1C17.5228 1 22 5.47715 22 11C22 14.2716 20.4289 17.1763 18 19.0007" stroke="#fff" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
199
+ <Path d="M6 19.0007C3.57111 17.1763 2 14.2716 2 11C2 5.47715 6.47715 1 12 1C17.5228 1 22 5.47715 22 11C22 14.2716 20.4289 17.1763 18 19.0007" stroke="#fff" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
200
+ <Path d="M7.52779 15C6.57771 13.9385 6 12.5367 6 11C6 7.68629 8.68629 5 12 5C15.3137 5 18 7.68629 18 11C18 12.5367 17.4223 13.9385 16.4722 15" stroke="#fff" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
201
+ <Path fillRule="evenodd" clipRule="evenodd" d="M9.25 11C9.25 9.48122 10.4812 8.25 12 8.25C13.5188 8.25 14.75 9.48122 14.75 11C14.75 12.5188 13.5188 13.75 12 13.75C10.4812 13.75 9.25 12.5188 9.25 11Z" fill="#fff" />
202
+ <Path d="M15.0776 21.4865C14.8566 22.8126 13.7093 23.7844 12.365 23.7844H11.7536C10.4093 23.7844 9.262 22.8126 9.041 21.4865L8.53213 18.4333C8.29232 16.9946 9.43086 15.8854 10.5339 15.15C11.9123 14.231 12.3864 14.3406 13.5847 15.1396C14.6421 15.8445 15.8263 16.9946 15.5865 18.4333L15.0776 21.4865Z" fill="#fff" />
203
+ </Svg>
204
+ )
205
+
206
+ const NFCIcon =({ color = "#111", size = 22 })=>(
207
+ <Svg width={size} height={size} strokeWidth="0.9" viewBox="0 0 24 24" fill="none" color={color}>
208
+ <Path d="M12 19.51L12.01 19.4989" stroke="#fff" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
209
+ <Path d="M2 8C8 3.5 16 3.5 22 8" stroke="#fff" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
210
+ <Path d="M5 12C9 9 15 9 19 12" stroke="#fff" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
211
+ <Path d="M8.5 15.5C10.7504 14.1 13.2498 14.0996 15.5001 15.5" stroke="#fff" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" />
212
+ </Svg>
213
+ )
214
+
215
+ const QRIcon =({ color = "#111", size = 22 })=>(
216
+ <Svg width={size} height={size} strokeWidth="0.9" viewBox="0 0 24 24" fill="none" color={color}>
217
+ <Path fillRule="evenodd" clipRule="evenodd" d="M2.25 3C2.25 2.58579 2.58579 2.25 3 2.25H6C6.41421 2.25 6.75 2.58579 6.75 3C6.75 3.41421 6.41421 3.75 6 3.75H3.75V6C3.75 6.41421 3.41421 6.75 3 6.75C2.58579 6.75 2.25 6.41421 2.25 6V3Z" fill="#fff" />
218
+ <Path fillRule="evenodd" clipRule="evenodd" d="M17.25 3C17.25 2.58579 17.5858 2.25 18 2.25H21C21.4142 2.25 21.75 2.58579 21.75 3V6C21.75 6.41421 21.4142 6.75 21 6.75C20.5858 6.75 20.25 6.41421 20.25 6V3.75H18C17.5858 3.75 17.25 3.41421 17.25 3Z" fill="#fff" />
219
+ <Path fillRule="evenodd" clipRule="evenodd" d="M3 17.25C3.41421 17.25 3.75 17.5858 3.75 18V20.25H6C6.41421 20.25 6.75 20.5858 6.75 21C6.75 21.4142 6.41421 21.75 6 21.75H3C2.58579 21.75 2.25 21.4142 2.25 21V18C2.25 17.5858 2.58579 17.25 3 17.25Z" fill="#fff" />
220
+ <Path fillRule="evenodd" clipRule="evenodd" d="M21 17.25C21.4142 17.25 21.75 17.5858 21.75 18V21C21.75 21.4142 21.4142 21.75 21 21.75H18C17.5858 21.75 17.25 21.4142 17.25 21C17.25 20.5858 17.5858 20.25 18 20.25H20.25V18C20.25 17.5858 20.5858 17.25 21 17.25Z" fill="#fff" />
221
+ <Path fillRule="evenodd" clipRule="evenodd" d="M12.9004 6.6654C12.3462 6.33289 11.6538 6.33289 11.0996 6.6654L7.09963 9.0654C6.57252 9.38167 6.25 9.95131 6.25 10.566V14.4336C6.25 15.0483 6.57252 15.618 7.09963 15.9342L11.0996 18.3342C11.6538 18.6668 12.3462 18.6668 12.9004 18.3342L16.9004 15.9342C17.4275 15.618 17.75 15.0483 17.75 14.4336V10.566C17.75 9.95131 17.4275 9.38167 16.9004 9.0654L12.9004 6.6654ZM9.3642 10.6785C9.00209 10.4773 8.5455 10.6078 8.34437 10.9699C8.14324 11.332 8.27373 11.7886 8.63583 11.9898L11.25 13.4418V16.0001C11.25 16.4144 11.5858 16.7501 12 16.7501C12.4142 16.7501 12.75 16.4144 12.75 16.0001V13.4456C12.9152 13.3554 13.1243 13.241 13.3607 13.1115C13.9447 12.7916 14.6961 12.3787 15.3642 12.0077C15.7263 11.8066 15.8568 11.35 15.6557 10.9879C15.4546 10.6257 14.998 10.4952 14.6359 10.6964C13.9716 11.0653 13.223 11.4766 12.6401 11.796C12.3908 11.9325 12.172 12.0521 12.0032 12.1443L9.3642 10.6785Z" fill="#fff" />
222
+ </Svg>
223
+ );
224
+
225
+ const ProximityIcon =({ color = "#111", size = 22 })=>(
226
+ <Svg width={size} height={size} strokeWidth="0.9" viewBox="0 0 24 24" fill="none" color={color}>
227
+ <Path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke={color} strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round"/>
228
+ <Path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" fill={color} stroke={color} strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round"/>
229
+ <Path d="M19 19L17.5 17.5" stroke={color} strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round"/>
230
+ <Path d="M15.5 15.5L14.5 14.5" stroke={color} strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round"/>
231
+ </Svg>
232
+ )
233
+
234
+
235
+
236
+ const SendScreen = ({ amount, currency, onClose, onPaymentSent, onError }: Props) => {
237
+ const [channel, setChannel] = useState<Channel>('TRANSFER');
35
238
 
36
239
  const formatted = `${currency} ${(amount).toLocaleString('en-NG', { minimumFractionDigits: 2 })}`;
37
240
 
38
- if (channel === 'transfer') return <TransferSubScreen amount={amount} currency={currency} onBack={() => setChannel(null)} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />;
39
- if (channel === 'nqr') return <NQRSubScreen mode="send" amount={amount} currency={currency} onBack={() => setChannel(null)} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />;
40
- if (channel === 'proximity') return <ProximitySubScreen amount={amount} currency={currency} onBack={() => setChannel(null)} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />;
41
- if (channel === 'bluetooth') return <BluetoothSubScreen mode="send" amount={amount} currency={currency} onBack={() => setChannel(null)} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />;
42
- if (channel === 'nfc') return <NFCSubScreen mode="send" amount={amount} currency={currency} onBack={() => setChannel(null)} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />;
241
+
43
242
 
44
243
  return (
45
- <View style={styles.container}>
46
- {/* Amount banner */}
47
- <View style={styles.amountBanner}>
48
- <Text style={styles.amountLabel}>Sending</Text>
49
- <Text style={styles.amountValue}>{formatted}</Text>
50
- </View>
51
-
52
- <Text style={styles.sectionTitle}>How do you want to send?</Text>
53
-
54
- <ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.channelList}>
55
- {CHANNELS.map(ch => (
56
- <TouchableOpacity key={ch.id} style={styles.card} onPress={() => setChannel(ch.id)} activeOpacity={0.8}>
57
- <View style={styles.cardIcon}>
58
- <Text style={styles.cardEmoji}>{ch.emoji}</Text>
59
- </View>
60
- <View style={styles.cardText}>
61
- <Text style={styles.cardTitle}>{ch.title}</Text>
62
- <Text style={styles.cardDesc}>{ch.desc}</Text>
63
- </View>
64
- <Text style={styles.arrow}>›</Text>
65
- </TouchableOpacity>
66
- ))}
67
- </ScrollView>
68
- </View>
244
+ <Container>
245
+ {channel === 'TRANSFER' && (<TransferSubScreen amount={amount} currency={currency} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />)}
246
+ {channel === 'QRCODE' && (<NQRSubScreen mode="send" amount={amount} currency={currency} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />)}
247
+ {channel === 'NFC' && (<NFCSubScreen mode="send" amount={amount} currency={currency} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />)}
248
+ {channel === 'BTH' && (<BluetoothSubScreen mode="send" amount={amount} currency={currency} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />)}
249
+ {channel === 'PXTR' && (<ProximitySubScreen amount={amount} currency={currency} onDone={onClose} onSuccess={onPaymentSent} onError={onError} />)}
250
+
251
+ <BottomSheet>
252
+ <SheetHandle />
253
+
254
+ <SheetScrollView showsVerticalScrollIndicator={false}>
255
+ <SectionTitle>Pay and send</SectionTitle>
256
+
257
+ <ActionsRow>
258
+ <ActionButton onPress={() => setChannel('TRANSFER')}>
259
+ <ActionIconContainer color="#0a3d2e">
260
+ <Ionicons name="business-sharp" size={26} color="#ffffff" />
261
+ </ActionIconContainer>
262
+ <ActionLabel>To bank{'\n'}account</ActionLabel>
263
+ </ActionButton>
264
+
265
+ <ActionButton onPress={() => setChannel('QRCODE')}>
266
+ <ActionIconContainer color="#0a3d2e">
267
+ <QRIcon color="#FFF" size={22} />
268
+ </ActionIconContainer>
269
+ <ActionLabel>Scan{'\n'}QR</ActionLabel>
270
+ </ActionButton>
271
+
272
+ <ActionButton onPress={() => setChannel('PXTR')}>
273
+ <ActionIconContainer color="#0a3d2e">
274
+ <ProximityIcon size={44} color="#FFF" />
275
+ </ActionIconContainer>
276
+ <ActionLabel>Use{'\n'}Proximity</ActionLabel>
277
+ </ActionButton>
278
+
279
+ <ActionButton onPress={() => setChannel('BTH')}>
280
+ <ActionIconContainer color="#0a3d2e">
281
+ <BluetoothIcon size={26} color="#ffffff" />
282
+ </ActionIconContainer>
283
+ <ActionLabel>Use{'\n'}Bluetooth</ActionLabel>
284
+ </ActionButton>
285
+ </ActionsRow>
286
+
287
+ <AddContactButton onPress={()=>setChannel('NFC')}>
288
+ <PrxyIconContainer color="#0a3d2e">
289
+ <NFCIcon size={26} color="#ffffff" />
290
+ </PrxyIconContainer>
291
+ <ContactName>Use NFC payment</ContactName>
292
+ </AddContactButton>
293
+ </SheetScrollView>
294
+ </BottomSheet>
295
+ </Container>
69
296
  );
70
297
  }
71
298
 
72
- const styles = StyleSheet.create({
73
- container: { flex: 1 },
74
- amountBanner: { backgroundColor: C.brand, marginHorizontal: S.lg, borderRadius: R.xl, padding: S.lg, alignItems: 'center', marginBottom: S.lg },
75
- amountLabel: { color: 'rgba(255,255,255,0.7)', fontSize: F.sm, marginBottom: 4 },
76
- amountValue: { color: C.white, fontSize: F.hero, fontWeight: '800' },
77
- sectionTitle: { fontSize: F.sm, color: C.muted, fontWeight: '600', letterSpacing: 0.6, textTransform: 'uppercase', paddingHorizontal: S.lg, marginBottom: S.md },
78
- channelList: { paddingHorizontal: S.lg, paddingBottom: S.xl },
79
- card: { flexDirection: 'row', alignItems: 'center', backgroundColor: C.surface, borderRadius: R.lg, padding: S.md, marginBottom: S.sm, ...shadow.sm },
80
- cardIcon: { width: 48, height: 48, borderRadius: R.md, backgroundColor: C.white, justifyContent: 'center', alignItems: 'center', marginRight: S.md, ...shadow.sm },
81
- cardEmoji: { fontSize: 24 },
82
- cardText: { flex: 1 },
83
- cardTitle: { fontSize: F.md, fontWeight: '700', color: C.ink },
84
- cardDesc: { fontSize: F.sm, color: C.muted, marginTop: 2 },
85
- arrow: { fontSize: F.xl, color: C.ghost },
86
- });
299
+ export default SendScreen;
@@ -199,15 +199,18 @@ interface Props {
199
199
  mode: 'send' | 'receive';
200
200
  amount: number;
201
201
  currency: FPCurrency;
202
- onBack: () => void;
203
202
  onDone: () => void;
204
203
  onSuccess?: (tx: FPTransaction) => void;
205
204
  onError?: (err: FPError) => void;
206
205
  }
207
206
 
208
207
  export function BluetoothSubScreen({
209
- mode, amount, currency,
210
- onBack, onDone, onSuccess, onError,
208
+ mode,
209
+ amount,
210
+ currency,
211
+ onDone,
212
+ onSuccess,
213
+ onError,
211
214
  }: Props) {
212
215
  const [devices, setDevices] = useState<FintechDevice[]>([]);
213
216
  const [scanning, setScanning] = useState(false);