central-auth-js-sdk 1.0.10

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/signup.js ADDED
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Central Auth Sign-Up Flow — mount implementation
3
+ * Vanilla JavaScript; used by index.js and auth.js
4
+ */
5
+
6
+ import { FlowController, STEPS } from './core/flow.js';
7
+ import { StateManager } from './core/state.js';
8
+ import { createApi, dataUrlToFile } from './core/api.js';
9
+ import { resolveApiBaseUrl } from './core/env-config.js';
10
+ import { createLayout } from './ui/layout.js';
11
+ import { createScreen } from './ui/screens/index.js';
12
+ import { applyTheme } from './theme.js';
13
+
14
+ /** Parse URL query for OAuth callback params (login_token, provider) */
15
+ function parseOAuthCallback() {
16
+ if (typeof window === 'undefined' || !window.location) return null;
17
+ const params = new URLSearchParams(window.location.search);
18
+ const loginToken = params.get('login_token');
19
+ const provider = (params.get('provider') || '').toLowerCase();
20
+ if (!loginToken) return null;
21
+ if (provider !== 'google' && provider !== 'microsoft') return { loginToken, provider: 'google' };
22
+ return { loginToken, provider };
23
+ }
24
+
25
+ /** Remove OAuth params from URL without reload */
26
+ function clearOAuthParams() {
27
+ if (typeof window === 'undefined' || !window.history) return;
28
+ const params = new URLSearchParams(window.location.search);
29
+ params.delete('login_token');
30
+ params.delete('provider');
31
+ const clean = params.toString();
32
+ const url = window.location.pathname + (clean ? '?' + clean : '') + window.location.hash;
33
+ window.history.replaceState({}, '', url);
34
+ }
35
+
36
+ /** Map API next_step from POST /signup/start to our flow step */
37
+ function mapNextStep(apiNextStep) {
38
+ if (!apiNextStep) return null;
39
+ const s = String(apiNextStep).toLowerCase().replace(/-/g, '_');
40
+ const map = {
41
+ email_verification: STEPS.EMAIL_VERIFY,
42
+ mobile_verification: STEPS.MOBILE,
43
+ mobile: STEPS.MOBILE,
44
+ mobile_verify: STEPS.MOBILE_VERIFY,
45
+ password: STEPS.PASSWORD,
46
+ password_set: STEPS.PASSWORD,
47
+ face: STEPS.FACE,
48
+ face_registration: STEPS.FACE,
49
+ fr_enrollment: STEPS.FACE,
50
+ complete: STEPS.SUCCESS,
51
+ completed: STEPS.SUCCESS,
52
+ success: STEPS.SUCCESS,
53
+ };
54
+ return map[s] || null;
55
+ }
56
+
57
+ /**
58
+ * Mount the signup flow into a container element
59
+ * @param {string|HTMLElement} container - CSS selector or DOM element
60
+ * @param {Object} options - env, appIdentifier (for social), enableFaceRegistration, onSignupComplete, onContinue
61
+ * @param {Function} [options.onSignupComplete] - Called when signup completes with (payload). Payload includes email, mobile, userId, and any token/fields from the final API response.
62
+ * @param {Function} [options.onContinue] - Called when user clicks Continue on the success screen (e.g. to unmount and redirect).
63
+ * @param {Function} [options.onSwitchToLogin] - Called when user clicks "Sign in" (e.g. unmount signup and start login + FRS).
64
+ * @returns {Object} - SDK instance with unmount, getState, etc.
65
+ */
66
+ export function mount(container, options = {}) {
67
+ const el = typeof container === 'string' ? document.querySelector(container) : container;
68
+ if (!el) {
69
+ console.error('[Central Auth SDK] Container not found:', container);
70
+ return null;
71
+ }
72
+
73
+ const apiBaseUrl = options.apiBaseUrl ?? resolveApiBaseUrl(options);
74
+ const {
75
+ appIdentifier,
76
+ enableFaceRegistration = true,
77
+ theme,
78
+ onSignupComplete = null,
79
+ onContinue = null,
80
+ onSwitchToLogin = null,
81
+ onBack = null,
82
+ } = options;
83
+
84
+ const flow = new FlowController();
85
+ const state = new StateManager();
86
+ const api = createApi(apiBaseUrl);
87
+
88
+ const layout = createLayout(el, () => {});
89
+ const cleanupTheme = applyTheme(layout.root, theme);
90
+ const showError = (msg) => layout.showError(msg);
91
+
92
+ // SDK handles OAuth callback: if URL has login_token, resume social flow
93
+ const oauth = parseOAuthCallback();
94
+ if (oauth) {
95
+ state.set('loginToken', oauth.loginToken);
96
+ state.set('socialProvider', oauth.provider);
97
+ clearOAuthParams();
98
+ flow.setSocialFlow();
99
+ }
100
+
101
+ const isSocial = () => !!state.get('loginToken');
102
+
103
+ function buildSignupCompletePayload(apiResult) {
104
+ const all = state.getAll();
105
+ const payload = {
106
+ email: all.email,
107
+ mobile: all.mobile,
108
+ userId: all.userId,
109
+ };
110
+ if (apiResult && typeof apiResult === 'object' && apiResult !== null) {
111
+ Object.assign(payload, apiResult);
112
+ }
113
+ return payload;
114
+ }
115
+
116
+ async function handleEmailSubmit(data) {
117
+ state.clearError();
118
+ const email = typeof data === 'string' ? data : data.email;
119
+ if (typeof data === 'object') {
120
+ if (data.firstName) state.set('firstName', data.firstName);
121
+ if (data.lastName) state.set('lastName', data.lastName);
122
+ }
123
+ state.set('email', email);
124
+ try {
125
+ const res = await api.signupStart(email);
126
+ const apiStep = mapNextStep(res?.next_step);
127
+ if (res?.user_id) state.set('userId', res.user_id);
128
+ if (res?.mobile) state.set('mobile', res.mobile);
129
+ if (apiStep && apiStep !== STEPS.EMAIL && apiStep !== STEPS.EMAIL_VERIFY) {
130
+ flow.setStep(apiStep);
131
+ return;
132
+ }
133
+ if (apiStep === STEPS.EMAIL_VERIFY) {
134
+ flow.goNext();
135
+ return;
136
+ }
137
+ flow.goNext();
138
+ } catch (err) {
139
+ const msg = err?.data?.error || err?.message || 'Request failed';
140
+ state.set('error', msg);
141
+ showError(msg);
142
+ }
143
+ }
144
+
145
+ async function handleEmailVerify(data) {
146
+ state.clearError();
147
+ state.setMultiple({ emailOtp: data.otp, firstName: data.firstName, lastName: data.lastName });
148
+ try {
149
+ const res = await api.signupEmailVerify({
150
+ email: state.get('email'),
151
+ otp: data.otp,
152
+ first_name: data.firstName,
153
+ last_name: data.lastName,
154
+ });
155
+ const userId = res?.user_id ?? res?.data?.user_id;
156
+ if (userId) state.set('userId', userId);
157
+ flow.goNext();
158
+ } catch (err) {
159
+ const msg = err?.data?.error || err?.message || 'Verification failed';
160
+ state.set('error', msg);
161
+ showError(msg);
162
+ }
163
+ }
164
+
165
+ async function handleMobileSubmit(mobile) {
166
+ state.clearError();
167
+ state.set('mobile', mobile);
168
+ try {
169
+ if (isSocial()) {
170
+ const token = state.get('loginToken');
171
+ const provider = state.get('socialProvider');
172
+ if (provider === 'microsoft') await api.microsoftMobileSend(token, mobile, appIdentifier);
173
+ else await api.googleMobileSend(token, mobile, appIdentifier);
174
+ } else {
175
+ await api.signupMobileSendOtp(state.get('email'), mobile);
176
+ }
177
+ flow.goNext();
178
+ } catch (err) {
179
+ const msg = err?.data?.error || err?.message || 'Failed to send code';
180
+ state.set('error', msg);
181
+ showError(msg);
182
+ }
183
+ }
184
+
185
+ async function handleMobileVerify(otp) {
186
+ state.clearError();
187
+ state.set('mobileOtp', otp);
188
+ try {
189
+ if (isSocial()) {
190
+ const token = state.get('loginToken');
191
+ const provider = state.get('socialProvider');
192
+ const mobile = state.get('mobile');
193
+ if (provider === 'microsoft') await api.microsoftMobileVerify(token, mobile, otp, appIdentifier);
194
+ else await api.googleMobileVerify(token, mobile, otp, appIdentifier);
195
+ } else {
196
+ await api.signupMobileVerify(state.get('email'), state.get('mobile'), otp);
197
+ }
198
+ flow.goNext();
199
+ } catch (err) {
200
+ const msg = err?.data?.error || err?.message || 'Verification failed';
201
+ state.set('error', msg);
202
+ showError(msg);
203
+ }
204
+ }
205
+
206
+ async function handlePasswordSubmit(password) {
207
+ state.clearError();
208
+ state.set('password', password);
209
+ const email = state.get('email');
210
+ if (!email) {
211
+ showError('Session error. Please start over.');
212
+ return;
213
+ }
214
+ try {
215
+ await api.signupPasswordSet(email, password);
216
+ flow.goNext();
217
+ } catch (err) {
218
+ const msg = err?.data?.error || err?.message || 'Failed to set password';
219
+ state.set('error', msg);
220
+ showError(msg);
221
+ }
222
+ }
223
+
224
+ async function handleFaceSubmit(captureDataUrl) {
225
+ state.clearError();
226
+ state.set('faceCapture', captureDataUrl);
227
+ const file = dataUrlToFile(captureDataUrl);
228
+ if (!file) {
229
+ showError('Please capture your face first.');
230
+ return;
231
+ }
232
+ try {
233
+ let apiResult = null;
234
+ if (isSocial()) {
235
+ const token = state.get('loginToken');
236
+ const provider = state.get('socialProvider');
237
+ if (provider === 'microsoft') apiResult = await api.microsoftFrEnroll(token, file, null, appIdentifier);
238
+ else apiResult = await api.googleFrEnroll(token, file, null, appIdentifier);
239
+ } else {
240
+ const email = state.get('email');
241
+ if (!email) {
242
+ showError('Session error. Please start over.');
243
+ return;
244
+ }
245
+ apiResult = await api.signupFrEnroll(email, file);
246
+ }
247
+ state.set('signupResult', apiResult);
248
+ flow.goNext();
249
+ if (typeof onSignupComplete === 'function') {
250
+ onSignupComplete(buildSignupCompletePayload(apiResult));
251
+ }
252
+ } catch (err) {
253
+ const msg = err?.data?.error || err?.message || 'Face registration failed';
254
+ state.set('error', msg);
255
+ showError(msg);
256
+ }
257
+ }
258
+
259
+ function handleSocialSignUp(provider) {
260
+ state.clearError();
261
+ const origin = typeof window !== 'undefined' && window.location ? window.location.origin : '';
262
+ const path = typeof window !== 'undefined' && window.location ? (window.location.pathname || '/') : '/';
263
+ const redirectUri = origin + path;
264
+ const url = provider === 'google'
265
+ ? api.getGoogleStartUrl(redirectUri, appIdentifier)
266
+ : api.getMicrosoftStartUrl(redirectUri, appIdentifier);
267
+ window.location.href = url;
268
+ }
269
+
270
+ const handlers = {
271
+ onEmailSubmit: handleEmailSubmit,
272
+ onEmailVerify: handleEmailVerify,
273
+ onMobileSubmit: handleMobileSubmit,
274
+ onMobileVerify: handleMobileVerify,
275
+ onPasswordSubmit: handlePasswordSubmit,
276
+ onFaceSubmit: handleFaceSubmit,
277
+ onBack: () => {
278
+ state.clearError();
279
+ showError('');
280
+ flow.goBack();
281
+ },
282
+ onSocialSignUp: handleSocialSignUp,
283
+ onContinue: typeof onContinue === 'function' ? onContinue : () => {},
284
+ onSwitchToLogin: typeof onSwitchToLogin === 'function' ? onSwitchToLogin : () => {},
285
+ onBackToApp: typeof onBack === 'function' ? onBack : () => { cleanupTheme(); layout.root.remove(); },
286
+ };
287
+
288
+ function render() {
289
+ state.clearError();
290
+ showError('');
291
+ const { current, total } = flow.getProgress();
292
+ layout.updateProgress(current, total);
293
+ const screen = createScreen(flow.getCurrentStep(), state, flow, handlers);
294
+ layout.setContent(screen);
295
+ }
296
+
297
+ flow.subscribe(render);
298
+ render();
299
+
300
+ return {
301
+ unmount() {
302
+ cleanupTheme();
303
+ layout.root.remove();
304
+ },
305
+ getState() {
306
+ return state.getAll();
307
+ },
308
+ getFlow() {
309
+ return flow;
310
+ },
311
+ getCurrentStep() {
312
+ return flow.getCurrentStep();
313
+ },
314
+ };
315
+ }