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,253 @@
1
+ import { XCircle } from 'lucide-react';
2
+ import logo from "../../assets/brand/saafe-logo.svg";
3
+ import { motion } from "framer-motion";
4
+ import { useRedirectStore } from "@/store/redirect.store";
5
+ import { useState, useEffect } from "react";
6
+ import { feedbackService } from "@/services/api";
7
+ import { useLocation } from 'react-router-dom';
8
+ import { useFipStore } from '@/store/fip.store';
9
+
10
+ interface ConsentResultItem {
11
+ reasonForRejection: string;
12
+ ConsentStatus?: string;
13
+ }
14
+
15
+ const Rejected = () => {
16
+ const [selectedFeedback, setSelectedFeedback] = useState<string | null>(null);
17
+ const [customFeedback, setCustomFeedback] = useState<string>("");
18
+ const [isSubmitting, setIsSubmitting] = useState(false);
19
+ const [isSubmitted, setIsSubmitted] = useState(false);
20
+ const [countdown, setCountdown] = useState(9);
21
+ const { decodedInfo } = useRedirectStore();
22
+ const { selectedAccountToLink } = useFipStore()
23
+ const location = useLocation();
24
+ const consentResult = location.state?.consentResult as ConsentResultItem[] | undefined;
25
+ const consentHandle = decodedInfo?.srcref || '';
26
+
27
+ // Send failed postMessage after 8 seconds of countdown (when countdown reaches 1)
28
+ useEffect(() => {
29
+ if (countdown === 1) {
30
+ console.log("Sending message to parent");
31
+ const fipCodes = consentResult?.map((item: ConsentResultItem) =>
32
+ item.reasonForRejection.match(/\[(.*?)\]/)?.[1]
33
+ );
34
+ const fipNames = selectedAccountToLink?.filter(item =>
35
+ fipCodes?.join(", ").split(", ").includes(item.fipHandle)
36
+ )?.map(item => item.fipName);
37
+ const uniqueFipNames = [...new Set(fipNames)];
38
+
39
+ window.parent.postMessage(
40
+ {
41
+ type: 'AA',
42
+ status: 'rejected',
43
+ flowCompleted: true,
44
+ consentHandle: consentHandle,
45
+ rejectionReason: consentResult,
46
+ rejectedByFips: uniqueFipNames,
47
+ userFeedback: selectedFeedback,
48
+ customFeedback: selectedFeedback === "custom" ? customFeedback : null,
49
+ redirectUrl: decodedInfo?.redirect,
50
+ timestamp: new Date().toISOString()
51
+ },
52
+ '*'
53
+ );
54
+ }
55
+ }, [countdown, consentHandle, consentResult, selectedAccountToLink, selectedFeedback, customFeedback, decodedInfo?.redirect]);
56
+
57
+ // Handle countdown and redirection
58
+ useEffect(() => {
59
+ if (countdown <= 0 && decodedInfo?.redirect) {
60
+ // Redirect to the URL when countdown reaches zero
61
+ window.location.href = decodedInfo.redirect;
62
+ return;
63
+ }
64
+
65
+ const timer = setTimeout(() => {
66
+ setCountdown((prev) => prev - 1);
67
+ }, 1000);
68
+
69
+ localStorage.clear();
70
+ sessionStorage.clear();
71
+ console.log("Session and localStorage cleared");
72
+
73
+ return () => clearTimeout(timer);
74
+ }, [countdown, decodedInfo?.redirect]);
75
+
76
+ const feedbackOptions = [
77
+ { id: "unclear-details", text: "Unclear details 🤔" },
78
+ { id: "confusing-flow", text: "Confusing flow 😕" },
79
+ { id: "linking-issues", text: "Linking Issues 🔗" },
80
+ ];
81
+
82
+ const handleFeedbackSelect = async (feedback: string) => {
83
+ // Don't allow selection if already submitted or submitting
84
+ if (isSubmitted || isSubmitting) return;
85
+
86
+ setSelectedFeedback(feedback);
87
+
88
+ // For custom feedback, don't submit immediately
89
+ if (feedback === "custom") return;
90
+
91
+ await submitFeedback(feedback);
92
+ };
93
+
94
+ const handleCustomFeedbackSubmit = async () => {
95
+ if (!customFeedback.trim() || isSubmitting || isSubmitted) return;
96
+
97
+ await submitFeedback(customFeedback);
98
+ };
99
+
100
+ const submitFeedback = async (feedback: string) => {
101
+ try {
102
+ setIsSubmitting(true);
103
+
104
+ await feedbackService.submitFeedback({
105
+ feedback,
106
+ consentHandle
107
+ });
108
+
109
+ setIsSubmitted(true);
110
+ } catch {
111
+ // console.error('Error submitting feedback:', error);
112
+ } finally {
113
+ setIsSubmitting(false);
114
+ }
115
+ };
116
+
117
+ return (
118
+ <div className="flex flex-col min-h-screen bg-background">
119
+ {/* Timer Section - Top */}
120
+ {decodedInfo?.redirect && (
121
+ <div className="p-4 text-center">
122
+ <p className="text-muted-secondary">
123
+ Redirecting in <span className="font-semibold">00:{countdown < 10 ? `0${countdown}` : countdown}</span>
124
+ </p>
125
+ </div>
126
+ )}
127
+
128
+ {/* Main Content - Center */}
129
+ <div className="flex-1 flex items-center justify-center p-6">
130
+ <motion.div
131
+ initial={{ y: 20, opacity: 0 }}
132
+ animate={{ y: 0, opacity: 1 }}
133
+ transition={{ delay: 0.2, duration: 0.5 }}
134
+ className="flex flex-col items-center w-full max-w-md mx-auto"
135
+ >
136
+ <div className="rounded-full bg-red-100 p-6 mb-8">
137
+ <XCircle className="h-16 w-16 text-red-600" />
138
+ </div>
139
+
140
+ <h1 className="md:text-3xl text-lg font-semibold mb-4 dark:text-gray-300">Consent Rejected</h1>
141
+
142
+ {consentResult && consentResult.length > 0 && <p className="text-center mb-2 text-muted-secondary md:text-md text-xs">
143
+ Your consent has been rejected for the following reason:
144
+ <br />
145
+ <div className="mt-1">
146
+ {
147
+ (() => {
148
+ const fipCodes = consentResult?.map(item =>
149
+ item.reasonForRejection.match(/\[(.*?)\]/)?.[1]
150
+ );
151
+ const fipNames = selectedAccountToLink?.filter(item => fipCodes?.join(", ").split(", ").includes(item.fipHandle))?.map(item => item.fipName)
152
+ // remove the duplicate fipCodes
153
+ const uniqueFipCodes = [...new Set((fipNames))];
154
+ const result = `Consent rejected by FIP: [${uniqueFipCodes?.join(", ")}]`;
155
+ return <div>{result}</div>;
156
+ })()
157
+ }
158
+ </div>
159
+ </p>}
160
+ <p className="text-center mb-8 text-muted-secondary md:text-md text-xs">
161
+ View this consent detail anytime later using saafe app
162
+ </p>
163
+
164
+ {/* Feedback Section */}
165
+ <motion.div
166
+ initial={{ opacity: 0, y: 20 }}
167
+ animate={{ opacity: 1, y: 0 }}
168
+ transition={{ delay: 0.5 }}
169
+ className="w-full max-w-md mt-8"
170
+ >
171
+ <h2 className="md:text-lg text-sm font-normal text-muted-secondary text-center mb-4">
172
+ Mind sharing what held you back?
173
+ </h2>
174
+
175
+ <div className="flex flex-wrap gap-3 justify-center mb-3">
176
+ {feedbackOptions.map((option) => (
177
+ <button
178
+ key={option.id}
179
+ onClick={() => handleFeedbackSelect(option.text)}
180
+ className={`
181
+ px-4 py-2 rounded-full border transition-all md:text-md text-xs dark:text-gray-400
182
+ ${selectedFeedback === option.text
183
+ ? "border-blue-500 bg-blue-50 dark:bg-blue-900 text-blue-700 dark:text-white"
184
+ : "border-gray-300 hover:border-gray-400"}
185
+ ${isSubmitting || isSubmitted ? "opacity-70 cursor-not-allowed" : ""}
186
+ `}
187
+ disabled={isSubmitting || isSubmitted}
188
+ >
189
+ {option.text}
190
+ </button>
191
+ ))}
192
+
193
+ <button
194
+ onClick={() => handleFeedbackSelect("custom")}
195
+ className={`
196
+ px-4 py-2 rounded-full border transition-all md:text-md text-xs dark:text-gray-400
197
+ ${selectedFeedback === "custom"
198
+ ? "border-blue-500 bg-blue-50 text-blue-700"
199
+ : "border-gray-300 hover:border-gray-400"}
200
+ ${isSubmitting || isSubmitted ? "opacity-70 cursor-not-allowed" : ""}
201
+ `}
202
+ disabled={isSubmitting || isSubmitted}
203
+ >
204
+ Type here
205
+ </button>
206
+ </div>
207
+
208
+ {selectedFeedback === "custom" && (
209
+ <div className="mt-3 flex md:flex-row flex-col md:gap-0 gap-1">
210
+ <input
211
+ type="text"
212
+ value={customFeedback}
213
+ onChange={(e) => setCustomFeedback(e.target.value)}
214
+ placeholder="Tell us what went wrong..."
215
+ className="flex-1 px-4 py-2 border border-gray-300 md:rounded-l-lg rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-gray-400"
216
+ disabled={isSubmitting || isSubmitted}
217
+ />
218
+ <button
219
+ onClick={handleCustomFeedbackSubmit}
220
+ className={`
221
+ px-4 py-2 bg-blue-600 text-white md:rounded-r-lg rounded-lg
222
+ ${(isSubmitting || isSubmitted || !customFeedback.trim())
223
+ ? "opacity-70 cursor-not-allowed"
224
+ : "hover:bg-blue-700"}
225
+ `}
226
+ disabled={isSubmitting || isSubmitted || !customFeedback.trim()}
227
+ >
228
+ Submit
229
+ </button>
230
+ </div>
231
+ )}
232
+
233
+ {isSubmitted && (
234
+ <p className="text-green-600 text-center mt-3">
235
+ Thank you for your feedback!
236
+ </p>
237
+ )}
238
+ </motion.div>
239
+ </motion.div>
240
+ </div>
241
+
242
+ {/* Footer Section - Bottom */}
243
+ <div className="p-4 text-center">
244
+ <div className="text-sm flex items-center justify-center gap-2 opacity-70">
245
+ <p>Powered by</p>
246
+ <img src={logo} alt="Saafe Logo" className="w-16 h-auto" />
247
+ </div>
248
+ </div>
249
+ </div>
250
+ );
251
+ };
252
+
253
+ export default Rejected;
@@ -0,0 +1,220 @@
1
+ import { MobileBackground } from "@/components/mobile-background";
2
+ import { motion } from "framer-motion";
3
+ import logo from "../../assets/brand/saafe-color-white-logo.svg";
4
+ import checkIcon from "../../assets/icons/check-icon.svg";
5
+ import { useState, useEffect } from "react";
6
+ import { useRedirectStore } from "@/store/redirect.store";
7
+ import { feedbackService } from "@/services/api";
8
+ import { StarFilledIcon } from "@radix-ui/react-icons";
9
+ import { useNavigationBlock } from "@/store/NavigationBlockContext";
10
+ import { MobileAppDownload } from "@/components/mobileAppDownload";
11
+
12
+ const Success = () => {
13
+ const [rating, setRating] = useState<number | null>(null);
14
+ const [hoveredRating, setHoveredRating] = useState<number | null>(null);
15
+ const [isSubmitted, setIsSubmitted] = useState(false);
16
+ const [isSubmitting, setIsSubmitting] = useState(false);
17
+ const [countdown, setCountdown] = useState(10);
18
+ const { decodedInfo } = useRedirectStore();
19
+ const { allowNextNavigation } = useNavigationBlock();
20
+ const consentHandle = decodedInfo?.srcref || '';
21
+
22
+ // Send success postMessage after 8 seconds of countdown (when countdown reaches 1)
23
+ useEffect(() => {
24
+ if (countdown === 1) {
25
+ console.log("Sending message to parent -->> From AA");
26
+ window.parent.postMessage(
27
+ {
28
+ type: 'AA',
29
+ status: 'approved',
30
+ flowCompleted: true,
31
+ consentHandle: consentHandle,
32
+ userRating: rating,
33
+ redirectUrl: decodedInfo?.redirect,
34
+ timestamp: new Date().toISOString()
35
+ },
36
+ '*'
37
+ );
38
+ }
39
+ }, [countdown, consentHandle, rating, decodedInfo?.redirect]);
40
+
41
+ // Prevent back navigation
42
+ useEffect(() => {
43
+ window.history.pushState(null, '', window.location.href);
44
+ window.onpopstate = () => {
45
+ window.history.pushState(null, '', window.location.href);
46
+ };
47
+
48
+ return () => {
49
+ window.onpopstate = null;
50
+ };
51
+ }, []);
52
+
53
+ // Handle countdown and redirection
54
+ useEffect(() => {
55
+ allowNextNavigation();
56
+ if (countdown <= 0 && decodedInfo?.redirect) {
57
+ allowNextNavigation();
58
+ // Redirect to the URL when countdown reaches zero
59
+ window.location.href = decodedInfo.redirect;
60
+ return;
61
+ }
62
+
63
+ const timer = setTimeout(() => {
64
+ setCountdown((prev) => prev - 1);
65
+ }, 1000);
66
+
67
+ localStorage.clear();
68
+ sessionStorage.clear();
69
+ console.log("Session and localStorage cleared");
70
+
71
+ return () => clearTimeout(timer);
72
+ }, [countdown, decodedInfo?.redirect]);
73
+
74
+ const handleRatingClick = async (selectedRating: number) => {
75
+ setRating(selectedRating);
76
+
77
+ // Don't submit again if user clicks multiple times
78
+ if (isSubmitted || isSubmitting) return;
79
+
80
+ try {
81
+ setIsSubmitting(true);
82
+
83
+ await feedbackService.submitRating({
84
+ ratings: selectedRating,
85
+ consentHandle: consentHandle
86
+ });
87
+
88
+ setIsSubmitted(true);
89
+ } catch {
90
+ // Silently catch errors
91
+ } finally {
92
+ setIsSubmitting(false);
93
+ }
94
+ };
95
+
96
+ return (
97
+ <div className="flex flex-col min-h-screen h-full">
98
+ <MobileBackground
99
+ blobConfig={{
100
+ count: 8,
101
+ color: "rgba(255, 255, 255, 0.4)",
102
+ minSize: 160,
103
+ maxSize: 220,
104
+ speed: 4,
105
+ }}
106
+ >
107
+ <div className="p-6 flex flex-col min-h-screen">
108
+ {/* Redirecting message */}
109
+ {decodedInfo?.redirect && (
110
+ <div className="absolute top-4 sm:top-6 left-0 right-0 text-center">
111
+ <p className="text-white/80 text-sm">
112
+ Redirecting in <span className="font-semibold">00:{countdown < 10 ? `0${countdown}` : countdown}</span>
113
+ </p>
114
+ </div>
115
+ )}
116
+
117
+ {/* Main content - centered vertically and horizontally */}
118
+ <div className="flex-1 flex flex-col justify-center items-center pt-10 sm:pt-16 pb-10 sm:pb-20">
119
+ <motion.div
120
+ initial={{ y: 20, opacity: 0 }}
121
+ animate={{ y: 0, opacity: 1 }}
122
+ transition={{ delay: 0.2, duration: 0.5 }}
123
+ className="text-white flex flex-col items-center w-full max-w-md mx-auto px-4"
124
+ >
125
+ <div className="rounded-full mb-10 sm:mb-16">
126
+ <motion.img
127
+ src={checkIcon}
128
+ alt="Check Icon"
129
+ className="w-32 h-32 sm:w-40 sm:h-40 md:w-48 md:h-48"
130
+ initial={{ scale: 0, rotate: -10 }}
131
+ animate={{ scale: 1, rotate: 0 }}
132
+ transition={{
133
+ type: "spring",
134
+ damping: 8,
135
+ stiffness: 100,
136
+ delay: 0.9,
137
+ duration: 4
138
+ }}
139
+ />
140
+ </div>
141
+
142
+ <motion.h1
143
+ className="text-xl md:text-3xl font-semibold mb-5 sm:mb-6 text-center"
144
+ initial={{ opacity: 0 }}
145
+ animate={{ opacity: 1 }}
146
+ transition={{ delay: 1.5 }}
147
+ >
148
+ Approved Successfully
149
+ </motion.h1>
150
+
151
+ <motion.p
152
+ className="text-white/80 text-center mb-10 sm:mb-14 text-sm sm:text-base md:text-lg max-w-sm"
153
+ initial={{ opacity: 0 }}
154
+ animate={{ opacity: 1 }}
155
+ transition={{ delay: 1.5 }}
156
+ >
157
+ You can pause/revoke this consent anytime using Saafe app
158
+ </motion.p>
159
+
160
+ {/* Mobile App Download - animated to appear last */}
161
+ <motion.div
162
+ className="w-full max-w-xs mx-auto"
163
+ initial={{ opacity: 0, y: 20 }}
164
+ animate={{ opacity: 1, y: 0 }}
165
+ transition={{ delay: 2.5, duration: 0.7 }}
166
+ >
167
+ <MobileAppDownload />
168
+ </motion.div>
169
+
170
+ {/* Rating Component */}
171
+ <motion.div
172
+ className="mt-12 sm:mt-16 w-full max-w-xs mx-auto px-4 text-center"
173
+ initial={{ opacity: 0, y: 20 }}
174
+ animate={{ opacity: 1, y: 0 }}
175
+ transition={{ delay: 2.0 }}
176
+ >
177
+ <h3 className="text-white text-sm sm:text-base md:text-lg mb-4 sm:mb-6">Rate your experience</h3>
178
+ <div className="flex justify-center gap-3 sm:gap-5">
179
+ {[1, 2, 3, 4, 5].map((star) => (
180
+ <button
181
+ key={star}
182
+ onClick={() => handleRatingClick(star)}
183
+ onMouseEnter={() => setHoveredRating(star)}
184
+ onMouseLeave={() => setHoveredRating(null)}
185
+ className="focus:outline-none transition-transform hover:scale-110 p-1 sm:p-2"
186
+ disabled={isSubmitting}
187
+ >
188
+ <StarFilledIcon
189
+ className={`
190
+ transition-colors duration-200 w-6 h-6 sm:w-8 sm:h-8 md:w-9 md:h-9
191
+ ${(hoveredRating !== null ? star <= hoveredRating : star <= (rating || 0))
192
+ ? "fill-white text-white"
193
+ : "text-white/40 fill-transparent"}
194
+ ${isSubmitting ? "opacity-70" : ""}
195
+ `}
196
+ />
197
+ </button>
198
+ ))}
199
+ </div>
200
+ {isSubmitted && (
201
+ <p className="mt-3 text-white/80 text-xs sm:text-sm">Thank you for your feedback!</p>
202
+ )}
203
+ </motion.div>
204
+ </motion.div>
205
+ </div>
206
+
207
+ {/* Footer */}
208
+ <div className="mt-auto pb-6 sm:pb-8 text-center">
209
+ <div className="text-white text-xs sm:text-sm flex items-center justify-center gap-1 sm:gap-2 opacity-70">
210
+ <p>Powered by</p>
211
+ <img src={logo} alt="Saafe Logo" className="w-14 sm:w-16 h-auto" />
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </MobileBackground>
216
+ </div>
217
+ );
218
+ };
219
+
220
+ export default Success;
@@ -0,0 +1,24 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
3
+ import { ReactNode, useState } from 'react';
4
+
5
+ interface QueryProviderProps {
6
+ children: ReactNode;
7
+ }
8
+
9
+ export function QueryProvider({ children }: QueryProviderProps) {
10
+ const [queryClient] = useState(() => new QueryClient({
11
+ defaultOptions: {
12
+ queries: {
13
+ refetchOnWindowFocus: false,
14
+ },
15
+ },
16
+ }));
17
+
18
+ return (
19
+ <QueryClientProvider client={queryClient}>
20
+ {children}
21
+ <ReactQueryDevtools initialIsOpen={false} />
22
+ </QueryClientProvider>
23
+ );
24
+ }
@@ -0,0 +1,26 @@
1
+ import { Toaster } from "sonner";
2
+
3
+ interface ToastProviderProps {
4
+ children: React.ReactNode;
5
+ }
6
+
7
+ export function ToastProvider({ children }: ToastProviderProps) {
8
+ return (
9
+ <>
10
+ <Toaster
11
+ position="bottom-right"
12
+ closeButton
13
+ richColors
14
+ toastOptions={{
15
+ classNames: {
16
+ toast: 'relative',
17
+ closeButton: 'ml-[93%] mt-[10%]',
18
+ description: 'text-[16px]',
19
+ title: 'text-lg'
20
+ },
21
+ }}
22
+ />
23
+ {children}
24
+ </>
25
+ );
26
+ }