saafe-redirection-flow 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/.github/workflows/build-and-deploy.yml +41 -0
  2. package/.gitlab-ci.yml +108 -0
  3. package/.releaserc.json +18 -0
  4. package/.storybook/main.ts +28 -0
  5. package/.storybook/preview.ts +16 -0
  6. package/.storybook/vitest.setup.ts +9 -0
  7. package/.vite/deps/@radix-ui_react-avatar.js +230 -0
  8. package/.vite/deps/@radix-ui_react-avatar.js.map +7 -0
  9. package/.vite/deps/@radix-ui_react-slot.js +12 -0
  10. package/.vite/deps/@radix-ui_react-slot.js.map +7 -0
  11. package/.vite/deps/_metadata.json +79 -0
  12. package/.vite/deps/chunk-5VGQBUCU.js +597 -0
  13. package/.vite/deps/chunk-5VGQBUCU.js.map +7 -0
  14. package/.vite/deps/chunk-DC5AMYBS.js +38 -0
  15. package/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
  16. package/.vite/deps/chunk-HUIEPYH7.js +11265 -0
  17. package/.vite/deps/chunk-HUIEPYH7.js.map +7 -0
  18. package/.vite/deps/chunk-TKHB4QMX.js +281 -0
  19. package/.vite/deps/chunk-TKHB4QMX.js.map +7 -0
  20. package/.vite/deps/chunk-YLDSBLSF.js +1139 -0
  21. package/.vite/deps/chunk-YLDSBLSF.js.map +7 -0
  22. package/.vite/deps/class-variance-authority.js +63 -0
  23. package/.vite/deps/class-variance-authority.js.map +7 -0
  24. package/.vite/deps/lucide-react.js +36984 -0
  25. package/.vite/deps/lucide-react.js.map +7 -0
  26. package/.vite/deps/package.json +3 -0
  27. package/.vite/deps/react-dom_client.js +17917 -0
  28. package/.vite/deps/react-dom_client.js.map +7 -0
  29. package/.vite/deps/react-router-dom.js +452 -0
  30. package/.vite/deps/react-router-dom.js.map +7 -0
  31. package/.vite/deps/react-router.js +234 -0
  32. package/.vite/deps/react-router.js.map +7 -0
  33. package/.vite/deps/react.js +5 -0
  34. package/.vite/deps/react.js.map +7 -0
  35. package/.vite/deps/react_jsx-dev-runtime.js +470 -0
  36. package/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  37. package/CHANGELOG.md +420 -0
  38. package/LICENSE +21 -0
  39. package/README.md +129 -0
  40. package/RELEASE_CHEATSHEET.md +93 -0
  41. package/RELEASE_NOTES.md +120 -0
  42. package/components.json +21 -0
  43. package/docs/DEPLOYMENT_WORKFLOW.md +262 -0
  44. package/docs/RELEASE_GUIDE.md +591 -0
  45. package/docs/architecture.md +432 -0
  46. package/docs/components.md +199 -0
  47. package/docs/index.md +69 -0
  48. package/docs/local-release-workflow.md +234 -0
  49. package/docs/routes.md +118 -0
  50. package/docs/sdk-integration.md +325 -0
  51. package/docs/semantic-release.md +124 -0
  52. package/docs/user-flow.md +206 -0
  53. package/eslint.config.js +28 -0
  54. package/index.html +19 -0
  55. package/install.sh +198 -0
  56. package/package.json +115 -0
  57. package/public/images/bank-logo.png +0 -0
  58. package/public/saafe-icon.svg +9 -0
  59. package/src/App.tsx +171 -0
  60. package/src/__tests__/url-parameters.test.ts +82 -0
  61. package/src/assets/brand/applestore.svg +13 -0
  62. package/src/assets/brand/playstore.svg +23 -0
  63. package/src/assets/brand/saafe-color-white-logo.svg +14 -0
  64. package/src/assets/brand/saafe-icon.svg +9 -0
  65. package/src/assets/brand/saafe-logo.svg +18 -0
  66. package/src/assets/icons/check-icon-dark.svg +27 -0
  67. package/src/assets/icons/check-icon.svg +23 -0
  68. package/src/components/ErrorBoundary.tsx +132 -0
  69. package/src/components/alert/alert.tsx +27 -0
  70. package/src/components/auth/AuthGuard.tsx +76 -0
  71. package/src/components/cards/BankCard.stories.tsx +69 -0
  72. package/src/components/cards/BankCard.tsx +227 -0
  73. package/src/components/cards/OuterCard.tsx +109 -0
  74. package/src/components/cards/WrapperCard.tsx +64 -0
  75. package/src/components/documents/PrivacyContent.tsx +1 -0
  76. package/src/components/dummyFooter.tsx +29 -0
  77. package/src/components/icons/github.tsx +12 -0
  78. package/src/components/language/LanguageSwitcher.tsx +44 -0
  79. package/src/components/layouts/FrostedLayout.stories.tsx +42 -0
  80. package/src/components/layouts/FrostedLayout.tsx +333 -0
  81. package/src/components/layouts/MobileLayout.tsx +403 -0
  82. package/src/components/mobile-background.tsx +136 -0
  83. package/src/components/mobileAppDownload.tsx +30 -0
  84. package/src/components/modal/ModalComp.tsx +27 -0
  85. package/src/components/mode-toggle.tsx +36 -0
  86. package/src/components/page-header.tsx +50 -0
  87. package/src/components/session/SessionTimeoutScreen.tsx +134 -0
  88. package/src/components/session/SessionTimer.tsx +173 -0
  89. package/src/components/step-navigation.tsx +87 -0
  90. package/src/components/title/AppBar.stories.tsx +50 -0
  91. package/src/components/title/AppBar.tsx +150 -0
  92. package/src/components/title/SectionTitle.tsx +31 -0
  93. package/src/components/ui/AnimatedButton.module.css +13 -0
  94. package/src/components/ui/alert.tsx +66 -0
  95. package/src/components/ui/animatedButton.tsx +111 -0
  96. package/src/components/ui/avatar.tsx +51 -0
  97. package/src/components/ui/badge.tsx +36 -0
  98. package/src/components/ui/bottom-sheet.tsx +122 -0
  99. package/src/components/ui/button.tsx +59 -0
  100. package/src/components/ui/calendar.tsx +86 -0
  101. package/src/components/ui/card.tsx +92 -0
  102. package/src/components/ui/checkbox.stories.tsx +49 -0
  103. package/src/components/ui/checkbox.tsx +67 -0
  104. package/src/components/ui/collapsible.tsx +45 -0
  105. package/src/components/ui/dialog.tsx +134 -0
  106. package/src/components/ui/document-link.tsx +26 -0
  107. package/src/components/ui/dot-stepper.tsx +57 -0
  108. package/src/components/ui/dropdown-menu.tsx +255 -0
  109. package/src/components/ui/form.tsx +165 -0
  110. package/src/components/ui/frosted-panel.stories.tsx +86 -0
  111. package/src/components/ui/frosted-panel.tsx +276 -0
  112. package/src/components/ui/input.tsx +39 -0
  113. package/src/components/ui/label.stories.tsx +67 -0
  114. package/src/components/ui/label.tsx +23 -0
  115. package/src/components/ui/mobile-footer.tsx +54 -0
  116. package/src/components/ui/modal.tsx +90 -0
  117. package/src/components/ui/otp-input.stories.tsx +62 -0
  118. package/src/components/ui/otp-input.tsx +221 -0
  119. package/src/components/ui/platform-specific-behavior.tsx +28 -0
  120. package/src/components/ui/popover.tsx +46 -0
  121. package/src/components/ui/progress.tsx +103 -0
  122. package/src/components/ui/radio-group.tsx +45 -0
  123. package/src/components/ui/scroll-area.tsx +56 -0
  124. package/src/components/ui/sdk-params-docs.tsx +53 -0
  125. package/src/components/ui/select.tsx +159 -0
  126. package/src/components/ui/separator.tsx +28 -0
  127. package/src/components/ui/sheet.tsx +137 -0
  128. package/src/components/ui/sidebar.tsx +724 -0
  129. package/src/components/ui/skeleton.stories.tsx +50 -0
  130. package/src/components/ui/skeleton.tsx +15 -0
  131. package/src/components/ui/sonner.tsx +23 -0
  132. package/src/components/ui/step.stories.tsx +132 -0
  133. package/src/components/ui/step.tsx +234 -0
  134. package/src/components/ui/stepper-progress.tsx +136 -0
  135. package/src/components/ui/stepper.tsx +259 -0
  136. package/src/components/ui/tabs.tsx +55 -0
  137. package/src/components/ui/tooltip.tsx +61 -0
  138. package/src/components/ui/url-decode-loader.tsx +36 -0
  139. package/src/components/ui/version-display.tsx +104 -0
  140. package/src/components/ui/web-footer.tsx +36 -0
  141. package/src/config/environments.ts +99 -0
  142. package/src/config/urls.ts +53 -0
  143. package/src/const/fiTypeCategoryMap.ts +19 -0
  144. package/src/contexts/LanguageContext.tsx +41 -0
  145. package/src/contexts/RTLContext.tsx +42 -0
  146. package/src/contexts/ThemeContext.tsx +93 -0
  147. package/src/hooks/use-account-discovery.ts +205 -0
  148. package/src/hooks/use-auth-query.ts +141 -0
  149. package/src/hooks/use-fip-query.ts +72 -0
  150. package/src/hooks/use-media-query.ts +32 -0
  151. package/src/hooks/use-mobile.ts +24 -0
  152. package/src/hooks/use-page-title.tsx +48 -0
  153. package/src/hooks/use-platform.ts +52 -0
  154. package/src/hooks/use-trusted-count.ts +21 -0
  155. package/src/hooks/use-url-decode.ts +90 -0
  156. package/src/hooks/useStep.ts +170 -0
  157. package/src/index.css +154 -0
  158. package/src/interfaces/app.interfaces.ts +39 -0
  159. package/src/interfaces/services.interfaces.ts +65 -0
  160. package/src/lib/i18n.ts +68 -0
  161. package/src/lib/utils.ts +6 -0
  162. package/src/locales/en/common.json +167 -0
  163. package/src/locales/hi/common.json +137 -0
  164. package/src/locales/kn/common.json +137 -0
  165. package/src/locales/ml/common.json +137 -0
  166. package/src/locales/ta/common.json +137 -0
  167. package/src/locales/te/common.json +137 -0
  168. package/src/locales/ur/common.json +138 -0
  169. package/src/main.tsx +46 -0
  170. package/src/pages/Login.tsx +363 -0
  171. package/src/pages/accounts/AccountsToProceed.tsx +396 -0
  172. package/src/pages/accounts/Discover.tsx +76 -0
  173. package/src/pages/accounts/DiscoverAccount.tsx +751 -0
  174. package/src/pages/accounts/LinkSelectedAccounts.tsx +638 -0
  175. package/src/pages/accounts/OldUser.tsx +329 -0
  176. package/src/pages/accounts/link-accounts.tsx +913 -0
  177. package/src/pages/consent/ReviewConsent.tsx +836 -0
  178. package/src/pages/consent/rejected.tsx +253 -0
  179. package/src/pages/consent/success.tsx +220 -0
  180. package/src/providers/query-provider.tsx +24 -0
  181. package/src/providers/toast-provider.tsx +26 -0
  182. package/src/services/api/account.service.ts +296 -0
  183. package/src/services/api/auth.service.ts +206 -0
  184. package/src/services/api/axios.ts +138 -0
  185. package/src/services/api/consent.service.ts +142 -0
  186. package/src/services/api/decode.service.ts +53 -0
  187. package/src/services/api/feedback.service.ts +34 -0
  188. package/src/services/api/fip.service.ts +187 -0
  189. package/src/services/api/index.ts +9 -0
  190. package/src/services/api/public.service.ts +18 -0
  191. package/src/services/api.ts +2 -0
  192. package/src/services/postMessage.service.ts +179 -0
  193. package/src/store/NavigationBlockContext.tsx +34 -0
  194. package/src/store/auth.store.ts +79 -0
  195. package/src/store/fip.store.ts +396 -0
  196. package/src/store/mandatoryConsent.store.ts +24 -0
  197. package/src/store/redirect.store.ts +73 -0
  198. package/src/store/step.store.ts +124 -0
  199. package/src/stories/Button.stories.ts +53 -0
  200. package/src/stories/Button.tsx +37 -0
  201. package/src/stories/Configure.mdx +364 -0
  202. package/src/stories/Header.stories.ts +33 -0
  203. package/src/stories/Header.tsx +56 -0
  204. package/src/stories/Page.stories.ts +32 -0
  205. package/src/stories/Page.tsx +73 -0
  206. package/src/stories/button.css +30 -0
  207. package/src/stories/header.css +32 -0
  208. package/src/stories/page.css +68 -0
  209. package/src/styles/rtl-utils.css +90 -0
  210. package/src/styles/rtl.css +105 -0
  211. package/src/utils/api-error.ts +26 -0
  212. package/src/utils/cn.ts +10 -0
  213. package/src/utils/error-callback.ts +116 -0
  214. package/src/utils/formatAccountNumber.ts +9 -0
  215. package/src/utils/handleIdentifiers.ts +90 -0
  216. package/src/utils/posthog.ts +67 -0
  217. package/src/utils/toast-helpers.ts +61 -0
  218. package/src/vite-env.d.ts +1 -0
  219. package/stage-aa-2506251021.zip +0 -0
  220. package/tsconfig.app.json +33 -0
  221. package/tsconfig.json +13 -0
  222. package/tsconfig.node.json +24 -0
  223. package/vite.config.ts +45 -0
  224. package/vitest.shims.d.ts +1 -0
  225. package/vitest.workspace.ts +46 -0
@@ -0,0 +1,296 @@
1
+ import axiosInstance from './axios'
2
+ import { handleApiError, showSuccessToast } from '@/utils/toast-helpers'
3
+
4
+ interface BankAccount {
5
+ id: string
6
+ bankName: string
7
+ accountType: string
8
+ accountNumber: string
9
+ balance: number
10
+ logoUrl: string
11
+ }
12
+
13
+ interface DiscoverAccountsRequest {
14
+ mobileNumber: string
15
+ banks: string[] // Bank IDs to discover
16
+ }
17
+
18
+ // Auto discovery types
19
+ export interface AutoDiscoveryRequest {
20
+ Identifiers: {
21
+ type: string
22
+ value: string
23
+ categoryType: string
24
+ }[]
25
+ FiuId: string
26
+ FipId: string[]
27
+ FIType?: string[]
28
+ }
29
+
30
+ export interface AccountDiscoveryRequest {
31
+ Identifiers: {
32
+ type: string
33
+ value: string
34
+ categoryType: string
35
+ }[]
36
+ FiuId: string
37
+ FipId: string
38
+ FITypes: string | string[]
39
+ }
40
+
41
+ export interface AccountDiscoveryResponse {
42
+ DiscoveredAccounts: {
43
+ FIType: string
44
+ accType: string
45
+ accRefNumber: string
46
+ maskedAccNumber: string
47
+ state: string | null
48
+ amcName: string | null
49
+ logoUrl: string | null
50
+ }[]
51
+ signature: string
52
+ }
53
+
54
+ export interface AutoDiscoveryResponse {
55
+ Accounts: {
56
+ fipId: string
57
+ fipName: string
58
+ DiscoveredAccounts: {
59
+ FIType: string
60
+ accType: string
61
+ accRefNumber: string
62
+ maskedAccNumber: string
63
+ state: string | null
64
+ amcName: string | null
65
+ logoUrl: string | null
66
+ }[]
67
+ }[]
68
+ signature: string
69
+ }
70
+
71
+ export interface DiscoveredAccount {
72
+ id: string
73
+ type: string
74
+ maskedAccountNumber: string
75
+ bankName: string
76
+ logoUrl?: string
77
+ isNew?: boolean
78
+ }
79
+
80
+ /**
81
+ * Account service for bank account operations
82
+ */
83
+ export const accountService = {
84
+ /**
85
+ * Get list of all available banks
86
+ */
87
+ getBanks: async () => {
88
+ try {
89
+ const response = await axiosInstance.get('/banks')
90
+ return response.data
91
+ } catch (error) {
92
+ handleApiError(error, 'Failed to fetch banks')
93
+ throw error
94
+ }
95
+ },
96
+
97
+ /**
98
+ * Discover bank accounts linked to a mobile number
99
+ */
100
+ discoverAccounts: async (data: DiscoverAccountsRequest) => {
101
+ try {
102
+ const response = await axiosInstance.post('/accounts/discover', data)
103
+ return response.data
104
+ } catch (error) {
105
+ handleApiError(error, 'Failed to discover accounts')
106
+ throw error
107
+ }
108
+ },
109
+
110
+ /**
111
+ * Get user's linked accounts
112
+ */
113
+ getLinkedAccounts: async (consentHandle: string, fipId?: string) => {
114
+ try {
115
+ const params: Record<string, string> = { consentHandle }
116
+ if (fipId) {
117
+ params.fipId = fipId
118
+ }
119
+ const response = await axiosInstance.get<BankAccount[]>(
120
+ '/User/linkedaccount',
121
+ {
122
+ params
123
+ }
124
+ )
125
+ return response.data
126
+ } catch (error) {
127
+ handleApiError(error, 'Failed to fetch linked accounts')
128
+ throw error
129
+ }
130
+ },
131
+
132
+ /**
133
+ * Link a new account
134
+ */
135
+ linkAccount: async (accountId: string) => {
136
+ try {
137
+ const response = await axiosInstance.post(`/accounts/${accountId}/link`)
138
+ showSuccessToast('Account linked successfully')
139
+ return response.data
140
+ } catch (error) {
141
+ handleApiError(error, 'Failed to link account')
142
+ throw error
143
+ }
144
+ },
145
+
146
+ /**
147
+ * Unlink an account
148
+ */
149
+ unlinkAccount: async (accountId: string) => {
150
+ try {
151
+ const response = await axiosInstance.post(`/accounts/${accountId}/unlink`)
152
+ showSuccessToast('Account unlinked successfully')
153
+ return response.data
154
+ } catch (error) {
155
+ handleApiError(error, 'Failed to unlink account')
156
+ throw error
157
+ }
158
+ },
159
+
160
+ /**
161
+ * Get account details
162
+ */
163
+ getAccountDetails: async (accountId: string) => {
164
+ try {
165
+ const response = await axiosInstance.get<BankAccount>(
166
+ `/accounts/${accountId}`
167
+ )
168
+ return response.data
169
+ } catch (error) {
170
+ handleApiError(error, 'Failed to fetch account details')
171
+ throw error
172
+ }
173
+ },
174
+
175
+ /**
176
+ * Get account transactions
177
+ */
178
+ getAccountTransactions: async (
179
+ accountId: string,
180
+ params: { page: number; limit: number }
181
+ ) => {
182
+ try {
183
+ const response = await axiosInstance.get(
184
+ `/accounts/${accountId}/transactions`,
185
+ { params }
186
+ )
187
+ return response.data
188
+ } catch (error) {
189
+ handleApiError(error, 'Failed to fetch account transactions')
190
+ throw error
191
+ }
192
+ },
193
+
194
+ /**
195
+ * Add new number
196
+ */
197
+ getMobileNumbers: async () => {
198
+ const response = await axiosInstance.get('/User/Identifiers')
199
+ return response.data
200
+ },
201
+
202
+ /**
203
+ * Add new number
204
+ */
205
+ addNewNumber: async (data: {
206
+ value: string
207
+ type: string
208
+ categoryType: string
209
+ }) => {
210
+ const response = await axiosInstance.post('/User/Identifiers', data)
211
+ return response.data
212
+ },
213
+
214
+ /**
215
+ * Verify new number
216
+ */
217
+ verifyNewNumber: async (data: {
218
+ phoneNumber: string
219
+ code: string
220
+ otpUniqueID: string
221
+ identifierType: string
222
+ }) => {
223
+ const response = await axiosInstance.post('/User/Identifiers/Verify', data)
224
+ return response.data
225
+ },
226
+
227
+ /**
228
+ * Auto discover accounts based on identifiers and FIP IDs
229
+ */
230
+ autoDiscoverAccounts: async (data: AutoDiscoveryRequest) => {
231
+ try {
232
+ const response = await axiosInstance.post<AutoDiscoveryResponse>(
233
+ '/User/account/autoDiscovery',
234
+ data
235
+ )
236
+ return response.data
237
+ } catch (error) {
238
+ handleApiError(error, 'Failed to auto-discover accounts')
239
+ throw error
240
+ }
241
+ },
242
+
243
+ /**
244
+ * Used to send the bank verification OTP code
245
+ */
246
+ sendBankOTP: async (data: {
247
+ FipId: string
248
+ Accounts: Array<{
249
+ accRefNumber: string
250
+ FIType: string
251
+ }>
252
+ signature: string
253
+ }) => {
254
+ const response = await axiosInstance.post<AutoDiscoveryResponse>(
255
+ '/User/account/link',
256
+ data
257
+ )
258
+ return response.data
259
+ },
260
+
261
+ /**
262
+ * Used to verify the bank OTP
263
+ */
264
+ verifyBankOTP: async (data: {
265
+ RefNumber: string
266
+ token: string
267
+ fipId: string
268
+ }) => {
269
+ const response = await axiosInstance.post('/User/account/link/verify', data)
270
+ return response.data
271
+ },
272
+
273
+ /**
274
+ * Get most used banks
275
+ */
276
+ getMostUsedBanks: async ({ type }: { type: string }) => {
277
+ const response = await axiosInstance.get(`/User/topFip?fi_type=${type}`)
278
+ return response.data
279
+ },
280
+
281
+ /**
282
+ * Discover accounts based on identifiers and a single FIP ID
283
+ */
284
+ accountDiscovery: async (data: AccountDiscoveryRequest) => {
285
+ try {
286
+ const response = await axiosInstance.post<AccountDiscoveryResponse>(
287
+ '/User/account/discovery',
288
+ data
289
+ )
290
+ return response.data
291
+ } catch (error) {
292
+ handleApiError(error, 'Failed to discover accounts')
293
+ throw error
294
+ }
295
+ }
296
+ }
@@ -0,0 +1,206 @@
1
+ import axios, { AxiosError } from 'axios'
2
+ import { useAuthStore } from '@/store/auth.store'
3
+ import { handleApiError, showSuccessToast } from '@/utils/toast-helpers'
4
+ import { API_URLS } from '@/config/urls'
5
+ import {
6
+ TokenResponse,
7
+ RetryableRequest
8
+ } from '@/interfaces/services.interfaces'
9
+
10
+ // Define API endpoints with absolute URLs
11
+ const API_BASE_URL = API_URLS.BASE_URL
12
+ const AUTH_ENDPOINTS = {
13
+ INIT_OTP: `${API_BASE_URL}/public/user/combined/init-otp`,
14
+ VERIFY_OTP: `${API_BASE_URL}/public/user/combined/verify-otp`,
15
+ REFRESH_TOKEN: `${API_BASE_URL}/public/user/refreshtoken`
16
+ }
17
+
18
+ export interface InitOtpResponse {
19
+ access_token?: string
20
+ expires_in?: number
21
+ refresh_expires_in?: number
22
+ refresh_token?: string
23
+ token_type?: string
24
+ firstName?: string
25
+ lastName?: string
26
+ vuaId?: string
27
+ phoneNumber?: string
28
+ isTwoFactorEnabled?: boolean | null
29
+ isBioMetricsEnabled?: boolean | null
30
+ otpUniqueID: string
31
+ }
32
+
33
+ export interface ErrorResponse {
34
+ ver: string
35
+ txnid: string
36
+ timestamp: string
37
+ errorCode: string
38
+ errorMsg: string | null
39
+ }
40
+
41
+ // Verify OTP extends InitOtpResponse but might have additional fields
42
+ export interface VerifyOtpResponse extends InitOtpResponse {
43
+ isVerified?: boolean
44
+ }
45
+
46
+ // Custom error type
47
+ export type ApiErrorType = Error & {
48
+ response?: {
49
+ data: ErrorResponse
50
+ }
51
+ errorData?: ErrorResponse
52
+ }
53
+
54
+ // Create axios instance with interceptors
55
+ const api = axios.create({
56
+ headers: {
57
+ 'Content-Type': 'application/json'
58
+ }
59
+ })
60
+
61
+ // Define auth service methods
62
+ export const authService = {
63
+ /**
64
+ * Initiates OTP flow for login/signup
65
+ */
66
+ initiateOtp: async (
67
+ phoneNumber: string,
68
+ isTermsAndConditionAgreed: boolean
69
+ ): Promise<InitOtpResponse> => {
70
+ const response = await api.post<InitOtpResponse>(
71
+ AUTH_ENDPOINTS.INIT_OTP,
72
+ {
73
+ phoneNumber,
74
+ isTermsAndConditionAgreed
75
+ }
76
+ )
77
+
78
+ return response.data
79
+ },
80
+
81
+ /**
82
+ * Verifies OTP code sent to the phone number
83
+ */
84
+ verifyOtp: async (
85
+ phoneNumber: string,
86
+ code: string,
87
+ otpUniqueID: string
88
+ ): Promise<VerifyOtpResponse> => {
89
+ const response = await api.post<VerifyOtpResponse>(
90
+ AUTH_ENDPOINTS.VERIFY_OTP,
91
+ {
92
+ phoneNumber,
93
+ code,
94
+ otpUniqueID
95
+ }
96
+ )
97
+
98
+ return response.data
99
+ },
100
+
101
+ /**
102
+ * Refresh the access token using refresh token
103
+ */
104
+ refreshToken: async (refreshToken: string): Promise<TokenResponse> => {
105
+ try {
106
+ // Make a direct axios call to avoid interceptors
107
+ const response = await axios.post<TokenResponse>(
108
+ AUTH_ENDPOINTS.REFRESH_TOKEN,
109
+ {
110
+ refreshToken
111
+ },
112
+ {
113
+ headers: { 'Content-Type': 'application/json' }
114
+ }
115
+ )
116
+
117
+ return response.data
118
+ } catch (error) {
119
+ handleApiError(error, 'Failed to refresh session')
120
+ throw error
121
+ }
122
+ },
123
+
124
+ /**
125
+ * Logout user by clearing state
126
+ */
127
+ logout: () => {
128
+ useAuthStore.getState().logout()
129
+ showSuccessToast('Logged out successfully')
130
+ }
131
+ }
132
+
133
+ // Request interceptor to add the auth token
134
+ api.interceptors.request.use(
135
+ config => {
136
+ const { accessToken } = useAuthStore.getState()
137
+
138
+ if (accessToken) {
139
+ config.headers.Authorization = `Bearer ${accessToken}`
140
+ }
141
+
142
+ return config
143
+ },
144
+ error => Promise.reject(error)
145
+ )
146
+
147
+ // Helper function for token refresh
148
+ const refreshAuthToken = async () => {
149
+ const { refreshToken } = useAuthStore.getState()
150
+
151
+ if (!refreshToken) {
152
+ throw new Error('No refresh token available')
153
+ }
154
+
155
+ const response = await authService.refreshToken(refreshToken)
156
+
157
+ useAuthStore
158
+ .getState()
159
+ .setTokens(response.access_token, response.refresh_token)
160
+
161
+ return response.access_token
162
+ }
163
+
164
+ // Response interceptor to handle errors
165
+ api.interceptors.response.use(
166
+ response => response,
167
+ async (error: AxiosError<ErrorResponse>) => {
168
+ // Get the original request
169
+ const originalRequest = error.config as RetryableRequest
170
+
171
+ // Special case for refresh token endpoint, never retry
172
+ const isRefreshTokenEndpoint = originalRequest.url?.includes('refreshtoken')
173
+
174
+ // Check if the error is due to an expired token and not from the refresh token endpoint
175
+ if (
176
+ error.response?.status === 401 &&
177
+ originalRequest &&
178
+ !originalRequest._retry &&
179
+ !isRefreshTokenEndpoint
180
+ ) {
181
+ originalRequest._retry = true
182
+
183
+ try {
184
+ // Try to refresh the token
185
+ const newToken = await refreshAuthToken()
186
+
187
+ // Update the Authorization header with the new token
188
+ originalRequest.headers = originalRequest.headers || {}
189
+ originalRequest.headers.Authorization = `Bearer ${newToken}`
190
+
191
+ // Retry the original request with the new token
192
+ return api(originalRequest)
193
+ } catch (refreshError) {
194
+ // Logout the user on refresh token failure
195
+ useAuthStore.getState().logout()
196
+ handleApiError(refreshError, 'Session expired. Please login again.')
197
+ return Promise.reject(new Error('Session expired. Please login again.'))
198
+ }
199
+ }
200
+
201
+ // Handle all API errors
202
+ // handleApiError(error)
203
+
204
+ return Promise.reject(error)
205
+ }
206
+ )
@@ -0,0 +1,138 @@
1
+ import axios, {
2
+ AxiosError,
3
+ AxiosInstance,
4
+ InternalAxiosRequestConfig,
5
+ AxiosResponse,
6
+ AxiosRequestConfig
7
+ } from 'axios'
8
+ import { useAuthStore } from '@/store/auth.store'
9
+ import { authService } from './auth.service'
10
+ import { handleApiError } from '@/utils/toast-helpers'
11
+ import { API_URLS } from '@/config/urls'
12
+ import { RetryableRequest } from '@/interfaces/services.interfaces'
13
+
14
+ // API constants
15
+ const API_BASE_URL = API_URLS.BASE_URL
16
+ const API_TIMEOUT = 30000 // 30 seconds
17
+
18
+ /**
19
+ * Axios instance creation with default config
20
+ */
21
+ const axiosInstance: AxiosInstance = axios.create({
22
+ baseURL: API_BASE_URL,
23
+ timeout: API_TIMEOUT,
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ Accept: 'application/json'
27
+ }
28
+ })
29
+
30
+ /**
31
+ * Helper function for token refresh
32
+ */
33
+ const refreshAuthToken = async () => {
34
+ const { refreshToken } = useAuthStore.getState()
35
+
36
+ if (!refreshToken) {
37
+ throw new Error('No refresh token available')
38
+ }
39
+
40
+ const response = await authService.refreshToken(refreshToken)
41
+
42
+ useAuthStore
43
+ .getState()
44
+ .setTokens(response.access_token, response.refresh_token)
45
+
46
+ return response.access_token
47
+ }
48
+
49
+ /**
50
+ * Request interceptor
51
+ * - Add authorization token
52
+ * - Format request data
53
+ */
54
+ axiosInstance.interceptors.request.use(
55
+ (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
56
+ // Get token from local storage or auth store
57
+ const token =
58
+ sessionStorage.getItem('auth_token') ||
59
+ useAuthStore.getState().accessToken
60
+
61
+ // Add token to request headers if available
62
+ if (token && config.headers) {
63
+ config.headers.Authorization = `Bearer ${token}`
64
+ }
65
+
66
+ // Ensure Content-Type and Accept headers are set
67
+ if (config.headers) {
68
+ // Don't set Content-Type for FormData
69
+ if (!config.headers['Content-Type'] && !(config.data instanceof FormData)) {
70
+ config.headers['Content-Type'] = 'application/json'
71
+ }
72
+
73
+ if (!config.headers['Accept']) {
74
+ config.headers['Accept'] = 'application/json'
75
+ }
76
+ }
77
+
78
+ return config
79
+ },
80
+ (error: AxiosError): Promise<AxiosError> => {
81
+ // handleApiError(error, 'Failed to send request')
82
+ return Promise.reject(error)
83
+ }
84
+ )
85
+
86
+ /**
87
+ * Response interceptor
88
+ * - Handle response data formatting
89
+ * - Handle common errors (401, 403, etc.)
90
+ */
91
+ axiosInstance.interceptors.response.use(
92
+ (response: AxiosResponse): AxiosResponse => {
93
+ return response
94
+ },
95
+ async (error: AxiosError<unknown>): Promise<unknown> => {
96
+ const originalRequest = error.config as RetryableRequest
97
+ const { response } = error
98
+
99
+ // Check if the error is due to an expired token
100
+ if (
101
+ response?.status === 401 &&
102
+ originalRequest &&
103
+ !originalRequest._retry
104
+ ) {
105
+ originalRequest._retry = true
106
+
107
+ try {
108
+ // Try to refresh the token
109
+ const newToken = await refreshAuthToken()
110
+
111
+ // Update the Authorization header with the new token
112
+ if (originalRequest.headers) {
113
+ originalRequest.headers.Authorization = `Bearer ${newToken}`
114
+ }
115
+
116
+ // Retry the original request with the new token
117
+ return axiosInstance(originalRequest as AxiosRequestConfig)
118
+ } catch (refreshError) {
119
+ // Logout the user on refresh token failure
120
+ useAuthStore.getState().logout()
121
+
122
+ // Show toast notification
123
+ handleApiError(refreshError, 'Session expired. Please login again.')
124
+
125
+ // Redirect to login page
126
+ window.location.href = '/login'
127
+ return Promise.reject(new Error('Session expired. Please login again.'))
128
+ }
129
+ }
130
+
131
+ // Handle all API errors with toast notifications
132
+ // handleApiError(error)
133
+
134
+ return Promise.reject(error)
135
+ }
136
+ )
137
+
138
+ export default axiosInstance