forked from Creditra/Creditra-Frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathKycContext.tsx
More file actions
191 lines (172 loc) · 6.21 KB
/
Copy pathKycContext.tsx
File metadata and controls
191 lines (172 loc) · 6.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
/**
* KycContext — app-wide KYC progress state.
*
* Owns the ordered step list and exposes helpers so the drawer and any
* future KYC page can read/update status without prop-drilling.
*
* Persistence strategy:
* - Hydrated from `localStorage` on mount (key: `creditra_kyc`).
* - Written back on every status change.
* - Capped at version 1; migrations can be added to `migrateState`.
*
* Security note: localStorage is client-only and unverified. The source
* of truth for KYC status is always the backend. This context is for
* UI progress only; never gate security decisions on it alone.
*/
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import type {
KycOverallStatus,
KycPersistedState,
KycStep,
KycStepId,
KycStepStatus,
} from '../types/kyc';
// ─── Defaults ─────────────────────────────────────────────────────────────────
const STORAGE_KEY = 'creditra_kyc';
/** The canonical ordered step list. */
const DEFAULT_STEPS: KycStep[] = [
{
id: 'identity',
label: 'Identity Verification',
description: 'Confirm your legal name and date of birth.',
status: 'not_started',
},
{
id: 'address',
label: 'Address Verification',
description: 'Provide a current residential address.',
status: 'not_started',
},
{
id: 'documents',
label: 'Document Upload',
description: 'Upload a government-issued photo ID.',
status: 'not_started',
},
{
id: 'selfie',
label: 'Selfie Check',
description: 'Take a live selfie matching your ID photo.',
status: 'not_started',
},
{
id: 'review',
label: 'Final Review',
description: 'Our compliance team reviews your submission.',
status: 'not_started',
},
];
// ─── Context shape ────────────────────────────────────────────────────────────
interface KycContextValue {
/** Ordered KYC steps with their current statuses. */
steps: KycStep[];
/** Derived overall status badge shown on the header trigger. */
overallStatus: KycOverallStatus;
/**
* The step the "Resume" button should land on:
* the first step that is `in_progress` or `not_started`.
* `null` when all steps are completed/pending/failed.
*/
resumeStepId: KycStepId | null;
/**
* Number of completed steps. Used for the progress fraction in the drawer.
*/
completedCount: number;
/** Update the status of a single step. */
setStepStatus: (stepId: KycStepId, status: KycStepStatus) => void;
/** Reset all steps back to `not_started`. Intended for testing/dev. */
resetAll: () => void;
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function deriveOverallStatus(steps: KycStep[]): KycOverallStatus {
const statuses = steps.map(s => s.status);
if (statuses.every(s => s === 'not_started')) return 'not_started';
if (statuses.some(s => s === 'failed')) return 'rejected';
if (statuses.every(s => s === 'completed')) return 'approved';
if (statuses.every(s => s === 'completed' || s === 'pending')) return 'under_review';
return 'in_progress';
}
function deriveResumeStepId(steps: KycStep[]): KycStepId | null {
// Prefer the first in_progress step, then the first not_started step.
const inProgress = steps.find(s => s.status === 'in_progress');
if (inProgress) return inProgress.id;
const notStarted = steps.find(s => s.status === 'not_started');
return notStarted?.id ?? null;
}
function loadFromStorage(): KycStep[] {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return DEFAULT_STEPS;
const parsed = JSON.parse(raw) as KycPersistedState;
if (parsed.version !== 1) return DEFAULT_STEPS;
// Merge stored statuses onto the canonical step list so new steps
// added in code appear with their defaults.
const stored = new Map(parsed.steps.map(s => [s.id, s]));
return DEFAULT_STEPS.map(def => {
const s = stored.get(def.id);
return s ? { ...def, status: s.status, updatedAt: s.updatedAt } : def;
});
} catch {
return DEFAULT_STEPS;
}
}
function saveToStorage(steps: KycStep[]): void {
try {
const payload: KycPersistedState = {
version: 1,
steps,
lastUpdated: new Date().toISOString(),
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
} catch {
// Silently ignore storage quota errors.
}
}
// ─── Context ──────────────────────────────────────────────────────────────────
const KycContext = createContext<KycContextValue | null>(null);
export function KycProvider({ children }: { children: React.ReactNode }) {
const [steps, setSteps] = useState<KycStep[]>(() => loadFromStorage());
// Persist on every change.
useEffect(() => {
saveToStorage(steps);
}, [steps]);
const setStepStatus = useCallback(
(stepId: KycStepId, status: KycStepStatus) => {
setSteps(prev =>
prev.map(s =>
s.id === stepId
? { ...s, status, updatedAt: new Date().toISOString() }
: s
)
);
},
[]
);
const resetAll = useCallback(() => {
setSteps(DEFAULT_STEPS.map(s => ({ ...s, updatedAt: undefined })));
}, []);
const overallStatus = deriveOverallStatus(steps);
const resumeStepId = deriveResumeStepId(steps);
const completedCount = steps.filter(s => s.status === 'completed').length;
return (
<KycContext.Provider
value={{ steps, overallStatus, resumeStepId, completedCount, setStepStatus, resetAll }}
>
{children}
</KycContext.Provider>
);
}
/**
* Read the KYC context. Throws outside KycProvider to surface misuse early.
*/
export function useKyc() {
const ctx = useContext(KycContext);
if (!ctx) throw new Error('useKyc must be used within a KycProvider');
return ctx;
}