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.
- package/.github/workflows/build-and-deploy.yml +41 -0
- package/.gitlab-ci.yml +108 -0
- package/.releaserc.json +18 -0
- package/.storybook/main.ts +28 -0
- package/.storybook/preview.ts +16 -0
- package/.storybook/vitest.setup.ts +9 -0
- package/.vite/deps/@radix-ui_react-avatar.js +230 -0
- package/.vite/deps/@radix-ui_react-avatar.js.map +7 -0
- package/.vite/deps/@radix-ui_react-slot.js +12 -0
- package/.vite/deps/@radix-ui_react-slot.js.map +7 -0
- package/.vite/deps/_metadata.json +79 -0
- package/.vite/deps/chunk-5VGQBUCU.js +597 -0
- package/.vite/deps/chunk-5VGQBUCU.js.map +7 -0
- package/.vite/deps/chunk-DC5AMYBS.js +38 -0
- package/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
- package/.vite/deps/chunk-HUIEPYH7.js +11265 -0
- package/.vite/deps/chunk-HUIEPYH7.js.map +7 -0
- package/.vite/deps/chunk-TKHB4QMX.js +281 -0
- package/.vite/deps/chunk-TKHB4QMX.js.map +7 -0
- package/.vite/deps/chunk-YLDSBLSF.js +1139 -0
- package/.vite/deps/chunk-YLDSBLSF.js.map +7 -0
- package/.vite/deps/class-variance-authority.js +63 -0
- package/.vite/deps/class-variance-authority.js.map +7 -0
- package/.vite/deps/lucide-react.js +36984 -0
- package/.vite/deps/lucide-react.js.map +7 -0
- package/.vite/deps/package.json +3 -0
- package/.vite/deps/react-dom_client.js +17917 -0
- package/.vite/deps/react-dom_client.js.map +7 -0
- package/.vite/deps/react-router-dom.js +452 -0
- package/.vite/deps/react-router-dom.js.map +7 -0
- package/.vite/deps/react-router.js +234 -0
- package/.vite/deps/react-router.js.map +7 -0
- package/.vite/deps/react.js +5 -0
- package/.vite/deps/react.js.map +7 -0
- package/.vite/deps/react_jsx-dev-runtime.js +470 -0
- package/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
- package/CHANGELOG.md +420 -0
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/RELEASE_CHEATSHEET.md +93 -0
- package/RELEASE_NOTES.md +120 -0
- package/components.json +21 -0
- package/docs/DEPLOYMENT_WORKFLOW.md +262 -0
- package/docs/RELEASE_GUIDE.md +591 -0
- package/docs/architecture.md +432 -0
- package/docs/components.md +199 -0
- package/docs/index.md +69 -0
- package/docs/local-release-workflow.md +234 -0
- package/docs/routes.md +118 -0
- package/docs/sdk-integration.md +325 -0
- package/docs/semantic-release.md +124 -0
- package/docs/user-flow.md +206 -0
- package/eslint.config.js +28 -0
- package/index.html +19 -0
- package/install.sh +198 -0
- package/package.json +115 -0
- package/public/images/bank-logo.png +0 -0
- package/public/saafe-icon.svg +9 -0
- package/src/App.tsx +171 -0
- package/src/__tests__/url-parameters.test.ts +82 -0
- package/src/assets/brand/applestore.svg +13 -0
- package/src/assets/brand/playstore.svg +23 -0
- package/src/assets/brand/saafe-color-white-logo.svg +14 -0
- package/src/assets/brand/saafe-icon.svg +9 -0
- package/src/assets/brand/saafe-logo.svg +18 -0
- package/src/assets/icons/check-icon-dark.svg +27 -0
- package/src/assets/icons/check-icon.svg +23 -0
- package/src/components/ErrorBoundary.tsx +132 -0
- package/src/components/alert/alert.tsx +27 -0
- package/src/components/auth/AuthGuard.tsx +76 -0
- package/src/components/cards/BankCard.stories.tsx +69 -0
- package/src/components/cards/BankCard.tsx +227 -0
- package/src/components/cards/OuterCard.tsx +109 -0
- package/src/components/cards/WrapperCard.tsx +64 -0
- package/src/components/documents/PrivacyContent.tsx +1 -0
- package/src/components/dummyFooter.tsx +29 -0
- package/src/components/icons/github.tsx +12 -0
- package/src/components/language/LanguageSwitcher.tsx +44 -0
- package/src/components/layouts/FrostedLayout.stories.tsx +42 -0
- package/src/components/layouts/FrostedLayout.tsx +333 -0
- package/src/components/layouts/MobileLayout.tsx +403 -0
- package/src/components/mobile-background.tsx +136 -0
- package/src/components/mobileAppDownload.tsx +30 -0
- package/src/components/modal/ModalComp.tsx +27 -0
- package/src/components/mode-toggle.tsx +36 -0
- package/src/components/page-header.tsx +50 -0
- package/src/components/session/SessionTimeoutScreen.tsx +134 -0
- package/src/components/session/SessionTimer.tsx +173 -0
- package/src/components/step-navigation.tsx +87 -0
- package/src/components/title/AppBar.stories.tsx +50 -0
- package/src/components/title/AppBar.tsx +150 -0
- package/src/components/title/SectionTitle.tsx +31 -0
- package/src/components/ui/AnimatedButton.module.css +13 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/animatedButton.tsx +111 -0
- package/src/components/ui/avatar.tsx +51 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/bottom-sheet.tsx +122 -0
- package/src/components/ui/button.tsx +59 -0
- package/src/components/ui/calendar.tsx +86 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.stories.tsx +49 -0
- package/src/components/ui/checkbox.tsx +67 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/dialog.tsx +134 -0
- package/src/components/ui/document-link.tsx +26 -0
- package/src/components/ui/dot-stepper.tsx +57 -0
- package/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/components/ui/form.tsx +165 -0
- package/src/components/ui/frosted-panel.stories.tsx +86 -0
- package/src/components/ui/frosted-panel.tsx +276 -0
- package/src/components/ui/input.tsx +39 -0
- package/src/components/ui/label.stories.tsx +67 -0
- package/src/components/ui/label.tsx +23 -0
- package/src/components/ui/mobile-footer.tsx +54 -0
- package/src/components/ui/modal.tsx +90 -0
- package/src/components/ui/otp-input.stories.tsx +62 -0
- package/src/components/ui/otp-input.tsx +221 -0
- package/src/components/ui/platform-specific-behavior.tsx +28 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/progress.tsx +103 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +56 -0
- package/src/components/ui/sdk-params-docs.tsx +53 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +137 -0
- package/src/components/ui/sidebar.tsx +724 -0
- package/src/components/ui/skeleton.stories.tsx +50 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +23 -0
- package/src/components/ui/step.stories.tsx +132 -0
- package/src/components/ui/step.tsx +234 -0
- package/src/components/ui/stepper-progress.tsx +136 -0
- package/src/components/ui/stepper.tsx +259 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/components/ui/url-decode-loader.tsx +36 -0
- package/src/components/ui/version-display.tsx +104 -0
- package/src/components/ui/web-footer.tsx +36 -0
- package/src/config/environments.ts +99 -0
- package/src/config/urls.ts +53 -0
- package/src/const/fiTypeCategoryMap.ts +19 -0
- package/src/contexts/LanguageContext.tsx +41 -0
- package/src/contexts/RTLContext.tsx +42 -0
- package/src/contexts/ThemeContext.tsx +93 -0
- package/src/hooks/use-account-discovery.ts +205 -0
- package/src/hooks/use-auth-query.ts +141 -0
- package/src/hooks/use-fip-query.ts +72 -0
- package/src/hooks/use-media-query.ts +32 -0
- package/src/hooks/use-mobile.ts +24 -0
- package/src/hooks/use-page-title.tsx +48 -0
- package/src/hooks/use-platform.ts +52 -0
- package/src/hooks/use-trusted-count.ts +21 -0
- package/src/hooks/use-url-decode.ts +90 -0
- package/src/hooks/useStep.ts +170 -0
- package/src/index.css +154 -0
- package/src/interfaces/app.interfaces.ts +39 -0
- package/src/interfaces/services.interfaces.ts +65 -0
- package/src/lib/i18n.ts +68 -0
- package/src/lib/utils.ts +6 -0
- package/src/locales/en/common.json +167 -0
- package/src/locales/hi/common.json +137 -0
- package/src/locales/kn/common.json +137 -0
- package/src/locales/ml/common.json +137 -0
- package/src/locales/ta/common.json +137 -0
- package/src/locales/te/common.json +137 -0
- package/src/locales/ur/common.json +138 -0
- package/src/main.tsx +46 -0
- package/src/pages/Login.tsx +363 -0
- package/src/pages/accounts/AccountsToProceed.tsx +396 -0
- package/src/pages/accounts/Discover.tsx +76 -0
- package/src/pages/accounts/DiscoverAccount.tsx +751 -0
- package/src/pages/accounts/LinkSelectedAccounts.tsx +638 -0
- package/src/pages/accounts/OldUser.tsx +329 -0
- package/src/pages/accounts/link-accounts.tsx +913 -0
- package/src/pages/consent/ReviewConsent.tsx +836 -0
- package/src/pages/consent/rejected.tsx +253 -0
- package/src/pages/consent/success.tsx +220 -0
- package/src/providers/query-provider.tsx +24 -0
- package/src/providers/toast-provider.tsx +26 -0
- package/src/services/api/account.service.ts +296 -0
- package/src/services/api/auth.service.ts +206 -0
- package/src/services/api/axios.ts +138 -0
- package/src/services/api/consent.service.ts +142 -0
- package/src/services/api/decode.service.ts +53 -0
- package/src/services/api/feedback.service.ts +34 -0
- package/src/services/api/fip.service.ts +187 -0
- package/src/services/api/index.ts +9 -0
- package/src/services/api/public.service.ts +18 -0
- package/src/services/api.ts +2 -0
- package/src/services/postMessage.service.ts +179 -0
- package/src/store/NavigationBlockContext.tsx +34 -0
- package/src/store/auth.store.ts +79 -0
- package/src/store/fip.store.ts +396 -0
- package/src/store/mandatoryConsent.store.ts +24 -0
- package/src/store/redirect.store.ts +73 -0
- package/src/store/step.store.ts +124 -0
- package/src/stories/Button.stories.ts +53 -0
- package/src/stories/Button.tsx +37 -0
- package/src/stories/Configure.mdx +364 -0
- package/src/stories/Header.stories.ts +33 -0
- package/src/stories/Header.tsx +56 -0
- package/src/stories/Page.stories.ts +32 -0
- package/src/stories/Page.tsx +73 -0
- package/src/stories/button.css +30 -0
- package/src/stories/header.css +32 -0
- package/src/stories/page.css +68 -0
- package/src/styles/rtl-utils.css +90 -0
- package/src/styles/rtl.css +105 -0
- package/src/utils/api-error.ts +26 -0
- package/src/utils/cn.ts +10 -0
- package/src/utils/error-callback.ts +116 -0
- package/src/utils/formatAccountNumber.ts +9 -0
- package/src/utils/handleIdentifiers.ts +90 -0
- package/src/utils/posthog.ts +67 -0
- package/src/utils/toast-helpers.ts +61 -0
- package/src/vite-env.d.ts +1 -0
- package/stage-aa-2506251021.zip +0 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +45 -0
- package/vitest.shims.d.ts +1 -0
- 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
|