@strivacity/sdk-angular 4.0.0-beta.1 → 4.0.0-beta.2

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.
@@ -1,12 +1,11 @@
1
1
  export * from '@strivacity/sdk-core/flows';
2
2
  export * from '@strivacity/sdk-core/handlers';
3
- import { unflattenObject, isSessionExpired } from '@strivacity/sdk-core/utils';
3
+ import { flushSetCookies, toClientSession, createNativeLoginController } from '@strivacity/sdk-core/utils';
4
4
  export * from '@strivacity/sdk-core/utils';
5
5
  import * as i0 from '@angular/core';
6
- import { InjectionToken, CUSTOM_ELEMENTS_SCHEMA, NgModule, makeStateKey, provideAppInitializer, inject, REQUEST, TransferState, signal, PLATFORM_ID, DestroyRef, Injectable } from '@angular/core';
6
+ import { InjectionToken, CUSTOM_ELEMENTS_SCHEMA, NgModule, makeStateKey, provideAppInitializer, inject, REQUEST, TransferState, RESPONSE_INIT, signal, PLATFORM_ID, DestroyRef, Injectable, computed } from '@angular/core';
7
7
  import { isPlatformBrowser } from '@angular/common';
8
8
  import { initFlow } from '@strivacity/sdk-core';
9
- import { FallbackError } from '@strivacity/sdk-core/utils/errors';
10
9
  export { COOKIE_CHUNK_SIZE, COOKIE_CONTEXT, createCacheAPIStorage, createIndexedDBStorage, createLocalStorage, createMemoryStorage, createServerMemoryStorage, createSessionStorage, createWorkerStorage, handleWorkerStorageRequests } from '@strivacity/sdk-core/storages';
11
10
 
12
11
  const STRIVACITY_SDK = new InjectionToken('strivacity-sdk');
@@ -51,7 +50,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
51
50
  const SESSION_TRANSFER_KEY = makeStateKey('sty.session');
52
51
  /**
53
52
  * Loads the current session from the encrypted cookie storage (using Angular's `REQUEST` token) before the app
54
- * renders, and hands it over to the client through `TransferState`, so `StrivacityAuthService` can hydrate without
53
+ * renders, and hands it over to the client through `TransferState` (without the refresh and ID tokens, which stay server-side), so `StrivacityAuthService` can hydrate without
55
54
  * an extra round-trip or a loading flash.
56
55
  *
57
56
  * Add this to the server-only `ApplicationConfig` (e.g. `app.config.server.ts`), alongside `provideServerRendering()`.
@@ -66,8 +65,17 @@ function provideStrivacityServerSession(serverSdk) {
66
65
  return;
67
66
  }
68
67
  const transferState = inject(TransferState);
68
+ const responseInit = inject(RESPONSE_INIT, { optional: true });
69
69
  const session = await serverSdk.getSession(request);
70
- transferState.set(SESSION_TRANSFER_KEY, session);
70
+ const cookies = flushSetCookies(request, new Response(null)).headers.getSetCookie();
71
+ if (responseInit && cookies.length) {
72
+ const headers = new Headers(responseInit.headers);
73
+ for (const cookie of cookies) {
74
+ headers.append('set-cookie', cookie);
75
+ }
76
+ responseInit.headers = headers;
77
+ }
78
+ transferState.set(SESSION_TRANSFER_KEY, toClientSession(session));
71
79
  });
72
80
  }
73
81
 
@@ -136,8 +144,8 @@ class StrivacityAuthService {
136
144
  handleCallback(...args) {
137
145
  return this.sdk.handleCallback(...args);
138
146
  }
139
- refresh() {
140
- return this.sdk.refresh();
147
+ refresh(...args) {
148
+ return this.sdk.refresh(...args);
141
149
  }
142
150
  revoke() {
143
151
  return this.sdk.revoke();
@@ -191,22 +199,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
191
199
  class StrivacityNativeLoginService {
192
200
  constructor() {
193
201
  this.authService = inject(StrivacityAuthService);
194
- this.sdk = this.authService.sdk;
195
- this.abortController = new AbortController();
196
- this.options = {};
197
- this.loadingSignal = signal(true, /* @ts-ignore */
198
- ...(ngDevMode ? [{ debugName: "loadingSignal" }] : /* istanbul ignore next */ []));
199
- this.formsSignal = signal({}, /* @ts-ignore */
200
- ...(ngDevMode ? [{ debugName: "formsSignal" }] : /* istanbul ignore next */ []));
201
- this.messagesSignal = signal({}, /* @ts-ignore */
202
- ...(ngDevMode ? [{ debugName: "messagesSignal" }] : /* istanbul ignore next */ []));
203
- this.stateSignal = signal({}, /* @ts-ignore */
204
- ...(ngDevMode ? [{ debugName: "stateSignal" }] : /* istanbul ignore next */ []));
205
- this.loading = this.loadingSignal.asReadonly();
206
- this.forms = this.formsSignal.asReadonly();
207
- this.messages = this.messagesSignal.asReadonly();
208
- this.state = this.stateSignal.asReadonly();
209
- inject(DestroyRef).onDestroy(() => this.abortController.abort());
202
+ this.controller = createNativeLoginController(() => this.authService.sdk);
203
+ this.snapshot = signal(this.controller.getSnapshot(), /* @ts-ignore */
204
+ ...(ngDevMode ? [{ debugName: "snapshot" }] : /* istanbul ignore next */ []));
205
+ this.loading = computed(() => this.snapshot().loading, /* @ts-ignore */
206
+ ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
207
+ this.forms = computed(() => this.snapshot().forms, /* @ts-ignore */
208
+ ...(ngDevMode ? [{ debugName: "forms" }] : /* istanbul ignore next */ []));
209
+ this.messages = computed(() => this.snapshot().messages, /* @ts-ignore */
210
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
211
+ this.state = computed(() => this.snapshot().state, /* @ts-ignore */
212
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
213
+ const unsubscribe = this.controller.subscribe((snapshot) => this.snapshot.set(snapshot));
214
+ inject(DestroyRef).onDestroy(unsubscribe);
210
215
  }
211
216
  /**
212
217
  * Starts a native login flow session. Should be called once, from the page component that owns this service.
@@ -214,123 +219,23 @@ class StrivacityNativeLoginService {
214
219
  * @param {NativeLoginOptions} [options] - Optional options for the login session.
215
220
  */
216
221
  async start(options = {}) {
217
- this.options = options;
218
- await this.sdk.init();
219
- try {
220
- await this.startSession(options.params);
221
- }
222
- catch (error) {
223
- if (error instanceof DOMException && error.name === 'AbortError') {
224
- return;
225
- }
226
- this.sdk.logging?.error('Error fetching authorization URI', error);
227
- await this.options.onError?.(error);
228
- this.loadingSignal.set(false);
229
- }
222
+ this.controller.setOptions(options);
223
+ await this.controller.start();
230
224
  }
231
- async submitForm(formId, customBody) {
232
- try {
233
- this.loadingSignal.set(true);
234
- const nextState = await this.sdk.submitForm(formId, customBody ?? unflattenObject(this.formsSignal()[formId] ?? {}));
235
- if (nextState) {
236
- await this.handleResponse(nextState);
237
- }
238
- }
239
- catch (error) {
240
- if (error instanceof FallbackError) {
241
- this.sdk.logging?.error('Fallback error occurred', error);
242
- await this.options.onFallback?.(error);
243
- }
244
- else {
245
- this.sdk.logging?.error('Error submitting form', error);
246
- await this.options.onError?.(error);
247
- }
248
- }
225
+ submitForm(formId, customBody) {
226
+ return this.controller.submitForm(formId, customBody);
249
227
  }
250
228
  setFormValue(formId, widgetId, value) {
251
- const forms = { ...this.formsSignal() };
252
- forms[formId] = { ...(forms[formId] ?? {}), [widgetId]: value === '' ? null : value };
253
- this.formsSignal.set(forms);
229
+ this.controller.setFormValue(formId, widgetId, value);
254
230
  }
255
231
  setMessage(formId, widgetId, value) {
256
- const messages = { ...this.messagesSignal() };
257
- messages[formId] = { ...(messages[formId] ?? {}), [widgetId]: value };
258
- this.messagesSignal.set(messages);
232
+ this.controller.setMessage(formId, widgetId, value);
259
233
  }
260
234
  triggerFallback(message) {
261
- this.sdk.logging?.warn(message ? `Triggering fallback due to: ${message}` : 'Triggering fallback');
262
- const hostedUrl = this.stateSignal().hostedUrl;
263
- if (!hostedUrl) {
264
- const error = new Error('No hosted URL provided');
265
- this.sdk.logging?.error('Fallback error', error);
266
- throw error;
267
- }
268
- void this.options.onFallback?.(new FallbackError(new URL(hostedUrl)));
235
+ this.controller.triggerFallback(message);
269
236
  }
270
237
  triggerClose() {
271
- this.sdk.logging?.debug('Triggering close');
272
- void this.options.onClose?.();
273
- }
274
- async startSession(loginParams = {}) {
275
- try {
276
- this.loadingSignal.set(true);
277
- const nextState = await this.sdk.startSession(loginParams);
278
- if (nextState) {
279
- await this.handleResponse(nextState);
280
- }
281
- }
282
- catch (error) {
283
- if (error instanceof FallbackError) {
284
- this.sdk.logging?.error('Fallback error occurred', error);
285
- await this.options.onFallback?.(error);
286
- }
287
- else {
288
- this.sdk.logging?.error('Error starting session', error);
289
- await this.options.onError?.(error);
290
- }
291
- }
292
- }
293
- async handleResponse(nextState) {
294
- if (this.sdk.session && !isSessionExpired(this.sdk.session)) {
295
- return await this.options.onLogin?.(this.sdk.session);
296
- }
297
- const currentState = this.stateSignal();
298
- const newState = {
299
- hostedUrl: nextState.hostedUrl ?? currentState.hostedUrl,
300
- finalizeUrl: nextState.finalizeUrl ?? currentState.finalizeUrl,
301
- screen: nextState.screen ?? currentState.screen,
302
- forms: nextState.forms ?? currentState.forms,
303
- layout: nextState.layout ?? currentState.layout,
304
- messages: nextState.messages ?? {},
305
- branding: nextState.branding ?? currentState.branding,
306
- };
307
- if (newState.screen !== currentState.screen) {
308
- const nextForms = {};
309
- const nextMessages = {};
310
- for (const form of newState.forms ?? []) {
311
- nextForms[form.id] = {};
312
- nextMessages[form.id] = {};
313
- }
314
- this.formsSignal.set(nextForms);
315
- this.messagesSignal.set(nextMessages);
316
- }
317
- else {
318
- this.sdk.logging?.info(`Updating screen: ${newState.screen}`);
319
- }
320
- const nextMessageMap = { ...this.messagesSignal() };
321
- for (const formId of Object.keys(newState.messages ?? {})) {
322
- if (formId === 'global') {
323
- await this.options.onGlobalMessage?.(newState.messages.global);
324
- }
325
- else {
326
- nextMessageMap[formId] = newState.messages[formId] ?? {};
327
- }
328
- }
329
- this.messagesSignal.set(nextMessageMap);
330
- this.stateSignal.set(newState);
331
- if (!newState.finalizeUrl) {
332
- this.loadingSignal.set(false);
333
- }
238
+ this.controller.triggerClose();
334
239
  }
335
240
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrivacityNativeLoginService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
336
241
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrivacityNativeLoginService }); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strivacity/sdk-angular",
3
- "version": "4.0.0-beta.1",
3
+ "version": "4.0.0-beta.2",
4
4
  "description": "Strivacity Angular SDK client",
5
5
  "license": "MIT",
6
6
  "author": "strivacity <opensource@strivacity.com>",
@@ -32,7 +32,7 @@
32
32
  }
33
33
  },
34
34
  "dependencies": {
35
- "@strivacity/sdk-core": "4.0.0-beta.1"
35
+ "@strivacity/sdk-core": "4.0.0-beta.2"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@angular/core": ">=20",
@@ -76,8 +76,8 @@ export class StrivacityAuthService {
76
76
  return this.sdk.handleCallback(...args);
77
77
  }
78
78
 
79
- refresh(): ReturnType<typeof this.sdk.refresh> {
80
- return this.sdk.refresh();
79
+ refresh(...args: Parameters<typeof this.sdk.refresh>): ReturnType<typeof this.sdk.refresh> {
80
+ return this.sdk.refresh(...args);
81
81
  }
82
82
 
83
83
  revoke(): ReturnType<typeof this.sdk.revoke> {
@@ -1,28 +1,23 @@
1
- import type { NativeFlowMessage, NativeFlowState, NativeParams, NativeLoginOptions } from '../../types';
2
- import { DestroyRef, Injectable, inject, signal } from '@angular/core';
3
- import { FallbackError } from '@strivacity/sdk-core/utils/errors';
4
- import { isSessionExpired, unflattenObject } from '@strivacity/sdk-core/utils';
1
+ import type { NativeFlowMessage, NativeLoginOptions } from '../../types';
2
+ import { DestroyRef, Injectable, computed, inject, signal } from '@angular/core';
3
+ import { createNativeLoginController } from '@strivacity/sdk-core/utils';
5
4
  import { StrivacityAuthService } from './auth.service';
6
5
 
7
6
  @Injectable()
8
7
  export class StrivacityNativeLoginService {
9
8
  private readonly authService = inject(StrivacityAuthService);
10
- private readonly sdk = this.authService.sdk;
11
- private readonly abortController = new AbortController();
12
- private options: NativeLoginOptions = {};
9
+ private readonly controller = createNativeLoginController(() => this.authService.sdk);
10
+ private readonly snapshot = signal(this.controller.getSnapshot());
13
11
 
14
- private readonly loadingSignal = signal(true);
15
- private readonly formsSignal = signal<Record<string, Record<string, unknown>>>({});
16
- private readonly messagesSignal = signal<Record<string, Record<string, NativeFlowMessage>>>({});
17
- private readonly stateSignal = signal<Partial<NativeFlowState>>({});
18
-
19
- readonly loading = this.loadingSignal.asReadonly();
20
- readonly forms = this.formsSignal.asReadonly();
21
- readonly messages = this.messagesSignal.asReadonly();
22
- readonly state = this.stateSignal.asReadonly();
12
+ readonly loading = computed(() => this.snapshot().loading);
13
+ readonly forms = computed(() => this.snapshot().forms);
14
+ readonly messages = computed(() => this.snapshot().messages);
15
+ readonly state = computed(() => this.snapshot().state);
23
16
 
24
17
  constructor() {
25
- inject(DestroyRef).onDestroy(() => this.abortController.abort());
18
+ const unsubscribe = this.controller.subscribe((snapshot) => this.snapshot.set(snapshot));
19
+
20
+ inject(DestroyRef).onDestroy(unsubscribe);
26
21
  }
27
22
 
28
23
  /**
@@ -31,142 +26,27 @@ export class StrivacityNativeLoginService {
31
26
  * @param {NativeLoginOptions} [options] - Optional options for the login session.
32
27
  */
33
28
  async start(options: NativeLoginOptions = {}): Promise<void> {
34
- this.options = options;
35
-
36
- await this.sdk.init();
37
-
38
- try {
39
- await this.startSession(options.params);
40
- } catch (error) {
41
- if (error instanceof DOMException && error.name === 'AbortError') {
42
- return;
43
- }
44
-
45
- this.sdk.logging?.error('Error fetching authorization URI', error as Error);
46
- await this.options.onError?.(error as Error);
47
- this.loadingSignal.set(false);
48
- }
29
+ this.controller.setOptions(options);
30
+ await this.controller.start();
49
31
  }
50
32
 
51
- async submitForm(formId: string, customBody?: Record<string, unknown>): Promise<void> {
52
- try {
53
- this.loadingSignal.set(true);
54
-
55
- const nextState = await this.sdk.submitForm(formId, customBody ?? unflattenObject(this.formsSignal()[formId] ?? {}));
56
-
57
- if (nextState) {
58
- await this.handleResponse(nextState);
59
- }
60
- } catch (error) {
61
- if (error instanceof FallbackError) {
62
- this.sdk.logging?.error('Fallback error occurred', error);
63
- await this.options.onFallback?.(error);
64
- } else {
65
- this.sdk.logging?.error('Error submitting form', error as Error);
66
- await this.options.onError?.(error as Error);
67
- }
68
- }
33
+ submitForm(formId: string, customBody?: Record<string, unknown>): Promise<void> {
34
+ return this.controller.submitForm(formId, customBody);
69
35
  }
70
36
 
71
37
  setFormValue(formId: string, widgetId: string, value: unknown): void {
72
- const forms = { ...this.formsSignal() };
73
-
74
- forms[formId] = { ...(forms[formId] ?? {}), [widgetId]: value === '' ? null : value };
75
- this.formsSignal.set(forms);
38
+ this.controller.setFormValue(formId, widgetId, value);
76
39
  }
77
40
 
78
41
  setMessage(formId: string, widgetId: string, value: NativeFlowMessage): void {
79
- const messages = { ...this.messagesSignal() };
80
-
81
- messages[formId] = { ...(messages[formId] ?? {}), [widgetId]: value };
82
- this.messagesSignal.set(messages);
42
+ this.controller.setMessage(formId, widgetId, value);
83
43
  }
84
44
 
85
45
  triggerFallback(message?: string): void {
86
- this.sdk.logging?.warn(message ? `Triggering fallback due to: ${message}` : 'Triggering fallback');
87
-
88
- const hostedUrl = this.stateSignal().hostedUrl;
89
-
90
- if (!hostedUrl) {
91
- const error = new Error('No hosted URL provided');
92
- this.sdk.logging?.error('Fallback error', error);
93
- throw error;
94
- }
95
-
96
- void this.options.onFallback?.(new FallbackError(new URL(hostedUrl)));
46
+ this.controller.triggerFallback(message);
97
47
  }
98
48
 
99
49
  triggerClose(): void {
100
- this.sdk.logging?.debug('Triggering close');
101
- void this.options.onClose?.();
102
- }
103
-
104
- private async startSession(loginParams: NativeParams = {}): Promise<void> {
105
- try {
106
- this.loadingSignal.set(true);
107
-
108
- const nextState = await this.sdk.startSession(loginParams);
109
-
110
- if (nextState) {
111
- await this.handleResponse(nextState);
112
- }
113
- } catch (error) {
114
- if (error instanceof FallbackError) {
115
- this.sdk.logging?.error('Fallback error occurred', error);
116
- await this.options.onFallback?.(error);
117
- } else {
118
- this.sdk.logging?.error('Error starting session', error as Error);
119
- await this.options.onError?.(error as Error);
120
- }
121
- }
122
- }
123
-
124
- private async handleResponse(nextState: Partial<NativeFlowState>): Promise<void> {
125
- if (this.sdk.session && !isSessionExpired(this.sdk.session)) {
126
- return await this.options.onLogin?.(this.sdk.session);
127
- }
128
-
129
- const currentState = this.stateSignal();
130
- const newState: NativeFlowState = {
131
- hostedUrl: nextState.hostedUrl ?? currentState.hostedUrl,
132
- finalizeUrl: nextState.finalizeUrl ?? currentState.finalizeUrl,
133
- screen: nextState.screen ?? currentState.screen,
134
- forms: nextState.forms ?? currentState.forms,
135
- layout: nextState.layout ?? currentState.layout,
136
- messages: nextState.messages ?? {},
137
- branding: nextState.branding ?? currentState.branding,
138
- };
139
-
140
- if (newState.screen !== currentState.screen) {
141
- const nextForms: Record<string, Record<string, unknown>> = {};
142
- const nextMessages: Record<string, Record<string, NativeFlowMessage>> = {};
143
-
144
- for (const form of newState.forms ?? []) {
145
- nextForms[form.id] = {};
146
- nextMessages[form.id] = {};
147
- }
148
-
149
- this.formsSignal.set(nextForms);
150
- this.messagesSignal.set(nextMessages);
151
- } else {
152
- this.sdk.logging?.info(`Updating screen: ${newState.screen}`);
153
- }
154
-
155
- const nextMessageMap = { ...this.messagesSignal() };
156
-
157
- for (const formId of Object.keys(newState.messages ?? {})) {
158
- if (formId === 'global') {
159
- await this.options.onGlobalMessage?.(newState.messages!.global!);
160
- } else {
161
- nextMessageMap[formId] = newState.messages![formId] ?? {};
162
- }
163
- }
164
-
165
- this.messagesSignal.set(nextMessageMap);
166
- this.stateSignal.set(newState);
167
-
168
- if (!newState.finalizeUrl) {
169
- this.loadingSignal.set(false);
170
- }
50
+ this.controller.triggerClose();
171
51
  }
172
52
  }
package/src/server/sdk.ts CHANGED
@@ -93,7 +93,7 @@ export function createServerSDK(initConfig: AngularServerSDKInitConfig): Angular
93
93
  return base.options;
94
94
  },
95
95
  handlers: getRouteHandlers(base),
96
- getSession: (req) => base.getSession(req),
96
+ getSession: (req, params) => base.getSession(req, params),
97
97
  updateSession: (session, req) => base.updateSession(session, req),
98
98
  refreshSession: (req) => base.refreshSession(req),
99
99
  revokeSession: (req) => base.revokeSession(req),
@@ -1,12 +1,13 @@
1
1
  import type { EnvironmentProviders } from '@angular/core';
2
2
  import type { AngularServerSDK, SessionData } from './types';
3
- import { REQUEST, TransferState, inject, makeStateKey, provideAppInitializer } from '@angular/core';
3
+ import { REQUEST, RESPONSE_INIT, TransferState, inject, makeStateKey, provideAppInitializer } from '@angular/core';
4
+ import { flushSetCookies, toClientSession } from '@strivacity/sdk-core/utils';
4
5
 
5
6
  export const SESSION_TRANSFER_KEY = makeStateKey<SessionData | null>('sty.session');
6
7
 
7
8
  /**
8
9
  * Loads the current session from the encrypted cookie storage (using Angular's `REQUEST` token) before the app
9
- * renders, and hands it over to the client through `TransferState`, so `StrivacityAuthService` can hydrate without
10
+ * renders, and hands it over to the client through `TransferState` (without the refresh and ID tokens, which stay server-side), so `StrivacityAuthService` can hydrate without
10
11
  * an extra round-trip or a loading flash.
11
12
  *
12
13
  * Add this to the server-only `ApplicationConfig` (e.g. `app.config.server.ts`), alongside `provideServerRendering()`.
@@ -23,8 +24,21 @@ export function provideStrivacityServerSession(serverSdk: AngularServerSDK | und
23
24
  }
24
25
 
25
26
  const transferState = inject(TransferState);
27
+ const responseInit = inject(RESPONSE_INIT, { optional: true });
28
+
26
29
  const session = await serverSdk.getSession(request);
30
+ const cookies = flushSetCookies(request, new Response(null)).headers.getSetCookie();
31
+
32
+ if (responseInit && cookies.length) {
33
+ const headers = new Headers(responseInit.headers);
34
+
35
+ for (const cookie of cookies) {
36
+ headers.append('set-cookie', cookie);
37
+ }
38
+
39
+ responseInit.headers = headers;
40
+ }
27
41
 
28
- transferState.set(SESSION_TRANSFER_KEY, session);
42
+ transferState.set(SESSION_TRANSFER_KEY, toClientSession(session));
29
43
  });
30
44
  }
@@ -16,17 +16,17 @@ export type AngularServerStorage = SDKStorage<
16
16
  export type AngularServerSDKOptions<
17
17
  Storage extends AngularServerStorage = AngularServerStorage,
18
18
  StateStorage extends SDKStorage = SDKStorage,
19
- > = ServerSDKOptions<Storage, StateStorage>;
19
+ > = ServerSDKOptions<AngularServerRequest | undefined, Storage, StateStorage>;
20
20
 
21
21
  export type AngularServerSDKInitConfig<
22
22
  Storage extends AngularServerStorage = AngularServerStorage,
23
23
  StateStorage extends SDKStorage = SDKStorage,
24
- > = ServerSDKInitConfig & Partial<AngularServerSDKOptions<Storage, StateStorage>>;
24
+ > = ServerSDKInitConfig<AngularServerRequest | undefined> & Partial<AngularServerSDKOptions<Storage, StateStorage>>;
25
25
 
26
26
  export type AngularServerSDK<TEvent extends AngularServerRequest | undefined = AngularServerRequest | undefined> = BaseServerSDK<TEvent> & {
27
27
  /**
28
28
  * The Express router exposing the authentication endpoints.
29
- * Mount it under `authUrlPrefix`, e.g. `app.use('/auth', sdk.router)`.
29
+ * Mount it under `authUrlPrefix`, e.g. `app.use('/auth', sdk.handlers)`.
30
30
  */
31
31
  readonly handlers: Router;
32
32
  };
@@ -1,6 +1,5 @@
1
- import type { SDKInitConfig, NativeFlowMessage, NativeParams, SessionData } from '@strivacity/sdk-core/types';
1
+ import type { SDKInitConfig, NativeLoginControllerOptions } from '@strivacity/sdk-core/types';
2
2
  import type { initFlow } from '@strivacity/sdk-core';
3
- import type { FallbackError } from '@strivacity/sdk-core/utils';
4
3
 
5
4
  export * from '@strivacity/sdk-core/types';
6
5
 
@@ -14,34 +13,7 @@ export type AngularSDKInitConfig = SDKInitConfig;
14
13
  */
15
14
  export type SDKInstance = ReturnType<typeof initFlow>;
16
15
 
17
- export type NativeLoginOptions = {
18
- /**
19
- * Optional parameters to be passed to the login session request. These parameters will be sent to the `authorizationUri` endpoint.
20
- */
21
- params?: NativeParams;
22
-
23
- /**
24
- * Called when the user successfully completes the login flow.
25
- */
26
- onLogin?: (session: SessionData) => void | Promise<void>;
27
-
28
- /**
29
- * Called when the fallback flow is triggered, typically due to an error or unsupported environment.
30
- */
31
- onFallback?: (error: FallbackError) => void | Promise<void>;
32
-
33
- /**
34
- * Called when the login flow is closed by the user.
35
- */
36
- onClose?: () => void | Promise<void>;
37
-
38
- /**
39
- * Called when an error occurs during the login session initialization or flow.
40
- */
41
- onError?: (error: Error) => void | Promise<void>;
42
-
43
- /**
44
- * Called when a global message is received during the login flow.
45
- */
46
- onGlobalMessage?: (message: NativeFlowMessage) => void | Promise<void>;
47
- };
16
+ /**
17
+ * Callbacks and parameters of the native login flow. See `NativeLoginControllerOptions` in `@strivacity/sdk-core`.
18
+ */
19
+ export type NativeLoginOptions = NativeLoginControllerOptions;
@@ -202,7 +202,7 @@ describe('StrivacityAuthService', () => {
202
202
  ['checkAuthentication', [{ autoRefresh: false }]],
203
203
  ['tokenExchange', [{ code: 'abc' }]],
204
204
  ['handleCallback', ['https://brandtegrity.io/callback?code=abc']],
205
- ['refresh', []],
205
+ ['refresh', [{ returnTo: '/orders' }]],
206
206
  ['revoke', []],
207
207
  ['logout', [{ postLogoutRedirectUri: 'https://brandtegrity.io' }]],
208
208
  ['login', [{ scopes: ['openid'] }]],
@@ -35,6 +35,7 @@ describe('client', () => {
35
35
  'buildEndSessionUrl',
36
36
  'createDefaultLogging',
37
37
  'createHttpClient',
38
+ 'createNativeLoginController',
38
39
  'createSession',
39
40
  'createState',
40
41
  'createWebAuthnCredential',
@@ -72,6 +73,7 @@ describe('client', () => {
72
73
  'throwHttpError',
73
74
  'throwTokenEndpointError',
74
75
  'timestamp',
76
+ 'toClientSession',
75
77
  'toSafeRedirect',
76
78
  'unflattenObject',
77
79
  'verifyIdToken',
@@ -128,7 +130,10 @@ describe('server', () => {
128
130
  'createServerStateStorage',
129
131
  'createSessionStorage',
130
132
  'createWorkerStorage',
133
+ 'deleteEncryptedCookie',
131
134
  'handleWorkerStorageRequests',
135
+ 'readEncryptedCookie',
136
+ 'writeEncryptedCookie',
132
137
  'AlgorithmTypeList',
133
138
  'GrantTypeList',
134
139
  'ResponseModeList',
@@ -147,6 +152,7 @@ describe('server', () => {
147
152
  'buildEndSessionUrl',
148
153
  'createDefaultLogging',
149
154
  'createHttpClient',
155
+ 'createNativeLoginController',
150
156
  'createSession',
151
157
  'createState',
152
158
  'createWebAuthnCredential',
@@ -182,6 +188,7 @@ describe('server', () => {
182
188
  'serializeState',
183
189
  'submitLoginForm',
184
190
  'timestamp',
191
+ 'toClientSession',
185
192
  'toSafeRedirect',
186
193
  'unflattenObject',
187
194
  'verifyIdToken',
@@ -149,19 +149,17 @@ describe('StrivacityNativeLoginService', () => {
149
149
  expect(login.loading()).toBe(true);
150
150
  });
151
151
 
152
- test('propagates a rejection from sdk.init() as-is, since only the startSession() call is guarded by the try/catch', async () => {
153
- // the AbortError short-circuit in start()'s own catch guards `this.startSession(...)`, not `await this.sdk.init()`
154
- // above it - startSession() also swallows every error itself (including AbortError) and reports it through onError,
155
- // so that branch is unreachable via a mocked flow; a rejecting init() is the one thing genuinely uncaught here.
152
+ test('reports a rejection from sdk.init() to onError and stops loading, without starting a session', async () => {
156
153
  const flow = createMockFlow<NativeFlow>();
157
- const abortError = new DOMException('The operation was aborted', 'AbortError');
158
- vi.mocked(flow.init).mockRejectedValue(abortError);
154
+ const error = new Error('init failed');
155
+ vi.mocked(flow.init).mockRejectedValue(error);
159
156
  const onError = vi.fn();
160
157
  const login = configure(flow);
161
158
 
162
- await expect(login.start({ onError })).rejects.toBe(abortError);
159
+ await login.start({ onError });
163
160
 
164
- expect(onError).not.toHaveBeenCalled();
161
+ expect(onError).toHaveBeenCalledWith(error);
162
+ expect(login.loading()).toBe(false);
165
163
  expect(flow.startSession).not.toHaveBeenCalled();
166
164
  });
167
165
  });
@@ -293,19 +291,17 @@ describe('StrivacityNativeLoginService', () => {
293
291
  expect(flow.logging!.debug).toHaveBeenCalledWith('Triggering close');
294
292
  });
295
293
 
296
- test('aborts the in-flight abort controller when the owning injector is destroyed', () => {
294
+ test('stops tracking the flow state when the owning injector is destroyed', () => {
297
295
  const flow = createMockFlow<NativeFlow>();
298
296
  vi.mocked(initFlow).mockReturnValue(flow);
299
297
  TestBed.configureTestingModule({ providers: [{ provide: STRIVACITY_SDK, useValue: createOptions({ mode: 'native' }) }, StrivacityAuthService] });
300
298
 
301
- const abortSpy = vi.spyOn(AbortController.prototype, 'abort');
302
-
303
299
  // both services must live on the component's own injector for fixture.destroy() to fire their DestroyRef callbacks
304
- const { destroy } = mountWithProviders([StrivacityAuthService, StrivacityNativeLoginService], () => inject(StrivacityNativeLoginService));
300
+ const { value: login, destroy } = mountWithProviders([StrivacityAuthService, StrivacityNativeLoginService], () => inject(StrivacityNativeLoginService));
305
301
 
306
302
  destroy();
303
+ login.setFormValue('form1', 'email', 'a@b.c');
307
304
 
308
- expect(abortSpy).toHaveBeenCalledTimes(1);
309
- abortSpy.mockRestore();
305
+ expect(login.forms()).toEqual({});
310
306
  });
311
307
  });