@kiosinc/commons-rn 0.15.7 → 0.17.0

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 (100) hide show
  1. package/README.md +20 -0
  2. package/lib/commonjs/constants/index.js +1 -1
  3. package/lib/commonjs/constants/index.js.map +1 -1
  4. package/lib/commonjs/hooks/index.js +0 -7
  5. package/lib/commonjs/hooks/index.js.map +1 -1
  6. package/lib/commonjs/hooks/useAuth.js +6 -0
  7. package/lib/commonjs/hooks/useAuth.js.map +1 -1
  8. package/lib/commonjs/hooks/useCustomer.js +0 -86
  9. package/lib/commonjs/hooks/useCustomer.js.map +1 -1
  10. package/lib/commonjs/providers/customer/CustomerProvider.js +214 -0
  11. package/lib/commonjs/providers/customer/CustomerProvider.js.map +1 -0
  12. package/lib/commonjs/providers/customer/README.md +122 -0
  13. package/lib/commonjs/providers/customer/classifyLoyaltyError.js +19 -0
  14. package/lib/commonjs/providers/customer/classifyLoyaltyError.js.map +1 -0
  15. package/lib/commonjs/providers/customer/index.js +27 -0
  16. package/lib/commonjs/providers/customer/index.js.map +1 -0
  17. package/lib/commonjs/providers/customer/types.js +2 -0
  18. package/lib/commonjs/providers/customer/types.js.map +1 -0
  19. package/lib/commonjs/providers/customer/useLoyalty.js +25 -0
  20. package/lib/commonjs/providers/customer/useLoyalty.js.map +1 -0
  21. package/lib/commonjs/providers/customer/useSquareCustomer.js +21 -0
  22. package/lib/commonjs/providers/customer/useSquareCustomer.js.map +1 -0
  23. package/lib/commonjs/providers/index.js +19 -0
  24. package/lib/commonjs/providers/index.js.map +1 -1
  25. package/lib/commonjs/screens/SavedCards/SavedCards.js +9 -4
  26. package/lib/commonjs/screens/SavedCards/SavedCards.js.map +1 -1
  27. package/lib/commonjs/utils/analytics.js +21 -0
  28. package/lib/commonjs/utils/analytics.js.map +1 -1
  29. package/lib/module/constants/index.js +1 -1
  30. package/lib/module/constants/index.js.map +1 -1
  31. package/lib/module/hooks/index.js +0 -1
  32. package/lib/module/hooks/index.js.map +1 -1
  33. package/lib/module/hooks/useAuth.js +6 -0
  34. package/lib/module/hooks/useAuth.js.map +1 -1
  35. package/lib/module/hooks/useCustomer.js +1 -87
  36. package/lib/module/hooks/useCustomer.js.map +1 -1
  37. package/lib/module/providers/customer/CustomerProvider.js +204 -0
  38. package/lib/module/providers/customer/CustomerProvider.js.map +1 -0
  39. package/lib/module/providers/customer/README.md +122 -0
  40. package/lib/module/providers/customer/classifyLoyaltyError.js +13 -0
  41. package/lib/module/providers/customer/classifyLoyaltyError.js.map +1 -0
  42. package/lib/module/providers/customer/index.js +4 -0
  43. package/lib/module/providers/customer/index.js.map +1 -0
  44. package/lib/module/providers/customer/types.js +2 -0
  45. package/lib/module/providers/customer/types.js.map +1 -0
  46. package/lib/module/providers/customer/useLoyalty.js +18 -0
  47. package/lib/module/providers/customer/useLoyalty.js.map +1 -0
  48. package/lib/module/providers/customer/useSquareCustomer.js +14 -0
  49. package/lib/module/providers/customer/useSquareCustomer.js.map +1 -0
  50. package/lib/module/providers/index.js +1 -0
  51. package/lib/module/providers/index.js.map +1 -1
  52. package/lib/module/screens/SavedCards/SavedCards.js +10 -5
  53. package/lib/module/screens/SavedCards/SavedCards.js.map +1 -1
  54. package/lib/module/utils/analytics.js +22 -0
  55. package/lib/module/utils/analytics.js.map +1 -1
  56. package/lib/typescript/src/constants/index.d.ts.map +1 -1
  57. package/lib/typescript/src/hooks/index.d.ts +0 -1
  58. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  59. package/lib/typescript/src/hooks/useAuth.d.ts.map +1 -1
  60. package/lib/typescript/src/hooks/useCustomer.d.ts +0 -13
  61. package/lib/typescript/src/hooks/useCustomer.d.ts.map +1 -1
  62. package/lib/typescript/src/providers/customer/CustomerProvider.d.ts +9 -0
  63. package/lib/typescript/src/providers/customer/CustomerProvider.d.ts.map +1 -0
  64. package/lib/typescript/src/providers/customer/classifyLoyaltyError.d.ts +3 -0
  65. package/lib/typescript/src/providers/customer/classifyLoyaltyError.d.ts.map +1 -0
  66. package/lib/typescript/src/providers/customer/index.d.ts +5 -0
  67. package/lib/typescript/src/providers/customer/index.d.ts.map +1 -0
  68. package/lib/typescript/src/providers/customer/types.d.ts +25 -0
  69. package/lib/typescript/src/providers/customer/types.d.ts.map +1 -0
  70. package/lib/typescript/src/providers/customer/useLoyalty.d.ts +11 -0
  71. package/lib/typescript/src/providers/customer/useLoyalty.d.ts.map +1 -0
  72. package/lib/typescript/src/providers/customer/useSquareCustomer.d.ts +7 -0
  73. package/lib/typescript/src/providers/customer/useSquareCustomer.d.ts.map +1 -0
  74. package/lib/typescript/src/providers/index.d.ts +2 -0
  75. package/lib/typescript/src/providers/index.d.ts.map +1 -1
  76. package/lib/typescript/src/screens/SavedCards/SavedCards.d.ts.map +1 -1
  77. package/lib/typescript/src/utils/analytics.d.ts +2 -0
  78. package/lib/typescript/src/utils/analytics.d.ts.map +1 -1
  79. package/package.json +1 -1
  80. package/src/constants/index.ts +1 -1
  81. package/src/hooks/index.ts +0 -1
  82. package/src/hooks/useAuth.ts +6 -0
  83. package/src/hooks/useCustomer.ts +0 -129
  84. package/src/providers/customer/CustomerProvider.tsx +272 -0
  85. package/src/providers/customer/README.md +122 -0
  86. package/src/providers/customer/classifyLoyaltyError.ts +25 -0
  87. package/src/providers/customer/index.ts +4 -0
  88. package/src/providers/customer/types.ts +34 -0
  89. package/src/providers/customer/useLoyalty.ts +27 -0
  90. package/src/providers/customer/useSquareCustomer.ts +19 -0
  91. package/src/providers/index.ts +6 -0
  92. package/src/screens/SavedCards/SavedCards.tsx +10 -3
  93. package/src/utils/analytics.ts +20 -0
  94. package/lib/commonjs/hooks/useCheckCustomer.js +0 -57
  95. package/lib/commonjs/hooks/useCheckCustomer.js.map +0 -1
  96. package/lib/module/hooks/useCheckCustomer.js +0 -49
  97. package/lib/module/hooks/useCheckCustomer.js.map +0 -1
  98. package/lib/typescript/src/hooks/useCheckCustomer.d.ts +0 -5
  99. package/lib/typescript/src/hooks/useCheckCustomer.d.ts.map +0 -1
  100. package/src/hooks/useCheckCustomer.ts +0 -49
@@ -0,0 +1,272 @@
1
+ import React, {
2
+ createContext,
3
+ useCallback,
4
+ useEffect,
5
+ useState,
6
+ type FC,
7
+ type PropsWithChildren,
8
+ } from 'react';
9
+ import auth, { type FirebaseAuthTypes } from '@react-native-firebase/auth';
10
+ import firestore from '@react-native-firebase/firestore';
11
+ import { useMMKVString } from 'react-native-mmkv';
12
+ import { useQueryClient } from '@tanstack/react-query';
13
+ import uuid from 'react-native-uuid';
14
+
15
+ import {
16
+ connectSquareCustomer,
17
+ connectSquareLoyalty,
18
+ createCustomer,
19
+ fetchSquareLoyalty,
20
+ getCustomer,
21
+ } from '../../api/customer';
22
+ import { MMKV_KEYS } from '../../constants';
23
+ import { queryKeys } from '../../hooks/queryKeys';
24
+ import { classifyLoyaltyError } from './classifyLoyaltyError';
25
+ import type {
26
+ CustomerContextValue,
27
+ CustomerLoyalty,
28
+ CustomerStatus,
29
+ LoyaltyStatus,
30
+ } from './types';
31
+
32
+ export interface CustomerProviderProps {
33
+ autoEnroll: boolean;
34
+ loyaltyEnabled: boolean;
35
+ }
36
+
37
+ const customerSetupAttempted = new Set<string>();
38
+
39
+ export const CustomerContext = createContext<CustomerContextValue | null>(null);
40
+
41
+ export const CustomerProvider: FC<PropsWithChildren<CustomerProviderProps>> = ({
42
+ children,
43
+ autoEnroll,
44
+ loyaltyEnabled,
45
+ }) => {
46
+ const queryClient = useQueryClient();
47
+ const [selectedBusinessId] = useMMKVString(MMKV_KEYS.BUSINESS_ID);
48
+
49
+ const [uid, setUid] = useState<string | null>(null);
50
+ const uidRef = React.useRef<string | null>(null);
51
+ const [customerStatus, setCustomerStatus] = useState<CustomerStatus>('idle');
52
+ const [squareCustomerId, setSquareCustomerId] = useState<string | null>(null);
53
+ const [customerError, setCustomerError] = useState<string | null>(null);
54
+ const [loyaltyStatus, setLoyaltyStatus] = useState<LoyaltyStatus>('idle');
55
+ const [loyalty, setLoyaltyData] = useState<CustomerLoyalty | null>(null);
56
+ const [loyaltyError, setLoyaltyError] = useState<string | null>(null);
57
+
58
+ const resetState = useCallback(
59
+ (userId: string | null) => {
60
+ setCustomerStatus('idle');
61
+ setSquareCustomerId(null);
62
+ setCustomerError(null);
63
+ setLoyaltyStatus('idle');
64
+ setLoyaltyData(null);
65
+ setLoyaltyError(null);
66
+ if (userId) {
67
+ for (const key of [...customerSetupAttempted]) {
68
+ if (key.startsWith(`${userId}:`)) {
69
+ customerSetupAttempted.delete(key);
70
+ }
71
+ }
72
+ }
73
+ queryClient.removeQueries([queryKeys.LOYALTY]);
74
+ queryClient.removeQueries([queryKeys.PROGRAM]);
75
+ },
76
+ [queryClient]
77
+ );
78
+
79
+ useEffect(() => {
80
+ const unsubscribe = auth().onAuthStateChanged(
81
+ (user: FirebaseAuthTypes.User | null) => {
82
+ const loggedIn = Boolean(user && !user.isAnonymous);
83
+ if (loggedIn && user) {
84
+ setUid(user.uid);
85
+ uidRef.current = user.uid;
86
+ } else {
87
+ resetState(uidRef.current);
88
+ setUid(null);
89
+ uidRef.current = null;
90
+ }
91
+ }
92
+ );
93
+ return unsubscribe;
94
+ // eslint-disable-next-line react-hooks/exhaustive-deps
95
+ }, []);
96
+
97
+ useEffect(() => {
98
+ if (!uid || !selectedBusinessId) {
99
+ return;
100
+ }
101
+ const key = `${uid}:${selectedBusinessId}`;
102
+ if (customerSetupAttempted.has(key)) {
103
+ return;
104
+ }
105
+ customerSetupAttempted.add(key);
106
+ runCustomerSetup(uid, selectedBusinessId);
107
+ }, [uid, selectedBusinessId]);
108
+
109
+ const runCustomerSetup = async (userId: string, businessId: string) => {
110
+ const key = `${userId}:${businessId}`;
111
+ setCustomerStatus('loading');
112
+ try {
113
+ const customerRes = await getCustomer(userId);
114
+ if (customerRes?.data?.customer === null) {
115
+ await createCustomer({ uid: userId });
116
+ }
117
+ const programs = await firestore()
118
+ .collection(`customers/${userId}/programs`)
119
+ .where('type', '==', 'customer')
120
+ .where('businessId', '==', businessId)
121
+ .get();
122
+
123
+ let custId: string | null = null;
124
+ if (programs.empty) {
125
+ await connectSquareCustomer(userId, {
126
+ businessId,
127
+ uid: userId,
128
+ idempotentKey: uuid.v4() as string,
129
+ });
130
+ const updated = await firestore()
131
+ .collection(`customers/${userId}/programs`)
132
+ .where('type', '==', 'customer')
133
+ .where('businessId', '==', businessId)
134
+ .get();
135
+ if (!updated.empty) {
136
+ custId = updated.docs[0].data().linkedObjectId ?? null;
137
+ }
138
+ } else {
139
+ custId = programs.docs[0].data().linkedObjectId ?? null;
140
+ }
141
+ setSquareCustomerId(custId);
142
+ setCustomerStatus('ready');
143
+ console.log(`[CustomerProvider] Customer ready for ${key}`);
144
+ } catch (err) {
145
+ const msg = err instanceof Error ? err.message : String(err);
146
+ console.warn(
147
+ `[CustomerProvider] Customer setup failed for ${key}: ${msg}`
148
+ );
149
+ customerSetupAttempted.delete(key);
150
+ setCustomerError(msg);
151
+ setCustomerStatus('error');
152
+ }
153
+ };
154
+
155
+ const fetchLoyaltyData = useCallback(
156
+ async (userId: string, businessId: string) => {
157
+ try {
158
+ const response = await fetchSquareLoyalty(userId, businessId);
159
+ if (response?.data?.error) {
160
+ throw new Error(
161
+ response.data.error.message || 'Unknown loyalty error'
162
+ );
163
+ }
164
+ setLoyaltyData(response?.data ?? null);
165
+ setLoyaltyStatus('ready');
166
+ setLoyaltyError(null);
167
+ } catch (err) {
168
+ const msg = err instanceof Error ? err.message : String(err);
169
+ console.log(`[CustomerProvider] Loyalty fetch failed: ${msg}`);
170
+ if (msg.includes('does not have loyalty')) {
171
+ if (autoEnroll) {
172
+ await attemptEnroll(userId, businessId);
173
+ } else {
174
+ setLoyaltyStatus('not_enrolled');
175
+ }
176
+ } else {
177
+ setLoyaltyError(msg);
178
+ setLoyaltyStatus('error');
179
+ }
180
+ }
181
+ },
182
+ // eslint-disable-next-line react-hooks/exhaustive-deps
183
+ [autoEnroll]
184
+ );
185
+
186
+ const attemptEnroll = useCallback(
187
+ async (userId: string, businessId: string) => {
188
+ setLoyaltyStatus('enrolling');
189
+ try {
190
+ const response = await connectSquareLoyalty(userId, {
191
+ businessId,
192
+ uid: userId,
193
+ idempotentKey: uuid.v4() as string,
194
+ });
195
+ if (response?.data?.error) {
196
+ throw new Error(
197
+ response.data.error.message || 'Unknown connect error'
198
+ );
199
+ }
200
+ await fetchLoyaltyData(userId, businessId);
201
+ } catch (err) {
202
+ const msg = err instanceof Error ? err.message : String(err);
203
+ console.log(`[CustomerProvider] Loyalty connect failed: ${msg}`);
204
+ const outcome = classifyLoyaltyError(msg);
205
+ if (outcome === 'refetch') {
206
+ await fetchLoyaltyData(userId, businessId);
207
+ } else if (outcome === 'duplicate') {
208
+ setLoyaltyError('Multiple account records found. Contact support.');
209
+ setLoyaltyStatus('error');
210
+ } else if (outcome === 'no_loyalty') {
211
+ setLoyaltyStatus('no_loyalty');
212
+ } else {
213
+ setLoyaltyError(msg);
214
+ setLoyaltyStatus('error');
215
+ }
216
+ }
217
+ },
218
+ [fetchLoyaltyData]
219
+ );
220
+
221
+ const runLoyaltySetup = useCallback(
222
+ async (userId: string, businessId: string) => {
223
+ setLoyaltyStatus('loading');
224
+ await fetchLoyaltyData(userId, businessId);
225
+ },
226
+ [fetchLoyaltyData]
227
+ );
228
+
229
+ useEffect(() => {
230
+ if (customerStatus !== 'ready' || !uid || !selectedBusinessId) {
231
+ return;
232
+ }
233
+ if (!loyaltyEnabled) {
234
+ setLoyaltyStatus('no_loyalty');
235
+ return;
236
+ }
237
+ runLoyaltySetup(uid, selectedBusinessId);
238
+ // eslint-disable-next-line react-hooks/exhaustive-deps
239
+ }, [customerStatus, loyaltyEnabled, uid, selectedBusinessId]);
240
+
241
+ const enroll = useCallback(() => {
242
+ if (!uid || !selectedBusinessId || loyaltyStatus === 'enrolling') {
243
+ return;
244
+ }
245
+ attemptEnroll(uid, selectedBusinessId);
246
+ }, [uid, selectedBusinessId, loyaltyStatus, attemptEnroll]);
247
+
248
+ const refetchLoyalty = useCallback(() => {
249
+ if (!uid || !selectedBusinessId) {
250
+ return;
251
+ }
252
+ fetchLoyaltyData(uid, selectedBusinessId);
253
+ }, [uid, selectedBusinessId, fetchLoyaltyData]);
254
+
255
+ const value: CustomerContextValue = {
256
+ customerStatus,
257
+ squareCustomerId,
258
+ customerError,
259
+ loyaltyStatus,
260
+ loyalty,
261
+ points: loyalty?.program?.loyaltyDetails?.points ?? 0,
262
+ loyaltyError,
263
+ enroll,
264
+ refetchLoyalty,
265
+ };
266
+
267
+ return (
268
+ <CustomerContext.Provider value={value}>
269
+ {children}
270
+ </CustomerContext.Provider>
271
+ );
272
+ };
@@ -0,0 +1,122 @@
1
+ # CustomerProvider
2
+
3
+ Owns the full customer + loyalty lifecycle. Replaces the old scattered hooks (`useCheckCustomer`, `useFetchSquareLoyalty`, `useConnectSquareLoyalty`, `useFetchSquareProgram`, `useConnectSquareCustomer`).
4
+
5
+ ## What it does
6
+
7
+ 1. Subscribes to Firebase auth — starts setup on login, clears state on logout
8
+ 2. Ensures the customer record exists (`getCustomer` / `createCustomer`)
9
+ 3. Ensures the Square customer program doc exists (`connectSquareCustomer`) — guarded by a module-level `Set` so it never fires twice for the same `uid:businessId`, even across remounts
10
+ 4. Fetches loyalty data — on "does not have loyalty":
11
+ - `autoEnroll=true` (kiosk): calls `connectSquareLoyalty` automatically
12
+ - `autoEnroll=false` (Gusteau): sets `not_enrolled`, waits for `enroll()` call
13
+ 5. Classifies all `connectSquareLoyalty` errors so checkout is never blocked
14
+
15
+ ## Setup
16
+
17
+ Wrap your authenticated navigator with the provider:
18
+
19
+ ```tsx
20
+ import { CustomerProvider } from '@kiosinc/commons-rn';
21
+
22
+ // autoEnroll=true for kiosk (auto-enroll on login)
23
+ // autoEnroll=false for Gusteau (manual enroll via button)
24
+ <CustomerProvider
25
+ autoEnroll
26
+ loyaltyEnabled={isSquareLoyaltyEnabled}>
27
+ <YourNavigator />
28
+ </CustomerProvider>
29
+ ```
30
+
31
+ `businessId` is read internally from MMKV (`MMKV_KEYS.BUSINESS_ID`) — no prop needed.
32
+
33
+ ## Consumer hooks
34
+
35
+ ### `useSquareCustomer()` — for checkout
36
+
37
+ ```tsx
38
+ import { useSquareCustomer } from '@kiosinc/commons-rn';
39
+
40
+ const { squareCustomerId, customerStatus, customerError } = useSquareCustomer();
41
+ ```
42
+
43
+ | Field | Type | Description |
44
+ |---|---|---|
45
+ | `squareCustomerId` | `string \| null` | Pass to `useOptimizedCheckout`. Available even if loyalty is broken. |
46
+ | `customerStatus` | `CustomerStatus` | `idle \| loading \| ready \| error` |
47
+ | `customerError` | `string \| null` | Error message if setup failed |
48
+
49
+ ### `useLoyalty()` — for rewards screens
50
+
51
+ ```tsx
52
+ import { useLoyalty } from '@kiosinc/commons-rn';
53
+
54
+ const {
55
+ loyaltyStatus,
56
+ isReady,
57
+ loyalty,
58
+ points,
59
+ loyaltyError,
60
+ enroll,
61
+ refetchLoyalty,
62
+ } = useLoyalty();
63
+ ```
64
+
65
+ | Field | Type | Description |
66
+ |---|---|---|
67
+ | `loyaltyStatus` | `LoyaltyStatus` | See status machine below |
68
+ | `isReady` | `boolean` | `loyaltyStatus === 'ready'` |
69
+ | `loyalty` | `CustomerLoyalty \| null` | Full loyalty object |
70
+ | `points` | `number` | Available points (0 if not ready) |
71
+ | `loyaltyError` | `string \| null` | Show on error/no_loyalty screens |
72
+ | `enroll()` | `() => void` | Gusteau: call from "Join" button. Noop if already enrolling or autoEnroll=true. |
73
+ | `refetchLoyalty()` | `() => void` | Call after payment to refresh points |
74
+
75
+ ## Status machines
76
+
77
+ ### `customerStatus`
78
+ ```
79
+ idle → loading → ready
80
+ → error
81
+ ```
82
+
83
+ ### `loyaltyStatus`
84
+ ```
85
+ idle → loading → ready
86
+ → not_enrolled (autoEnroll=false, no loyalty account)
87
+ → enrolling → ready
88
+ → error
89
+ → no_loyalty (merchant has no loyalty program)
90
+ → error (duplicate customer docs, etc.)
91
+ ```
92
+
93
+ ## Error handling
94
+
95
+ `connectSquareLoyalty` errors are classified automatically:
96
+
97
+ | Error message | Outcome |
98
+ |---|---|
99
+ | contains `already has` / `ALREADY_EXISTS` / `already exists` | Refetch → `ready` |
100
+ | contains `More than one` / `more than one` | `loyaltyStatus = 'error'`, message: "Multiple account records found. Contact support." |
101
+ | contains `Square Loyalty not enabled` / `not enabled` | `loyaltyStatus = 'no_loyalty'` |
102
+ | anything else | `loyaltyStatus = 'error'`, raw message shown |
103
+
104
+ ## Session reset
105
+
106
+ Provider resets automatically on Firebase auth state change (logout). For kiosk sleep/wake cycles that don't sign out Firebase, sign the user out via `firebase().auth().signOut()` to trigger a full reset.
107
+
108
+ ## Gusteau usage
109
+
110
+ ```tsx
111
+ // In your business selector screen:
112
+ <CustomerProvider autoEnroll={false} loyaltyEnabled={true}>
113
+ <AppNavigator />
114
+ </CustomerProvider>
115
+
116
+ // In your loyalty join screen:
117
+ const { loyaltyStatus, enroll } = useLoyalty();
118
+
119
+ if (loyaltyStatus === 'not_enrolled') {
120
+ return <Button onPress={enroll} title="Join Loyalty Program" />;
121
+ }
122
+ ```
@@ -0,0 +1,25 @@
1
+ export type LoyaltyConnectOutcome =
2
+ | 'refetch'
3
+ | 'duplicate'
4
+ | 'no_loyalty'
5
+ | 'error';
6
+
7
+ export function classifyLoyaltyError(message: string): LoyaltyConnectOutcome {
8
+ if (
9
+ message.includes('already has') ||
10
+ message.includes('ALREADY_EXISTS') ||
11
+ message.toLowerCase().includes('already exists')
12
+ ) {
13
+ return 'refetch';
14
+ }
15
+ if (message.includes('More than one') || message.includes('more than one')) {
16
+ return 'duplicate';
17
+ }
18
+ if (
19
+ message.includes('Square Loyalty not enabled') ||
20
+ message.toLowerCase().includes('not enabled')
21
+ ) {
22
+ return 'no_loyalty';
23
+ }
24
+ return 'error';
25
+ }
@@ -0,0 +1,4 @@
1
+ export { CustomerProvider } from './CustomerProvider';
2
+ export { useSquareCustomer } from './useSquareCustomer';
3
+ export { useLoyalty } from './useLoyalty';
4
+ export type { CustomerStatus, LoyaltyStatus, CustomerLoyalty } from './types';
@@ -0,0 +1,34 @@
1
+ export type CustomerStatus = 'idle' | 'loading' | 'ready' | 'error';
2
+
3
+ export type LoyaltyStatus =
4
+ | 'idle'
5
+ | 'loading'
6
+ | 'not_enrolled'
7
+ | 'enrolling'
8
+ | 'ready'
9
+ | 'no_loyalty'
10
+ | 'error';
11
+
12
+ export interface CustomerLoyalty {
13
+ programId?: string;
14
+ program?: {
15
+ linkedObjectId?: string;
16
+ loyaltyDetails?: {
17
+ points?: number;
18
+ customerId?: string;
19
+ mainProgramId?: string;
20
+ };
21
+ } | null;
22
+ }
23
+
24
+ export interface CustomerContextValue {
25
+ customerStatus: CustomerStatus;
26
+ squareCustomerId: string | null;
27
+ customerError: string | null;
28
+ loyaltyStatus: LoyaltyStatus;
29
+ loyalty: CustomerLoyalty | null;
30
+ points: number;
31
+ loyaltyError: string | null;
32
+ enroll: () => void;
33
+ refetchLoyalty: () => void;
34
+ }
@@ -0,0 +1,27 @@
1
+ import { useContext } from 'react';
2
+ import { CustomerContext } from './CustomerProvider';
3
+ import type { CustomerLoyalty, LoyaltyStatus } from './types';
4
+
5
+ export const useLoyalty = (): {
6
+ loyaltyStatus: LoyaltyStatus;
7
+ isReady: boolean;
8
+ loyalty: CustomerLoyalty | null;
9
+ points: number;
10
+ loyaltyError: string | null;
11
+ enroll: () => void;
12
+ refetchLoyalty: () => void;
13
+ } => {
14
+ const ctx = useContext(CustomerContext);
15
+ if (!ctx) {
16
+ throw new Error('useLoyalty must be used within CustomerProvider');
17
+ }
18
+ return {
19
+ loyaltyStatus: ctx.loyaltyStatus,
20
+ isReady: ctx.loyaltyStatus === 'ready',
21
+ loyalty: ctx.loyalty,
22
+ points: ctx.points,
23
+ loyaltyError: ctx.loyaltyError,
24
+ enroll: ctx.enroll,
25
+ refetchLoyalty: ctx.refetchLoyalty,
26
+ };
27
+ };
@@ -0,0 +1,19 @@
1
+ import { useContext } from 'react';
2
+ import { CustomerContext } from './CustomerProvider';
3
+ import type { CustomerStatus } from './types';
4
+
5
+ export const useSquareCustomer = (): {
6
+ customerStatus: CustomerStatus;
7
+ squareCustomerId: string | null;
8
+ customerError: string | null;
9
+ } => {
10
+ const ctx = useContext(CustomerContext);
11
+ if (!ctx) {
12
+ throw new Error('useSquareCustomer must be used within CustomerProvider');
13
+ }
14
+ return {
15
+ customerStatus: ctx.customerStatus,
16
+ squareCustomerId: ctx.squareCustomerId,
17
+ customerError: ctx.customerError,
18
+ };
19
+ };
@@ -4,3 +4,9 @@ export {
4
4
  useAuthenticationDispatch,
5
5
  AuthenticationProvider,
6
6
  } from './authentication/AuthenticationProvider';
7
+ export { CustomerProvider, useSquareCustomer, useLoyalty } from './customer';
8
+ export type {
9
+ CustomerStatus,
10
+ LoyaltyStatus,
11
+ CustomerLoyalty,
12
+ } from './customer';
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useContext, useEffect, useState } from 'react';
2
2
  import {
3
3
  Alert,
4
4
  Button,
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
13
13
  import { FlatList, Platform } from 'react-native';
14
14
  import { onStartCardEntry } from '../../utils/square';
15
15
  import { useCustomerQueries } from '../../hooks/useCustomer';
16
- import { useCheckCustomer } from '../../hooks/useCheckCustomer';
16
+ import { CustomerContext } from '../../providers/customer/CustomerProvider';
17
17
  import { CardRow } from './CardRow';
18
18
  import { CustomerCardPayment } from '../../types/commons';
19
19
  import { useApplePay } from './useApplePay';
@@ -29,13 +29,20 @@ export const SavedCards = ({
29
29
  }) => {
30
30
  const { t } = useTranslation();
31
31
  const theme = useTheme();
32
- const { customer } = useCheckCustomer();
32
+ const customerCtx = useContext(CustomerContext);
33
+ if (__DEV__ && !customerCtx) {
34
+ console.warn(
35
+ 'SavedCards: CustomerProvider not found in tree — customer setup will not run'
36
+ );
37
+ }
33
38
  const {
39
+ useFetchCustomer,
34
40
  useSaveCustomerCard,
35
41
  useFetchSavedCards,
36
42
  useDisableCard,
37
43
  useUpdateCustomer,
38
44
  } = useCustomerQueries();
45
+ const { data: customer } = useFetchCustomer();
39
46
  const { saveCard, saveCardLoading } = useSaveCustomerCard();
40
47
  const { disableCard, disableCardLoading } = useDisableCard();
41
48
  const [selectedPaymentId, setSelectedPaymentId] = useState('');
@@ -2,6 +2,11 @@ import { Mixpanel, MixpanelProperties } from 'mixpanel-react-native';
2
2
  import { Platform } from 'react-native';
3
3
  import Config from 'react-native-config';
4
4
  import auth from '@react-native-firebase/auth';
5
+ import { MMKV } from 'react-native-mmkv';
6
+
7
+ // Persisted guard so alias fires at most once per install, even across restarts.
8
+ const aliasStorage = new MMKV({ id: 'mixpanel-alias' });
9
+ const ALIAS_DONE_KEY = 'mixpanel_alias_done';
5
10
 
6
11
  type CommonEventTypes =
7
12
  | { eventName: 'login_completed'; properties: { method: 'phone' | 'email' } }
@@ -52,6 +57,20 @@ export const Analytics = {
52
57
  registerCustomSuperProperties();
53
58
  };
54
59
 
60
+ // Links the current anonymous distinct_id to the signed-up user id exactly
61
+ // once per install, merging pre-sign-up activity into the authenticated
62
+ // profile. Must be awaited before identify so Mixpanel honors the merge.
63
+ const aliasOnce = async (userId: string) => {
64
+ if (!mixpanel || aliasStorage.getBoolean(ALIAS_DONE_KEY)) {
65
+ return;
66
+ }
67
+ const distinctId = await mixpanel.getDistinctId();
68
+ if (distinctId && distinctId !== userId) {
69
+ mixpanel.alias(userId, distinctId);
70
+ aliasStorage.set(ALIAS_DONE_KEY, true);
71
+ }
72
+ };
73
+
55
74
  const currentUser = auth().currentUser;
56
75
  if (currentUser?.isAnonymous) {
57
76
  // Register custom super properties on app lunch before login/signup.
@@ -69,6 +88,7 @@ export const Analytics = {
69
88
  mixpanel.unregisterSuperProperty(propertyName),
70
89
  track: createTrackFunction<CommonEventTypes | T>(),
71
90
  identify,
91
+ aliasOnce,
72
92
  profile: {
73
93
  set: (key: string, value?: any) => {
74
94
  mixpanel?.getPeople()?.set(key, value);
@@ -1,57 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.useCheckCustomer = void 0;
7
- var _react = require("react");
8
- var _useCustomer = require("./useCustomer");
9
- var _auth = _interopRequireDefault(require("@react-native-firebase/auth"));
10
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11
- const useCheckCustomer = () => {
12
- const user = (0, _auth.default)().currentUser;
13
- const isLoggedIn = user && !user.isAnonymous;
14
- const {
15
- useFetchCustomer,
16
- useCreateCustomer,
17
- useFetchSquareProgram,
18
- useConnectSquareCustomer
19
- } = (0, _useCustomer.useCustomerQueries)();
20
- const {
21
- connectSquare
22
- } = useConnectSquareCustomer();
23
- const {
24
- createCustomerHook
25
- } = useCreateCustomer();
26
- const {
27
- data: customer
28
- } = useFetchCustomer();
29
- const {
30
- data: program,
31
- isFetched: isProgramFetched
32
- } = useFetchSquareProgram({
33
- enabled: Boolean(customer?.uid),
34
- refetchOnWindowFocus: false
35
- });
36
- const hasAttemptedCreate = (0, _react.useRef)(false);
37
- const hasAttemptedConnect = (0, _react.useRef)(false);
38
- (0, _react.useEffect)(() => {
39
- if (isLoggedIn && customer?.customer === null && !hasAttemptedCreate.current) {
40
- hasAttemptedCreate.current = true;
41
- createCustomerHook();
42
- }
43
- }, [customer, createCustomerHook, isLoggedIn]);
44
- (0, _react.useEffect)(() => {
45
- if (isProgramFetched && program === null && !hasAttemptedConnect.current) {
46
- hasAttemptedConnect.current = true;
47
- connectSquare();
48
- }
49
- }, [isProgramFetched, program, connectSquare]);
50
- const isCustomerReady = Boolean(customer?.uid && isProgramFetched && program != null);
51
- return {
52
- customer,
53
- isCustomerReady
54
- };
55
- };
56
- exports.useCheckCustomer = useCheckCustomer;
57
- //# sourceMappingURL=useCheckCustomer.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["_react","require","_useCustomer","_auth","_interopRequireDefault","obj","__esModule","default","useCheckCustomer","user","firebase","currentUser","isLoggedIn","isAnonymous","useFetchCustomer","useCreateCustomer","useFetchSquareProgram","useConnectSquareCustomer","useCustomerQueries","connectSquare","createCustomerHook","data","customer","program","isFetched","isProgramFetched","enabled","Boolean","uid","refetchOnWindowFocus","hasAttemptedCreate","useRef","hasAttemptedConnect","useEffect","current","isCustomerReady","exports"],"sourceRoot":"../../../src","sources":["hooks/useCheckCustomer.ts"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AACA,IAAAE,KAAA,GAAAC,sBAAA,CAAAH,OAAA;AAAmD,SAAAG,uBAAAC,GAAA,WAAAA,GAAA,IAAAA,GAAA,CAAAC,UAAA,GAAAD,GAAA,KAAAE,OAAA,EAAAF,GAAA;AAE5C,MAAMG,gBAAgB,GAAGA,CAAA,KAAM;EACpC,MAAMC,IAAI,GAAG,IAAAC,aAAQ,EAAC,CAAC,CAACC,WAAW;EACnC,MAAMC,UAAU,GAAGH,IAAI,IAAI,CAACA,IAAI,CAACI,WAAW;EAC5C,MAAM;IACJC,gBAAgB;IAChBC,iBAAiB;IACjBC,qBAAqB;IACrBC;EACF,CAAC,GAAG,IAAAC,+BAAkB,EAAC,CAAC;EACxB,MAAM;IAAEC;EAAc,CAAC,GAAGF,wBAAwB,CAAC,CAAC;EAEpD,MAAM;IAAEG;EAAmB,CAAC,GAAGL,iBAAiB,CAAC,CAAC;EAClD,MAAM;IAAEM,IAAI,EAAEC;EAAS,CAAC,GAAGR,gBAAgB,CAAC,CAAC;EAC7C,MAAM;IAAEO,IAAI,EAAEE,OAAO;IAAEC,SAAS,EAAEC;EAAiB,CAAC,GAAGT,qBAAqB,CAAC;IAC3EU,OAAO,EAAEC,OAAO,CAACL,QAAQ,EAAEM,GAAG,CAAC;IAC/BC,oBAAoB,EAAE;EACxB,CAAC,CAAC;EAEF,MAAMC,kBAAkB,GAAG,IAAAC,aAAM,EAAC,KAAK,CAAC;EACxC,MAAMC,mBAAmB,GAAG,IAAAD,aAAM,EAAC,KAAK,CAAC;EAEzC,IAAAE,gBAAS,EAAC,MAAM;IACd,IACErB,UAAU,IACVU,QAAQ,EAAEA,QAAQ,KAAK,IAAI,IAC3B,CAACQ,kBAAkB,CAACI,OAAO,EAC3B;MACAJ,kBAAkB,CAACI,OAAO,GAAG,IAAI;MACjCd,kBAAkB,CAAC,CAAC;IACtB;EACF,CAAC,EAAE,CAACE,QAAQ,EAAEF,kBAAkB,EAAER,UAAU,CAAC,CAAC;EAE9C,IAAAqB,gBAAS,EAAC,MAAM;IACd,IAAIR,gBAAgB,IAAIF,OAAO,KAAK,IAAI,IAAI,CAACS,mBAAmB,CAACE,OAAO,EAAE;MACxEF,mBAAmB,CAACE,OAAO,GAAG,IAAI;MAClCf,aAAa,CAAC,CAAC;IACjB;EACF,CAAC,EAAE,CAACM,gBAAgB,EAAEF,OAAO,EAAEJ,aAAa,CAAC,CAAC;EAE9C,MAAMgB,eAAe,GAAGR,OAAO,CAC7BL,QAAQ,EAAEM,GAAG,IAAIH,gBAAgB,IAAIF,OAAO,IAAI,IAClD,CAAC;EAED,OAAO;IAAED,QAAQ;IAAEa;EAAgB,CAAC;AACtC,CAAC;AAACC,OAAA,CAAA5B,gBAAA,GAAAA,gBAAA"}