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,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
|
+
}
|