@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.
- package/README.md +20 -0
- package/lib/commonjs/constants/index.js +1 -1
- package/lib/commonjs/constants/index.js.map +1 -1
- package/lib/commonjs/hooks/index.js +0 -7
- package/lib/commonjs/hooks/index.js.map +1 -1
- package/lib/commonjs/hooks/useAuth.js +6 -0
- package/lib/commonjs/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/hooks/useCustomer.js +0 -86
- package/lib/commonjs/hooks/useCustomer.js.map +1 -1
- package/lib/commonjs/providers/customer/CustomerProvider.js +214 -0
- package/lib/commonjs/providers/customer/CustomerProvider.js.map +1 -0
- package/lib/commonjs/providers/customer/README.md +122 -0
- package/lib/commonjs/providers/customer/classifyLoyaltyError.js +19 -0
- package/lib/commonjs/providers/customer/classifyLoyaltyError.js.map +1 -0
- package/lib/commonjs/providers/customer/index.js +27 -0
- package/lib/commonjs/providers/customer/index.js.map +1 -0
- package/lib/commonjs/providers/customer/types.js +2 -0
- package/lib/commonjs/providers/customer/types.js.map +1 -0
- package/lib/commonjs/providers/customer/useLoyalty.js +25 -0
- package/lib/commonjs/providers/customer/useLoyalty.js.map +1 -0
- package/lib/commonjs/providers/customer/useSquareCustomer.js +21 -0
- package/lib/commonjs/providers/customer/useSquareCustomer.js.map +1 -0
- package/lib/commonjs/providers/index.js +19 -0
- package/lib/commonjs/providers/index.js.map +1 -1
- package/lib/commonjs/screens/SavedCards/SavedCards.js +9 -4
- package/lib/commonjs/screens/SavedCards/SavedCards.js.map +1 -1
- package/lib/commonjs/utils/analytics.js +21 -0
- package/lib/commonjs/utils/analytics.js.map +1 -1
- package/lib/module/constants/index.js +1 -1
- package/lib/module/constants/index.js.map +1 -1
- package/lib/module/hooks/index.js +0 -1
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/useAuth.js +6 -0
- package/lib/module/hooks/useAuth.js.map +1 -1
- package/lib/module/hooks/useCustomer.js +1 -87
- package/lib/module/hooks/useCustomer.js.map +1 -1
- package/lib/module/providers/customer/CustomerProvider.js +204 -0
- package/lib/module/providers/customer/CustomerProvider.js.map +1 -0
- package/lib/module/providers/customer/README.md +122 -0
- package/lib/module/providers/customer/classifyLoyaltyError.js +13 -0
- package/lib/module/providers/customer/classifyLoyaltyError.js.map +1 -0
- package/lib/module/providers/customer/index.js +4 -0
- package/lib/module/providers/customer/index.js.map +1 -0
- package/lib/module/providers/customer/types.js +2 -0
- package/lib/module/providers/customer/types.js.map +1 -0
- package/lib/module/providers/customer/useLoyalty.js +18 -0
- package/lib/module/providers/customer/useLoyalty.js.map +1 -0
- package/lib/module/providers/customer/useSquareCustomer.js +14 -0
- package/lib/module/providers/customer/useSquareCustomer.js.map +1 -0
- package/lib/module/providers/index.js +1 -0
- package/lib/module/providers/index.js.map +1 -1
- package/lib/module/screens/SavedCards/SavedCards.js +10 -5
- package/lib/module/screens/SavedCards/SavedCards.js.map +1 -1
- package/lib/module/utils/analytics.js +22 -0
- package/lib/module/utils/analytics.js.map +1 -1
- package/lib/typescript/src/constants/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +0 -1
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useCustomer.d.ts +0 -13
- package/lib/typescript/src/hooks/useCustomer.d.ts.map +1 -1
- package/lib/typescript/src/providers/customer/CustomerProvider.d.ts +9 -0
- package/lib/typescript/src/providers/customer/CustomerProvider.d.ts.map +1 -0
- package/lib/typescript/src/providers/customer/classifyLoyaltyError.d.ts +3 -0
- package/lib/typescript/src/providers/customer/classifyLoyaltyError.d.ts.map +1 -0
- package/lib/typescript/src/providers/customer/index.d.ts +5 -0
- package/lib/typescript/src/providers/customer/index.d.ts.map +1 -0
- package/lib/typescript/src/providers/customer/types.d.ts +25 -0
- package/lib/typescript/src/providers/customer/types.d.ts.map +1 -0
- package/lib/typescript/src/providers/customer/useLoyalty.d.ts +11 -0
- package/lib/typescript/src/providers/customer/useLoyalty.d.ts.map +1 -0
- package/lib/typescript/src/providers/customer/useSquareCustomer.d.ts +7 -0
- package/lib/typescript/src/providers/customer/useSquareCustomer.d.ts.map +1 -0
- package/lib/typescript/src/providers/index.d.ts +2 -0
- package/lib/typescript/src/providers/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/SavedCards/SavedCards.d.ts.map +1 -1
- package/lib/typescript/src/utils/analytics.d.ts +2 -0
- package/lib/typescript/src/utils/analytics.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants/index.ts +1 -1
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useAuth.ts +6 -0
- package/src/hooks/useCustomer.ts +0 -129
- package/src/providers/customer/CustomerProvider.tsx +272 -0
- package/src/providers/customer/README.md +122 -0
- package/src/providers/customer/classifyLoyaltyError.ts +25 -0
- package/src/providers/customer/index.ts +4 -0
- package/src/providers/customer/types.ts +34 -0
- package/src/providers/customer/useLoyalty.ts +27 -0
- package/src/providers/customer/useSquareCustomer.ts +19 -0
- package/src/providers/index.ts +6 -0
- package/src/screens/SavedCards/SavedCards.tsx +10 -3
- package/src/utils/analytics.ts +20 -0
- package/lib/commonjs/hooks/useCheckCustomer.js +0 -57
- package/lib/commonjs/hooks/useCheckCustomer.js.map +0 -1
- package/lib/module/hooks/useCheckCustomer.js +0 -49
- package/lib/module/hooks/useCheckCustomer.js.map +0 -1
- package/lib/typescript/src/hooks/useCheckCustomer.d.ts +0 -5
- package/lib/typescript/src/hooks/useCheckCustomer.d.ts.map +0 -1
- 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,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
|
+
};
|
package/src/providers/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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('');
|
package/src/utils/analytics.ts
CHANGED
|
@@ -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"}
|