react-native-salespanda 0.4.3 → 0.4.5

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 (42) hide show
  1. package/lib/module/SalespandaApp.js +16 -8
  2. package/lib/module/SalespandaApp.js.map +1 -1
  3. package/lib/module/assets/images/index.js +20 -0
  4. package/lib/module/assets/images/index.js.map +1 -0
  5. package/lib/module/components/Loader.js +45 -0
  6. package/lib/module/components/Loader.js.map +1 -0
  7. package/lib/module/config/SalespandaConfig.js +17 -0
  8. package/lib/module/config/SalespandaConfig.js.map +1 -1
  9. package/lib/module/index.js +1 -32
  10. package/lib/module/index.js.map +1 -1
  11. package/lib/module/screens/Tabs/HomeScreen.js +174 -83
  12. package/lib/module/screens/Tabs/HomeScreen.js.map +1 -1
  13. package/lib/module/services/api.js +132 -0
  14. package/lib/module/services/api.js.map +1 -0
  15. package/lib/module/services/authService.js +59 -0
  16. package/lib/module/services/authService.js.map +1 -0
  17. package/lib/module/store/index.js +13 -0
  18. package/lib/module/store/index.js.map +1 -0
  19. package/lib/typescript/src/SalespandaApp.d.ts.map +1 -1
  20. package/lib/typescript/src/components/Loader.d.ts +11 -0
  21. package/lib/typescript/src/components/Loader.d.ts.map +1 -0
  22. package/lib/typescript/src/config/SalespandaConfig.d.ts +14 -0
  23. package/lib/typescript/src/config/SalespandaConfig.d.ts.map +1 -1
  24. package/lib/typescript/src/index.d.ts +1 -12
  25. package/lib/typescript/src/index.d.ts.map +1 -1
  26. package/lib/typescript/src/screens/Tabs/HomeScreen.d.ts.map +1 -1
  27. package/lib/typescript/src/services/api.d.ts +638 -0
  28. package/lib/typescript/src/services/api.d.ts.map +1 -0
  29. package/lib/typescript/src/services/authService.d.ts +13 -0
  30. package/lib/typescript/src/services/authService.d.ts.map +1 -0
  31. package/lib/typescript/src/store/index.d.ts +36 -0
  32. package/lib/typescript/src/store/index.d.ts.map +1 -0
  33. package/package.json +9 -3
  34. package/react-native.config.js +8 -5
  35. package/src/SalespandaApp.tsx +18 -10
  36. package/src/components/Loader.tsx +48 -0
  37. package/src/config/SalespandaConfig.ts +37 -0
  38. package/src/index.tsx +8 -30
  39. package/src/screens/Tabs/HomeScreen.tsx +220 -61
  40. package/src/services/api.ts +173 -0
  41. package/src/services/authService.ts +75 -0
  42. package/src/store/index.ts +16 -0
@@ -9,22 +9,42 @@ import {
9
9
  FlatList,
10
10
  Dimensions,
11
11
  } from 'react-native';
12
- import { scale, verticalScale, moderateScale } from 'react-native-size-matters';
13
- import { Colors } from '../../constants/Colors';
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
- icon: string;
35
+ imageUrl?: string | null;
20
36
  onPress?: () => void;
21
37
  }
22
38
 
23
- const MenuItem: React.FC<MenuItemProps> = ({ title, icon, onPress }) => {
39
+ const MenuItem: React.FC<MenuItemProps> = ({ title, imageUrl, onPress }) => {
24
40
  return (
25
41
  <TouchableOpacity style={styles.menuItem} onPress={onPress}>
26
42
  <View style={styles.iconContainer}>
27
- <Text style={styles.iconText}>{icon}</Text>
43
+ {imageUrl ? (
44
+ <Image source={{ uri: imageUrl }} style={styles.menuItemImage} />
45
+ ) : (
46
+ <Text style={styles.iconText}>📌</Text>
47
+ )}
28
48
  </View>
29
49
  <Text style={styles.menuItemText}>{title}</Text>
30
50
  </TouchableOpacity>
@@ -32,14 +52,34 @@ const MenuItem: React.FC<MenuItemProps> = ({ title, icon, onPress }) => {
32
52
  };
33
53
 
34
54
  const HomeScreen: React.FC = () => {
35
- const images = [
36
- 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?q=80&w=1200&auto=format&fit=crop',
37
- 'https://images.unsplash.com/photo-1482192596544-9eb780fc7f66?q=80&w=1200&auto=format&fit=crop',
38
- 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?q=80&w=1200&auto=format&fit=crop',
39
- 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1200&auto=format&fit=crop',
40
- 'https://images.unsplash.com/photo-1460353581641-37baddab0fa2?q=80&w=1200&auto=format&fit=crop',
41
- 'https://images.unsplash.com/photo-1496302662116-35cc4f36df92?q=80&w=1200&auto=format&fit=crop',
42
- ];
55
+ const [loading, setLoading] = React.useState<boolean>(false);
56
+ const [error, setError] = React.useState<string | null>(null);
57
+ const [homeData, setHomeData] = React.useState<
58
+ HomeResponse['response'] | null
59
+ >(null);
60
+
61
+ type BannerItem = {
62
+ image: string;
63
+ title?: string;
64
+ };
65
+
66
+ const [authenticate] = useAuthenticateMutation();
67
+ const [spssoLogin] = useSpssoLoginMutation();
68
+ const [triggerHome, { isFetching: isHomeFetching }] = useLazyGetHomeQuery();
69
+
70
+ const bannerItems: BannerItem[] = React.useMemo(
71
+ () =>
72
+ homeData?.home_banner && homeData.home_banner.length > 0
73
+ ? homeData.home_banner.map((item) => ({
74
+ image: item.image,
75
+ title: item.title ?? undefined,
76
+ }))
77
+ : [],
78
+ [homeData?.home_banner]
79
+ );
80
+
81
+ const menuItems = React.useMemo(() => homeData?.menu ?? [], [homeData?.menu]);
82
+
43
83
  const screenWidth = Dimensions.get('window').width;
44
84
  const [activeIndex, setActiveIndex] = React.useState(0);
45
85
  const flatListRef = React.useRef<any>(null);
@@ -49,10 +89,92 @@ const HomeScreen: React.FC = () => {
49
89
  const index = Math.round(offsetX / screenWidth);
50
90
  setActiveIndex(index);
51
91
  };
92
+
93
+ const loadHomeData = React.useCallback(
94
+ async (isRetry?: boolean) => {
95
+ try {
96
+ setLoading(true);
97
+ setError(null);
98
+
99
+ const storedTokens = await loadPersistedTokens();
100
+ let accessToken = storedTokens.accessToken;
101
+ let token = storedTokens.token;
102
+
103
+ if (!accessToken) {
104
+ const authResp = await authenticate({}).unwrap();
105
+ accessToken = authResp.access_token;
106
+ setRuntimeTokens({ accessToken });
107
+ await persistTokens({ accessToken });
108
+ }
109
+
110
+ if (!token && accessToken) {
111
+ const ssoResp = await spssoLogin({
112
+ access_token: accessToken,
113
+ }).unwrap();
114
+ token = ssoResp.token;
115
+ setRuntimeTokens({ token });
116
+ await persistTokens({ token });
117
+ }
118
+
119
+ if (!token) {
120
+ throw new Error('Missing auth token, please try again.');
121
+ }
122
+
123
+ const tokenHeader: string | undefined = token ?? undefined;
124
+ const homeResp = await triggerHome({
125
+ tokenOverride: tokenHeader,
126
+ }).unwrap();
127
+
128
+ const isSuccess =
129
+ homeResp.statusCode === '200' &&
130
+ homeResp.status?.toLowerCase() === 'success';
131
+ if (!isSuccess) {
132
+ const code = homeResp.statusCode;
133
+
134
+ // If unauthorized, clear tokens once and retry
135
+ if (
136
+ !isRetry &&
137
+ code &&
138
+ (code === '1008' || code === '401' || code === '403')
139
+ ) {
140
+ await clearPersistedTokens();
141
+ setRuntimeTokens({ token: null, accessToken: null });
142
+ await loadHomeData(true);
143
+ return;
144
+ }
145
+
146
+ throw new Error(homeResp.message || 'Failed to load home data');
147
+ }
148
+
149
+ setHomeData(homeResp.response);
150
+ setLoading(false);
151
+ } catch (e: any) {
152
+ const statusCode = e?.status || e?.data?.statusCode;
153
+ if (statusCode === 401 || statusCode === 403) {
154
+ // For unauthorized keep loader spinning (no static fallback)
155
+ setError(null);
156
+ return;
157
+ }
158
+ setError(
159
+ e?.data?.message ||
160
+ e?.message ||
161
+ 'Failed to load home data. Please try again.'
162
+ );
163
+ setLoading(false);
164
+ }
165
+ },
166
+ [authenticate, spssoLogin, triggerHome]
167
+ );
168
+
169
+ React.useEffect(() => {
170
+ loadHomeData();
171
+ }, [loadHomeData]);
172
+
52
173
  React.useEffect(() => {
53
174
  const id = setInterval(() => {
54
175
  setActiveIndex((prev) => {
55
- const next = (prev + 1) % images.length;
176
+ const next =
177
+ bannerItems.length > 0 ? (prev + 1) % bannerItems.length : 0;
56
178
  if (flatListRef.current) {
57
179
  try {
58
180
  flatListRef.current.scrollToIndex({ index: next, animated: true });
@@ -63,23 +185,29 @@ const HomeScreen: React.FC = () => {
63
185
  return next;
64
186
  });
65
187
  }, 2500);
66
- return () => clearInterval(id);
67
- }, [images.length]);
188
+ return () => {
189
+ clearInterval(id);
190
+ };
191
+ }, [bannerItems.length]);
68
192
 
69
193
  return (
70
194
  <SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>
71
195
  <View style={styles.container}>
72
196
  <TabsHeader title="Home" />
73
- <ScrollView style={styles.scrollView}>
74
- {/* Edge-to-edge Image Carousel */}
197
+
198
+ <View style={styles.contentWrapper}>
199
+ {loading || isHomeFetching || !homeData ? (
200
+ <Loader overlay message="Loading home data..." />
201
+ ) : null}
202
+
75
203
  <View style={styles.carouselContainer}>
76
204
  <FlatList
77
205
  ref={flatListRef}
78
- data={images}
79
- keyExtractor={(_, idx) => `${idx}`}
206
+ data={bannerItems}
207
+ keyExtractor={(item, idx) => `${item.title || 'banner'}-${idx}`}
80
208
  renderItem={({ item }) => (
81
209
  <Image
82
- source={{ uri: item }}
210
+ source={{ uri: item.image }}
83
211
  style={[styles.carouselImage, { width: screenWidth }]}
84
212
  />
85
213
  )}
@@ -93,9 +221,8 @@ const HomeScreen: React.FC = () => {
93
221
  index,
94
222
  })}
95
223
  />
96
- {/* Overlay Indicators at bottom of the image */}
97
224
  <View style={styles.carouselIndicatorsOverlay}>
98
- {images.map((_, idx) => (
225
+ {bannerItems.map((_, idx) => (
99
226
  <View
100
227
  key={idx}
101
228
  style={[
@@ -107,30 +234,32 @@ const HomeScreen: React.FC = () => {
107
234
  </View>
108
235
  </View>
109
236
 
110
- {/* Menu Grid */}
111
- <View style={styles.menuGrid}>
112
- <MenuItem title="Content Library" icon="📚" />
113
- <MenuItem title="DigiCard" icon="�" />
114
- <MenuItem title="Leads" icon="🎯" />
237
+ <ScrollView
238
+ style={styles.scrollView}
239
+ contentContainerStyle={styles.scrollContent}
240
+ >
241
+ {error ? (
242
+ <>
243
+ <Text style={styles.errorText}>{error}</Text>
244
+ <TouchableOpacity onPress={() => loadHomeData()}>
245
+ <Text style={styles.retryText}>Tap to retry</Text>
246
+ </TouchableOpacity>
247
+ </>
248
+ ) : null}
115
249
 
116
- <MenuItem title="Email Campaign" icon="📧" />
117
- <MenuItem title="Cadence" icon="⏰" />
118
- <MenuItem title="Social Setup" icon="�" />
119
-
120
- <MenuItem title="Quiz,Calculator & Referrals" icon="❓" />
121
- <MenuItem title="My Profile" icon="�" />
122
-
123
- <MenuItem title="Microsite Setup" icon="⚙️" />
124
- <MenuItem title="My Activity" icon="📊" />
125
- <MenuItem title="Internal Communication" icon="💬" />
126
-
127
- <MenuItem title="Help Videos" icon="🎥" />
128
- <MenuItem title="Import Gmail" icon="�" />
129
- <MenuItem title="Proposal" icon="📋" />
130
-
131
- <MenuItem title="Courses" icon="🎓" />
132
- </View>
133
- </ScrollView>
250
+ {menuItems.length > 0 ? (
251
+ <View style={styles.menuGrid}>
252
+ {menuItems.map((item) => (
253
+ <MenuItem
254
+ key={item.title}
255
+ title={item.title}
256
+ imageUrl={item.image}
257
+ />
258
+ ))}
259
+ </View>
260
+ ) : null}
261
+ </ScrollView>
262
+ </View>
134
263
  </View>
135
264
  </SafeAreaView>
136
265
  );
@@ -151,20 +280,24 @@ const styles = StyleSheet.create({
151
280
  flex: 1,
152
281
  backgroundColor: Colors.white,
153
282
  },
283
+ scrollContent: {
284
+ paddingBottom: verticalScale(24),
285
+ },
154
286
  carouselContainer: {
155
287
  width: '100%',
156
- backgroundColor: Colors.white, // subtle backplate for image load
288
+ backgroundColor: Colors.white,
157
289
  position: 'relative',
158
290
  },
159
291
  carouselImage: {
160
- height: verticalScale(200),
161
- resizeMode: 'cover',
292
+ height: verticalScale(155),
293
+ resizeMode: 'contain',
294
+ backgroundColor: Colors.white,
162
295
  },
163
296
  carouselIndicatorsOverlay: {
164
297
  position: 'absolute',
165
298
  left: 0,
166
299
  right: 0,
167
- bottom: verticalScale(12),
300
+ bottom: verticalScale(20),
168
301
  flexDirection: 'row',
169
302
  justifyContent: 'center',
170
303
  alignItems: 'center',
@@ -183,30 +316,28 @@ const styles = StyleSheet.create({
183
316
  menuGrid: {
184
317
  flexDirection: 'row',
185
318
  flexWrap: 'wrap',
186
- padding: scale(8),
187
- justifyContent: 'space-between',
319
+ paddingTop: scale(8),
320
+ paddingBottom: verticalScale(16),
321
+ justifyContent: 'flex-start',
322
+ alignSelf: 'center',
323
+ width: '94%',
188
324
  },
189
325
  menuItem: {
190
- width: '31%',
326
+ width: '29.5%',
191
327
  aspectRatio: 1,
192
328
  backgroundColor: Colors.lightblue,
193
329
  borderRadius: moderateScale(16),
194
- padding: scale(12),
195
- marginBottom: verticalScale(12),
330
+ padding: moderateScale(12),
331
+ marginBottom: moderateVerticalScale(8),
332
+ marginHorizontal: scale(6),
196
333
  alignItems: 'center',
197
334
  justifyContent: 'center',
198
- shadowColor: Colors.black,
199
- shadowOffset: { width: 0, height: 0 },
200
- shadowOpacity: 0.1,
201
- shadowRadius: 0.5,
202
335
  borderWidth: 1,
203
336
  borderColor: Colors.border,
204
337
  },
205
338
  iconContainer: {
206
339
  width: scale(50),
207
340
  height: scale(50),
208
- borderRadius: moderateScale(25),
209
- backgroundColor: Colors.divider,
210
341
  justifyContent: 'center',
211
342
  alignItems: 'center',
212
343
  marginBottom: verticalScale(8),
@@ -214,10 +345,38 @@ const styles = StyleSheet.create({
214
345
  iconText: {
215
346
  fontSize: moderateScale(24),
216
347
  },
348
+ menuItemImage: {
349
+ width: scale(40),
350
+ height: scale(40),
351
+ borderRadius: moderateScale(20),
352
+ resizeMode: 'cover',
353
+ },
217
354
  menuItemText: {
218
355
  fontSize: moderateScale(11),
219
356
  textAlign: 'center',
220
357
  color: Colors.black,
221
358
  fontWeight: '500',
222
359
  },
360
+ statusText: {
361
+ fontSize: moderateScale(12),
362
+ color: Colors.black,
363
+ paddingHorizontal: scale(12),
364
+ paddingTop: verticalScale(8),
365
+ },
366
+ errorText: {
367
+ fontSize: moderateScale(12),
368
+ color: 'red',
369
+ paddingHorizontal: scale(12),
370
+ paddingTop: verticalScale(4),
371
+ },
372
+ retryText: {
373
+ fontSize: moderateScale(12),
374
+ color: Colors.primary,
375
+ paddingHorizontal: scale(12),
376
+ paddingTop: verticalScale(6),
377
+ },
378
+ contentWrapper: {
379
+ flex: 1,
380
+ position: 'relative',
381
+ },
223
382
  });
@@ -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;