react-native-salespanda 0.4.4 → 0.5.3
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/lib/module/SalespandaApp.js +16 -8
- package/lib/module/SalespandaApp.js.map +1 -1
- package/lib/module/assets/images/index.js +1 -22
- package/lib/module/assets/images/index.js.map +1 -1
- package/lib/module/components/Loader.js +45 -0
- package/lib/module/components/Loader.js.map +1 -0
- package/lib/module/config/SalespandaConfig.js +17 -0
- package/lib/module/config/SalespandaConfig.js.map +1 -1
- package/lib/module/index.js +1 -32
- package/lib/module/index.js.map +1 -1
- package/lib/module/navigation/BottomTabNavigator.js +1 -3
- package/lib/module/navigation/BottomTabNavigator.js.map +1 -1
- package/lib/module/screens/Tabs/HomeScreen.js +184 -104
- package/lib/module/screens/Tabs/HomeScreen.js.map +1 -1
- package/lib/module/services/api.js +132 -0
- package/lib/module/services/api.js.map +1 -0
- package/lib/module/services/authService.js +59 -0
- package/lib/module/services/authService.js.map +1 -0
- package/lib/module/store/index.js +13 -0
- package/lib/module/store/index.js.map +1 -0
- package/lib/typescript/src/SalespandaApp.d.ts.map +1 -1
- package/lib/typescript/src/components/Loader.d.ts +11 -0
- package/lib/typescript/src/components/Loader.d.ts.map +1 -0
- package/lib/typescript/src/config/SalespandaConfig.d.ts +14 -0
- package/lib/typescript/src/config/SalespandaConfig.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -12
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/Tabs/HomeScreen.d.ts.map +1 -1
- package/lib/typescript/src/services/api.d.ts +638 -0
- package/lib/typescript/src/services/api.d.ts.map +1 -0
- package/lib/typescript/src/services/authService.d.ts +13 -0
- package/lib/typescript/src/services/authService.d.ts.map +1 -0
- package/lib/typescript/src/store/index.d.ts +36 -0
- package/lib/typescript/src/store/index.d.ts.map +1 -0
- package/package.json +8 -2
- package/react-native.config.js +8 -5
- package/src/SalespandaApp.tsx +18 -10
- package/src/components/Loader.tsx +48 -0
- package/src/config/SalespandaConfig.ts +37 -0
- package/src/index.tsx +8 -30
- package/src/navigation/BottomTabNavigator.tsx +1 -3
- package/src/screens/Tabs/HomeScreen.tsx +229 -73
- package/src/services/api.ts +173 -0
- package/src/services/authService.ts +75 -0
- package/src/store/index.ts +16 -0
|
@@ -9,37 +9,80 @@ import {
|
|
|
9
9
|
FlatList,
|
|
10
10
|
Dimensions,
|
|
11
11
|
} from 'react-native';
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
scale,
|
|
14
|
+
verticalScale,
|
|
15
|
+
moderateScale,
|
|
16
|
+
moderateVerticalScale,
|
|
17
|
+
} from 'react-native-size-matters';
|
|
14
18
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
19
|
+
import Loader from '../../components/Loader';
|
|
15
20
|
import TabsHeader from '../../components/TabsHeader';
|
|
21
|
+
import { Colors } from '../../constants/Colors';
|
|
22
|
+
import {
|
|
23
|
+
useAuthenticateMutation,
|
|
24
|
+
useLazyGetHomeQuery,
|
|
25
|
+
useSpssoLoginMutation,
|
|
26
|
+
loadPersistedTokens,
|
|
27
|
+
persistTokens,
|
|
28
|
+
setRuntimeTokens,
|
|
29
|
+
clearPersistedTokens,
|
|
30
|
+
} from '../../services/api';
|
|
31
|
+
import type { HomeResponse } from '../../services/api';
|
|
16
32
|
|
|
17
33
|
interface MenuItemProps {
|
|
18
34
|
title: string;
|
|
19
|
-
|
|
35
|
+
imageUrl?: string | null;
|
|
20
36
|
onPress?: () => void;
|
|
21
37
|
}
|
|
22
38
|
|
|
23
|
-
const MenuItem: React.FC<MenuItemProps> = ({
|
|
39
|
+
const MenuItem: React.FC<MenuItemProps> = ({ imageUrl, onPress }) => {
|
|
24
40
|
return (
|
|
25
|
-
<TouchableOpacity
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
<TouchableOpacity
|
|
42
|
+
style={styles.menuItem}
|
|
43
|
+
onPress={onPress}
|
|
44
|
+
activeOpacity={0.8}
|
|
45
|
+
>
|
|
46
|
+
{imageUrl ? (
|
|
47
|
+
<Image source={{ uri: imageUrl }} style={styles.menuItemImage} />
|
|
48
|
+
) : (
|
|
49
|
+
<Loader message="" />
|
|
50
|
+
)}
|
|
30
51
|
</TouchableOpacity>
|
|
31
52
|
);
|
|
32
53
|
};
|
|
33
54
|
|
|
34
55
|
const HomeScreen: React.FC = () => {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
'
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
const [loading, setLoading] = React.useState<boolean>(false);
|
|
57
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
58
|
+
const [homeData, setHomeData] = React.useState<
|
|
59
|
+
HomeResponse['response'] | null
|
|
60
|
+
>(null);
|
|
61
|
+
|
|
62
|
+
type BannerItem = {
|
|
63
|
+
image: string;
|
|
64
|
+
title?: string;
|
|
65
|
+
link?: string | null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const [authenticate] = useAuthenticateMutation();
|
|
69
|
+
const [spssoLogin] = useSpssoLoginMutation();
|
|
70
|
+
const [triggerHome, { isFetching: isHomeFetching }] = useLazyGetHomeQuery();
|
|
71
|
+
|
|
72
|
+
const bannerItems: BannerItem[] = React.useMemo(
|
|
73
|
+
() =>
|
|
74
|
+
homeData?.home_banner && homeData.home_banner.length > 0
|
|
75
|
+
? homeData.home_banner.map((item) => ({
|
|
76
|
+
image: item.image,
|
|
77
|
+
title: item.title ?? undefined,
|
|
78
|
+
link: item.link ?? null,
|
|
79
|
+
}))
|
|
80
|
+
: [],
|
|
81
|
+
[homeData?.home_banner]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const menuItems = React.useMemo(() => homeData?.menu ?? [], [homeData?.menu]);
|
|
85
|
+
|
|
43
86
|
const screenWidth = Dimensions.get('window').width;
|
|
44
87
|
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
45
88
|
const flatListRef = React.useRef<any>(null);
|
|
@@ -49,10 +92,90 @@ const HomeScreen: React.FC = () => {
|
|
|
49
92
|
const index = Math.round(offsetX / screenWidth);
|
|
50
93
|
setActiveIndex(index);
|
|
51
94
|
};
|
|
95
|
+
|
|
96
|
+
const loadHomeData = React.useCallback(
|
|
97
|
+
async (isRetry?: boolean) => {
|
|
98
|
+
try {
|
|
99
|
+
setLoading(true);
|
|
100
|
+
setError(null);
|
|
101
|
+
|
|
102
|
+
const storedTokens = await loadPersistedTokens();
|
|
103
|
+
let accessToken = storedTokens.accessToken;
|
|
104
|
+
let token = storedTokens.token;
|
|
105
|
+
|
|
106
|
+
if (!accessToken) {
|
|
107
|
+
const authResp = await authenticate({}).unwrap();
|
|
108
|
+
accessToken = authResp.access_token;
|
|
109
|
+
setRuntimeTokens({ accessToken });
|
|
110
|
+
await persistTokens({ accessToken });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!token && accessToken) {
|
|
114
|
+
const ssoResp = await spssoLogin({
|
|
115
|
+
access_token: accessToken,
|
|
116
|
+
}).unwrap();
|
|
117
|
+
token = ssoResp.token;
|
|
118
|
+
setRuntimeTokens({ token });
|
|
119
|
+
await persistTokens({ token });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!token) {
|
|
123
|
+
throw new Error('Missing auth token, please try again.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const tokenHeader: string | undefined = token ?? undefined;
|
|
127
|
+
const homeResp = await triggerHome({
|
|
128
|
+
tokenOverride: tokenHeader,
|
|
129
|
+
}).unwrap();
|
|
130
|
+
|
|
131
|
+
const isSuccess =
|
|
132
|
+
homeResp.statusCode === '200' &&
|
|
133
|
+
homeResp.status?.toLowerCase() === 'success';
|
|
134
|
+
if (!isSuccess) {
|
|
135
|
+
const code = homeResp.statusCode;
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
!isRetry &&
|
|
139
|
+
code &&
|
|
140
|
+
(code === '1008' || code === '401' || code === '403')
|
|
141
|
+
) {
|
|
142
|
+
await clearPersistedTokens();
|
|
143
|
+
setRuntimeTokens({ token: null, accessToken: null });
|
|
144
|
+
await loadHomeData(true);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw new Error(homeResp?.message || 'Failed to load home data');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
setHomeData(homeResp.response);
|
|
152
|
+
setLoading(false);
|
|
153
|
+
} catch (e: any) {
|
|
154
|
+
const statusCode = e?.status || e?.data?.statusCode;
|
|
155
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
156
|
+
setError(null);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
setError(
|
|
160
|
+
e?.data?.message ||
|
|
161
|
+
e?.message ||
|
|
162
|
+
'Failed to load home data. Please try again.'
|
|
163
|
+
);
|
|
164
|
+
setLoading(false);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
[authenticate, spssoLogin, triggerHome]
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
React.useEffect(() => {
|
|
171
|
+
loadHomeData();
|
|
172
|
+
}, [loadHomeData]);
|
|
173
|
+
|
|
52
174
|
React.useEffect(() => {
|
|
53
175
|
const id = setInterval(() => {
|
|
54
176
|
setActiveIndex((prev) => {
|
|
55
|
-
const next =
|
|
177
|
+
const next =
|
|
178
|
+
bannerItems.length > 0 ? (prev + 1) % bannerItems.length : 0;
|
|
56
179
|
if (flatListRef.current) {
|
|
57
180
|
try {
|
|
58
181
|
flatListRef.current.scrollToIndex({ index: next, animated: true });
|
|
@@ -63,25 +186,33 @@ const HomeScreen: React.FC = () => {
|
|
|
63
186
|
return next;
|
|
64
187
|
});
|
|
65
188
|
}, 2500);
|
|
66
|
-
return () =>
|
|
67
|
-
|
|
189
|
+
return () => {
|
|
190
|
+
clearInterval(id);
|
|
191
|
+
};
|
|
192
|
+
}, [bannerItems.length]);
|
|
68
193
|
|
|
69
194
|
return (
|
|
70
195
|
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>
|
|
71
196
|
<View style={styles.container}>
|
|
72
197
|
<TabsHeader title="Home" />
|
|
73
|
-
|
|
74
|
-
|
|
198
|
+
|
|
199
|
+
<View style={styles.contentWrapper}>
|
|
200
|
+
{loading || isHomeFetching || !homeData ? (
|
|
201
|
+
<Loader overlay message="Loading home data..." />
|
|
202
|
+
) : null}
|
|
203
|
+
|
|
75
204
|
<View style={styles.carouselContainer}>
|
|
76
205
|
<FlatList
|
|
77
206
|
ref={flatListRef}
|
|
78
|
-
data={
|
|
79
|
-
keyExtractor={(
|
|
207
|
+
data={bannerItems}
|
|
208
|
+
keyExtractor={(item, idx) => `${item.title || 'banner'}-${idx}`}
|
|
80
209
|
renderItem={({ item }) => (
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
210
|
+
<TouchableOpacity activeOpacity={0.9} onPress={() => {}}>
|
|
211
|
+
<Image
|
|
212
|
+
source={{ uri: item.image }}
|
|
213
|
+
style={[styles.carouselImage, { width: screenWidth }]}
|
|
214
|
+
/>
|
|
215
|
+
</TouchableOpacity>
|
|
85
216
|
)}
|
|
86
217
|
horizontal
|
|
87
218
|
pagingEnabled
|
|
@@ -93,9 +224,8 @@ const HomeScreen: React.FC = () => {
|
|
|
93
224
|
index,
|
|
94
225
|
})}
|
|
95
226
|
/>
|
|
96
|
-
{/* Overlay Indicators at bottom of the image */}
|
|
97
227
|
<View style={styles.carouselIndicatorsOverlay}>
|
|
98
|
-
{
|
|
228
|
+
{bannerItems.map((_, idx) => (
|
|
99
229
|
<View
|
|
100
230
|
key={idx}
|
|
101
231
|
style={[
|
|
@@ -107,30 +237,32 @@ const HomeScreen: React.FC = () => {
|
|
|
107
237
|
</View>
|
|
108
238
|
</View>
|
|
109
239
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
240
|
+
<ScrollView
|
|
241
|
+
style={styles.scrollView}
|
|
242
|
+
contentContainerStyle={styles.scrollContent}
|
|
243
|
+
>
|
|
244
|
+
{error ? (
|
|
245
|
+
<>
|
|
246
|
+
<Text style={styles.errorText}>{error}</Text>
|
|
247
|
+
<TouchableOpacity onPress={() => loadHomeData()}>
|
|
248
|
+
<Text style={styles.retryText}>Tap to retry</Text>
|
|
249
|
+
</TouchableOpacity>
|
|
250
|
+
</>
|
|
251
|
+
) : null}
|
|
122
252
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
253
|
+
{menuItems.length > 0 ? (
|
|
254
|
+
<View style={styles.menuGrid}>
|
|
255
|
+
{menuItems.map((item) => (
|
|
256
|
+
<MenuItem
|
|
257
|
+
key={item.title}
|
|
258
|
+
title={item.title}
|
|
259
|
+
imageUrl={item.image}
|
|
260
|
+
/>
|
|
261
|
+
))}
|
|
262
|
+
</View>
|
|
263
|
+
) : null}
|
|
264
|
+
</ScrollView>
|
|
265
|
+
</View>
|
|
134
266
|
</View>
|
|
135
267
|
</SafeAreaView>
|
|
136
268
|
);
|
|
@@ -151,20 +283,24 @@ const styles = StyleSheet.create({
|
|
|
151
283
|
flex: 1,
|
|
152
284
|
backgroundColor: Colors.white,
|
|
153
285
|
},
|
|
286
|
+
scrollContent: {
|
|
287
|
+
paddingBottom: verticalScale(24),
|
|
288
|
+
},
|
|
154
289
|
carouselContainer: {
|
|
155
290
|
width: '100%',
|
|
156
|
-
backgroundColor: Colors.white,
|
|
291
|
+
backgroundColor: Colors.white,
|
|
157
292
|
position: 'relative',
|
|
158
293
|
},
|
|
159
294
|
carouselImage: {
|
|
160
|
-
height: verticalScale(
|
|
161
|
-
resizeMode: '
|
|
295
|
+
height: verticalScale(155),
|
|
296
|
+
resizeMode: 'contain',
|
|
297
|
+
backgroundColor: Colors.white,
|
|
162
298
|
},
|
|
163
299
|
carouselIndicatorsOverlay: {
|
|
164
300
|
position: 'absolute',
|
|
165
301
|
left: 0,
|
|
166
302
|
right: 0,
|
|
167
|
-
bottom: verticalScale(
|
|
303
|
+
bottom: verticalScale(20),
|
|
168
304
|
flexDirection: 'row',
|
|
169
305
|
justifyContent: 'center',
|
|
170
306
|
alignItems: 'center',
|
|
@@ -183,30 +319,23 @@ const styles = StyleSheet.create({
|
|
|
183
319
|
menuGrid: {
|
|
184
320
|
flexDirection: 'row',
|
|
185
321
|
flexWrap: 'wrap',
|
|
186
|
-
|
|
187
|
-
|
|
322
|
+
paddingTop: scale(8),
|
|
323
|
+
paddingBottom: verticalScale(16),
|
|
324
|
+
justifyContent: 'flex-start',
|
|
325
|
+
alignSelf: 'center',
|
|
326
|
+
width: '100%',
|
|
188
327
|
},
|
|
189
328
|
menuItem: {
|
|
190
|
-
width: '
|
|
329
|
+
width: '29.5%',
|
|
191
330
|
aspectRatio: 1,
|
|
192
|
-
backgroundColor: Colors.lightblue,
|
|
193
331
|
borderRadius: moderateScale(16),
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
justifyContent: 'center',
|
|
198
|
-
shadowColor: Colors.black,
|
|
199
|
-
shadowOffset: { width: 0, height: 0 },
|
|
200
|
-
shadowOpacity: 0.1,
|
|
201
|
-
shadowRadius: 0.5,
|
|
202
|
-
borderWidth: 1,
|
|
203
|
-
borderColor: Colors.border,
|
|
332
|
+
marginBottom: moderateVerticalScale(8),
|
|
333
|
+
marginHorizontal: scale(6),
|
|
334
|
+
overflow: 'hidden',
|
|
204
335
|
},
|
|
205
336
|
iconContainer: {
|
|
206
337
|
width: scale(50),
|
|
207
338
|
height: scale(50),
|
|
208
|
-
borderRadius: moderateScale(25),
|
|
209
|
-
backgroundColor: Colors.divider,
|
|
210
339
|
justifyContent: 'center',
|
|
211
340
|
alignItems: 'center',
|
|
212
341
|
marginBottom: verticalScale(8),
|
|
@@ -214,10 +343,37 @@ const styles = StyleSheet.create({
|
|
|
214
343
|
iconText: {
|
|
215
344
|
fontSize: moderateScale(24),
|
|
216
345
|
},
|
|
346
|
+
menuItemImage: {
|
|
347
|
+
width: '100%',
|
|
348
|
+
height: '100%',
|
|
349
|
+
resizeMode: 'cover',
|
|
350
|
+
},
|
|
217
351
|
menuItemText: {
|
|
218
352
|
fontSize: moderateScale(11),
|
|
219
353
|
textAlign: 'center',
|
|
220
354
|
color: Colors.black,
|
|
221
355
|
fontWeight: '500',
|
|
222
356
|
},
|
|
357
|
+
statusText: {
|
|
358
|
+
fontSize: moderateScale(12),
|
|
359
|
+
color: Colors.black,
|
|
360
|
+
paddingHorizontal: scale(12),
|
|
361
|
+
paddingTop: verticalScale(8),
|
|
362
|
+
},
|
|
363
|
+
errorText: {
|
|
364
|
+
fontSize: moderateScale(12),
|
|
365
|
+
color: 'red',
|
|
366
|
+
paddingHorizontal: scale(12),
|
|
367
|
+
paddingTop: verticalScale(4),
|
|
368
|
+
},
|
|
369
|
+
retryText: {
|
|
370
|
+
fontSize: moderateScale(12),
|
|
371
|
+
color: Colors.primary,
|
|
372
|
+
paddingHorizontal: scale(12),
|
|
373
|
+
paddingTop: verticalScale(6),
|
|
374
|
+
},
|
|
375
|
+
contentWrapper: {
|
|
376
|
+
flex: 1,
|
|
377
|
+
position: 'relative',
|
|
378
|
+
},
|
|
223
379
|
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
|
3
|
+
|
|
4
|
+
const BASE_URL = 'https://app.sptestmfp.com/';
|
|
5
|
+
const DEFAULT_CLIENT_ID = 'vPUz1fFucjlNAV8rujFg7AfGbS3Ay/4=';
|
|
6
|
+
const DEFAULT_AGENT_ID = '3pYw3fp0iTwOge6wjf4i+Fm7QBbx';
|
|
7
|
+
const TOKENS_KEY = '@salespanda/sdk/tokens';
|
|
8
|
+
|
|
9
|
+
export type RuntimeTokens = {
|
|
10
|
+
token: string | null;
|
|
11
|
+
accessToken: string | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type AuthenticateResponse = {
|
|
15
|
+
statusCode: string;
|
|
16
|
+
status: string;
|
|
17
|
+
message: string;
|
|
18
|
+
expire_datetime?: string;
|
|
19
|
+
access_token: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type SsoLoginResponse = {
|
|
23
|
+
statusCode: string;
|
|
24
|
+
status: string;
|
|
25
|
+
message: string;
|
|
26
|
+
token: string;
|
|
27
|
+
response?: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type HomeMenuItem = {
|
|
31
|
+
title: string;
|
|
32
|
+
desciption?: string | null;
|
|
33
|
+
image?: string | null;
|
|
34
|
+
banner_image?: string | null;
|
|
35
|
+
page_key?: string | null;
|
|
36
|
+
link?: string | null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type HomeBannerItem = {
|
|
40
|
+
title?: string | null;
|
|
41
|
+
desciption?: string | null;
|
|
42
|
+
image: string;
|
|
43
|
+
page_key?: string | null;
|
|
44
|
+
link?: string | null;
|
|
45
|
+
user_type?: string | null;
|
|
46
|
+
app_page?: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type HomeResponse = {
|
|
50
|
+
statusCode: string;
|
|
51
|
+
status: string;
|
|
52
|
+
message: string;
|
|
53
|
+
response: {
|
|
54
|
+
menu: HomeMenuItem[];
|
|
55
|
+
home_banner: HomeBannerItem[];
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let runtimeTokens: RuntimeTokens = {
|
|
61
|
+
token: null,
|
|
62
|
+
accessToken: null,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const setRuntimeTokens = (tokens: Partial<RuntimeTokens>) => {
|
|
66
|
+
runtimeTokens = { ...runtimeTokens, ...tokens };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const getRuntimeTokens = (): RuntimeTokens => runtimeTokens;
|
|
70
|
+
|
|
71
|
+
export const loadPersistedTokens = async (): Promise<RuntimeTokens> => {
|
|
72
|
+
try {
|
|
73
|
+
const stored = await AsyncStorage.getItem(TOKENS_KEY);
|
|
74
|
+
if (stored) {
|
|
75
|
+
const parsed = JSON.parse(stored) as RuntimeTokens;
|
|
76
|
+
runtimeTokens = {
|
|
77
|
+
token: parsed.token || null,
|
|
78
|
+
accessToken: parsed.accessToken || null,
|
|
79
|
+
};
|
|
80
|
+
return runtimeTokens;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// ignore storage failures
|
|
84
|
+
}
|
|
85
|
+
return { token: null, accessToken: null };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const persistTokens = async (tokens: Partial<RuntimeTokens>) => {
|
|
89
|
+
const next = { ...runtimeTokens, ...tokens };
|
|
90
|
+
runtimeTokens = next;
|
|
91
|
+
try {
|
|
92
|
+
await AsyncStorage.setItem(TOKENS_KEY, JSON.stringify(next));
|
|
93
|
+
} catch {
|
|
94
|
+
// ignore storage failures
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const clearPersistedTokens = async () => {
|
|
99
|
+
runtimeTokens = { token: null, accessToken: null };
|
|
100
|
+
try {
|
|
101
|
+
await AsyncStorage.removeItem(TOKENS_KEY);
|
|
102
|
+
} catch {
|
|
103
|
+
// ignore storage failures
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const baseQuery = fetchBaseQuery({
|
|
108
|
+
baseUrl: BASE_URL,
|
|
109
|
+
prepareHeaders: (headers) => {
|
|
110
|
+
if (runtimeTokens.token) {
|
|
111
|
+
headers.set('Token', runtimeTokens.token);
|
|
112
|
+
}
|
|
113
|
+
return headers;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
export const salespandaApi = createApi({
|
|
118
|
+
reducerPath: 'salespandaApi',
|
|
119
|
+
baseQuery,
|
|
120
|
+
endpoints: (builder) => ({
|
|
121
|
+
authenticate: builder.mutation<
|
|
122
|
+
AuthenticateResponse,
|
|
123
|
+
{ agent_id?: string; clientId?: string }
|
|
124
|
+
>({
|
|
125
|
+
query: ({ agent_id = DEFAULT_AGENT_ID, clientId }) => ({
|
|
126
|
+
url: 'framework/api/authenticate',
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: {
|
|
129
|
+
'Content-Type': 'application/json',
|
|
130
|
+
'Client-Id': clientId || DEFAULT_CLIENT_ID,
|
|
131
|
+
},
|
|
132
|
+
body: { agent_id },
|
|
133
|
+
}),
|
|
134
|
+
}),
|
|
135
|
+
spssoLogin: builder.mutation<
|
|
136
|
+
SsoLoginResponse,
|
|
137
|
+
{ access_token: string; app_version?: string }
|
|
138
|
+
>({
|
|
139
|
+
query: ({ access_token, app_version = '1.0' }) => ({
|
|
140
|
+
url: 'framework/api/spssologin',
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: {
|
|
143
|
+
'Content-Type': 'application/json',
|
|
144
|
+
},
|
|
145
|
+
body: { app_version, access_token },
|
|
146
|
+
}),
|
|
147
|
+
}),
|
|
148
|
+
getHome: builder.query<
|
|
149
|
+
HomeResponse,
|
|
150
|
+
{ app_version?: string; tokenOverride?: string }
|
|
151
|
+
>({
|
|
152
|
+
query: ({ app_version = '5.0.1', tokenOverride } = {}) => ({
|
|
153
|
+
url: 'manager/apis/V2/app-home-screen.php',
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: tokenOverride
|
|
156
|
+
? {
|
|
157
|
+
'Token': tokenOverride,
|
|
158
|
+
'Content-Type': 'application/json',
|
|
159
|
+
}
|
|
160
|
+
: {
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
},
|
|
163
|
+
body: { app_version },
|
|
164
|
+
}),
|
|
165
|
+
}),
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
export const {
|
|
170
|
+
useAuthenticateMutation,
|
|
171
|
+
useSpssoLoginMutation,
|
|
172
|
+
useLazyGetHomeQuery,
|
|
173
|
+
} = salespandaApi;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import {
|
|
3
|
+
clearPersistedTokens,
|
|
4
|
+
getRuntimeTokens,
|
|
5
|
+
setRuntimeTokens,
|
|
6
|
+
} from './api';
|
|
7
|
+
|
|
8
|
+
const USER_KEY = '@salespanda/sdk/user';
|
|
9
|
+
|
|
10
|
+
export type AuthUser = {
|
|
11
|
+
id?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
email?: string;
|
|
14
|
+
phone?: string;
|
|
15
|
+
raw?: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let runtimeUser: AuthUser | null = null;
|
|
19
|
+
|
|
20
|
+
export const login = async (
|
|
21
|
+
user?: AuthUser,
|
|
22
|
+
token?: string,
|
|
23
|
+
accessToken?: string
|
|
24
|
+
) => {
|
|
25
|
+
if (token) {
|
|
26
|
+
setRuntimeTokens({ token });
|
|
27
|
+
}
|
|
28
|
+
if (accessToken) {
|
|
29
|
+
setRuntimeTokens({ accessToken });
|
|
30
|
+
}
|
|
31
|
+
if (user) {
|
|
32
|
+
runtimeUser = user;
|
|
33
|
+
try {
|
|
34
|
+
await AsyncStorage.setItem(USER_KEY, JSON.stringify(user));
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore storage failures
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const logout = async () => {
|
|
42
|
+
runtimeUser = null;
|
|
43
|
+
await clearPersistedTokens();
|
|
44
|
+
try {
|
|
45
|
+
await AsyncStorage.removeItem(USER_KEY);
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore storage failures
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const isAuthenticated = (): boolean => {
|
|
52
|
+
const tokens = getRuntimeTokens();
|
|
53
|
+
return Boolean(tokens.token || tokens.accessToken);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const getCurrentUser = async (): Promise<AuthUser | null> => {
|
|
57
|
+
if (runtimeUser) {
|
|
58
|
+
return runtimeUser;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const stored = await AsyncStorage.getItem(USER_KEY);
|
|
62
|
+
if (stored) {
|
|
63
|
+
runtimeUser = JSON.parse(stored) as AuthUser;
|
|
64
|
+
return runtimeUser;
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// ignore storage failures
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const getCurrentToken = (): string | null => {
|
|
73
|
+
const tokens = getRuntimeTokens();
|
|
74
|
+
return tokens.token || tokens.accessToken;
|
|
75
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { configureStore } from '@reduxjs/toolkit';
|
|
2
|
+
import { setupListeners } from '@reduxjs/toolkit/query';
|
|
3
|
+
import { salespandaApi } from '../services/api';
|
|
4
|
+
|
|
5
|
+
export const store = configureStore({
|
|
6
|
+
reducer: {
|
|
7
|
+
[salespandaApi.reducerPath]: salespandaApi.reducer,
|
|
8
|
+
},
|
|
9
|
+
middleware: (getDefaultMiddleware) =>
|
|
10
|
+
getDefaultMiddleware().concat(salespandaApi.middleware),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
setupListeners(store.dispatch);
|
|
14
|
+
|
|
15
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
16
|
+
export type AppDispatch = typeof store.dispatch;
|