@kiosinc/commons-rn 0.15.6 → 0.16.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/useCustomer.js +0 -90
- 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/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/useCustomer.js +1 -91
- 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/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/useCustomer.d.ts +0 -16
- 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/package.json +1 -1
- package/src/constants/index.ts +1 -1
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useCustomer.ts +0 -134
- 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/lib/commonjs/hooks/useCheckCustomer.js +0 -56
- package/lib/commonjs/hooks/useCheckCustomer.js.map +0 -1
- package/lib/module/hooks/useCheckCustomer.js +0 -48
- package/lib/module/hooks/useCheckCustomer.js.map +0 -1
- package/lib/typescript/src/hooks/useCheckCustomer.d.ts +0 -4
- package/lib/typescript/src/hooks/useCheckCustomer.d.ts.map +0 -1
- package/src/hooks/useCheckCustomer.ts +0 -42
package/src/hooks/useCustomer.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
2
1
|
import { Alert } from '../components';
|
|
3
2
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
3
|
import firestore from '@react-native-firebase/firestore';
|
|
5
|
-
import uuid from 'react-native-uuid';
|
|
6
|
-
|
|
7
4
|
import {
|
|
8
|
-
connectSquareCustomer,
|
|
9
|
-
connectSquareLoyalty,
|
|
10
5
|
createCustomer,
|
|
11
6
|
fetchLoyaltyRewards,
|
|
12
|
-
fetchSquareLoyalty,
|
|
13
7
|
getCustomer,
|
|
14
8
|
saveCustomerAddress,
|
|
15
9
|
saveCustomerCard,
|
|
@@ -27,12 +21,6 @@ export const useCustomerQueries = () => {
|
|
|
27
21
|
const [selectedBusinessId] = useMMKVString(MMKV_KEYS.BUSINESS_ID);
|
|
28
22
|
const isLoggedIn = user && !user.isAnonymous;
|
|
29
23
|
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (!isLoggedIn) {
|
|
32
|
-
queryClient.invalidateQueries([queryKeys.LOYALTY, user?.uid]);
|
|
33
|
-
}
|
|
34
|
-
}, [isLoggedIn, queryClient, user?.uid]);
|
|
35
|
-
|
|
36
24
|
return {
|
|
37
25
|
useFetchCustomer: () =>
|
|
38
26
|
useQuery(
|
|
@@ -160,63 +148,6 @@ export const useCustomerQueries = () => {
|
|
|
160
148
|
enabled: Boolean(isLoggedIn),
|
|
161
149
|
}
|
|
162
150
|
),
|
|
163
|
-
useFetchSquareProgram: (queryOptions?: object) =>
|
|
164
|
-
useQuery(
|
|
165
|
-
[queryKeys.PROGRAM, user?.uid, selectedBusinessId],
|
|
166
|
-
async () => {
|
|
167
|
-
if (isLoggedIn) {
|
|
168
|
-
const programs = await firestore()
|
|
169
|
-
.collection(`customers/${user?.uid}/programs`)
|
|
170
|
-
.where('type', '==', 'customer')
|
|
171
|
-
.where('businessId', '==', selectedBusinessId)
|
|
172
|
-
.orderBy('updated', 'asc')
|
|
173
|
-
.get();
|
|
174
|
-
if (programs.docs.length > 0) {
|
|
175
|
-
return programs.docs.map((program: any) => program.data())[0];
|
|
176
|
-
} else {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
} else {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
enabled: Boolean(isLoggedIn && selectedBusinessId),
|
|
185
|
-
...queryOptions,
|
|
186
|
-
}
|
|
187
|
-
),
|
|
188
|
-
useFetchSquareLoyalty: (queryOptions?: object) =>
|
|
189
|
-
useQuery(
|
|
190
|
-
[
|
|
191
|
-
queryKeys.LOYALTY,
|
|
192
|
-
user?.uid ?? 'no-user',
|
|
193
|
-
selectedBusinessId ?? 'no-biz',
|
|
194
|
-
],
|
|
195
|
-
async () => {
|
|
196
|
-
const response = await fetchSquareLoyalty(
|
|
197
|
-
user!.uid,
|
|
198
|
-
selectedBusinessId!
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
if (response?.data?.error) {
|
|
202
|
-
throw new Error(
|
|
203
|
-
response.data.error.message || 'Unknown error occurred'
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
return response;
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
enabled: Boolean(isLoggedIn && user?.uid && selectedBusinessId),
|
|
210
|
-
refetchOnWindowFocus: false,
|
|
211
|
-
refetchOnReconnect: false,
|
|
212
|
-
refetchOnMount: false,
|
|
213
|
-
staleTime: 5 * 60 * 1000,
|
|
214
|
-
cacheTime: 30 * 60 * 1000,
|
|
215
|
-
retry: 0,
|
|
216
|
-
select: (res) => res?.data,
|
|
217
|
-
...(queryOptions || {}),
|
|
218
|
-
}
|
|
219
|
-
),
|
|
220
151
|
useSaveCustomerCard: () => {
|
|
221
152
|
const mutation = useMutation(
|
|
222
153
|
async (data: SaveCardRequest) => {
|
|
@@ -273,71 +204,6 @@ export const useCustomerQueries = () => {
|
|
|
273
204
|
disableCardLoading: mutation.isLoading,
|
|
274
205
|
};
|
|
275
206
|
},
|
|
276
|
-
useConnectSquareLoyalty: () => {
|
|
277
|
-
const mutation = useMutation(
|
|
278
|
-
async () => {
|
|
279
|
-
if (isLoggedIn && selectedBusinessId) {
|
|
280
|
-
const response = await connectSquareLoyalty(user?.uid, {
|
|
281
|
-
businessId: selectedBusinessId,
|
|
282
|
-
idempotentKey: uuid.v4() as string,
|
|
283
|
-
uid: user?.uid,
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
if (response?.data?.error) {
|
|
287
|
-
throw new Error(
|
|
288
|
-
response.data.error.message || 'Unknown error occurred'
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return response;
|
|
293
|
-
}
|
|
294
|
-
return null;
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
onError: (err: { code: string; message: string }) =>
|
|
298
|
-
Alert.show({
|
|
299
|
-
title: err?.code ? 'Code ' + err?.code : '',
|
|
300
|
-
description: err?.message,
|
|
301
|
-
}),
|
|
302
|
-
onSuccess: () => {
|
|
303
|
-
queryClient.invalidateQueries([queryKeys.LOYALTY, user?.uid]);
|
|
304
|
-
},
|
|
305
|
-
}
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
connectSquareLoyalty: mutation.mutate,
|
|
310
|
-
connectLoyaltyLoading: mutation.isLoading,
|
|
311
|
-
};
|
|
312
|
-
},
|
|
313
|
-
useConnectSquareCustomer: () => {
|
|
314
|
-
const mutation = useMutation(
|
|
315
|
-
async () => {
|
|
316
|
-
if (isLoggedIn && selectedBusinessId) {
|
|
317
|
-
await connectSquareCustomer(user?.uid, {
|
|
318
|
-
businessId: selectedBusinessId,
|
|
319
|
-
idempotentKey: uuid.v4() as string,
|
|
320
|
-
uid: user?.uid,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
onError: (err: { code: string; message: string }) =>
|
|
326
|
-
Alert.show({
|
|
327
|
-
title: 'Code ' + err?.code,
|
|
328
|
-
description: err?.message,
|
|
329
|
-
}),
|
|
330
|
-
onSuccess: () => {
|
|
331
|
-
queryClient.invalidateQueries([queryKeys.PROGRAM, user?.uid]);
|
|
332
|
-
},
|
|
333
|
-
}
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
return {
|
|
337
|
-
connectSquare: mutation.mutate,
|
|
338
|
-
connectSquareLoading: mutation.isLoading,
|
|
339
|
-
};
|
|
340
|
-
},
|
|
341
207
|
useFetchLoyaltyRewards: (options?: { enabled?: boolean }) =>
|
|
342
208
|
useQuery(
|
|
343
209
|
[queryKeys.REWARDS, selectedBusinessId],
|
|
@@ -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';
|