@rdlabo/ionic-angular-kit 0.0.2 → 0.0.3

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/README.md CHANGED
@@ -272,26 +272,28 @@ A fleet-canonical HTTP interceptor with:
272
272
  ```typescript
273
273
  // app.config.ts
274
274
  import { provideHttpClient, withInterceptors } from '@angular/common/http';
275
- import { kitAuthInterceptor, provideKitHttp, kitPresentReloadAlert } from '@rdlabo/ionic-angular-kit';
275
+ import { kitAuthInterceptor, provideKitHttp, KitReloadAlertController } from '@rdlabo/ionic-angular-kit';
276
276
 
277
277
  export const appConfig: ApplicationConfig = {
278
278
  providers: [
279
279
  provideHttpClient(withInterceptors([kitAuthInterceptor])),
280
280
  provideKitHttp(() => {
281
281
  const auth = inject(AuthService);
282
- const overlay = inject(KitOverlayController);
282
+ const reload = inject(KitReloadAlertController);
283
283
  return {
284
284
  getAuthHeaders: async (req) => ({
285
285
  Authorization: `Bearer ${await auth.getToken()}`,
286
286
  }),
287
287
  onUnauthorized: (req) => auth.signOut(),
288
- // Fleet-canonical "network error → offer reload" (see kitPresentReloadAlert).
288
+ // Fleet-canonical "network error → offer reload" (see KitReloadAlertController).
289
289
  onNetworkError: (status) =>
290
- kitPresentReloadAlert(overlay, {
290
+ reload.present({
291
291
  header: 'ネットワークエラー',
292
292
  message: `通信できませんでした。リフレッシュしますか?(${status})`,
293
293
  okText: 'リフレッシュ',
294
294
  }),
295
+ // Auto-dismiss the stale alert once connectivity is back.
296
+ onResponse: () => void reload.dismiss(),
295
297
  // buildExtraHeaders / bypass / offlineFallback / onForbidden / onServerError omitted → kit defaults.
296
298
  };
297
299
  }),
@@ -306,18 +308,28 @@ export const appConfig: ApplicationConfig = {
306
308
  4. Non-400/500 status AND device connected → `onNetworkError`
307
309
  5. 400 or 500 with `error.message` → `onServerError`
308
310
 
309
- ### kitPresentReloadAlert
311
+ ### KitReloadAlertController
310
312
 
311
- The fleet's canonical "network error → offer to reload" confirmation, folding together the three concerns every app used to copy-paste: (1) suppress stacking when an `ion-alert` is already shown, (2) `alertConfirm`, (3) `location.reload()` on confirm. All text is passed in, so the kit stays free of hardcoded i18n. Usually wired from `onNetworkError`, but callable anywhere (e.g. an offline fallback).
313
+ The fleet's canonical "network error → offer to reload" alert, as a stateful controller that unifies the good-UX variant that had drifted across apps:
314
+
315
+ - **De-dup** — never stacks; a second `present()` while one is showing is a no-op.
316
+ - **Backdrop lock** — `backdropDismiss: false`, so a critical error isn't dismissed by an accidental backdrop tap.
317
+ - **Auto-dismiss on reconnect** — `dismiss()` (called from a later successful response) clears a now-stale error alert.
318
+ - **Reload on confirm** — the confirm button calls `location.reload()`; cancel uses the configured `labels.cancel`.
319
+
320
+ All text is passed in, so the kit stays free of hardcoded i18n. Wire `present` from a network-class error and `dismiss` from a success (interceptor `onResponse`, or a class interceptor's success path).
312
321
 
313
322
  ```typescript
314
- import { kitPresentReloadAlert } from '@rdlabo/ionic-angular-kit';
323
+ import { KitReloadAlertController } from '@rdlabo/ionic-angular-kit';
315
324
 
316
- await kitPresentReloadAlert(overlay, {
325
+ const reload = inject(KitReloadAlertController);
326
+ await reload.present({
317
327
  header: 'ネットワークエラー',
318
328
  message: `通信できませんでした。リフレッシュしますか?(${status})`,
319
329
  okText: 'リフレッシュ',
320
330
  });
331
+ // later, on a successful response:
332
+ await reload.dismiss();
321
333
  ```
322
334
 
323
335
  ---
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "../../dist/kit",
4
+ "lib": {
5
+ "entryFile": "src/public-api.ts"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdlabo/ionic-angular-kit",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^21.0.0",
6
6
  "@angular/core": "^21.0.0",
@@ -16,17 +16,5 @@
16
16
  "dependencies": {
17
17
  "tslib": "^2.3.0"
18
18
  },
19
- "sideEffects": false,
20
- "module": "fesm2022/rdlabo-ionic-angular-kit.mjs",
21
- "typings": "types/rdlabo-ionic-angular-kit.d.ts",
22
- "exports": {
23
- "./package.json": {
24
- "default": "./package.json"
25
- },
26
- ".": {
27
- "types": "./types/rdlabo-ionic-angular-kit.d.ts",
28
- "default": "./fesm2022/rdlabo-ionic-angular-kit.mjs"
29
- }
30
- },
31
- "type": "module"
32
- }
19
+ "sideEffects": false
20
+ }
@@ -0,0 +1,210 @@
1
+ import { provideZonelessChangeDetection } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import type { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
4
+ import { Router } from '@angular/router';
5
+ import { NavController } from '@ionic/angular/standalone';
6
+ import type { Observable } from 'rxjs';
7
+ import { of } from 'rxjs';
8
+ import { firstValueFrom } from 'rxjs';
9
+
10
+ import {
11
+ type KitAuthState,
12
+ provideKitAuth,
13
+ kitRequiredUnauthorizedGuard,
14
+ kitRequireConfirmingGuard,
15
+ kitRequireAuthorizedGuard,
16
+ } from './auth-guards';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers
20
+ // ---------------------------------------------------------------------------
21
+ const REDIRECTS = {
22
+ whenAuthorized: '/home',
23
+ whenConfirming: '/auth/confirm',
24
+ whenNotConfirming: '/auth/signin',
25
+ whenUnauthorized: '/auth',
26
+ };
27
+
28
+ const routeStub = {} as ActivatedRouteSnapshot;
29
+ const stateStub = {} as RouterStateSnapshot;
30
+
31
+ /**
32
+ * The guards always return an Observable at runtime (rxjs pipe).
33
+ * CanActivateFn returns `MaybeAsync<GuardResult>` which widens the compile-time type,
34
+ * so we cast to Observable before handing to firstValueFrom.
35
+ */
36
+ function runGuard(value: unknown): Promise<boolean | UrlTree> {
37
+ return firstValueFrom(value as Observable<boolean | UrlTree>);
38
+ }
39
+
40
+ /** Cast a vi.fn() mock so it satisfies a typed function signature. */
41
+ function mockFn<T>(): T {
42
+ return vi.fn() as unknown as T;
43
+ }
44
+
45
+ function setup(
46
+ state: KitAuthState,
47
+ {
48
+ onAuthorized = vi.fn().mockResolvedValue(true) as unknown as (s: RouterStateSnapshot) => Promise<boolean | UrlTree>,
49
+ onUnauthenticated = vi.fn().mockResolvedValue(false) as unknown as (s: RouterStateSnapshot) => Promise<boolean | UrlTree>,
50
+ }: {
51
+ onAuthorized?: (s: RouterStateSnapshot) => Promise<boolean | UrlTree>;
52
+ onUnauthenticated?: (s: RouterStateSnapshot) => Promise<boolean | UrlTree>;
53
+ } = {},
54
+ ) {
55
+ const navigate = vi.fn().mockResolvedValue(true);
56
+ const setDirection = vi.fn();
57
+
58
+ TestBed.configureTestingModule({
59
+ providers: [
60
+ provideZonelessChangeDetection(),
61
+ provideKitAuth(() => ({
62
+ authState: () => of(state),
63
+ onAuthorized,
64
+ onUnauthenticated,
65
+ redirects: REDIRECTS,
66
+ })),
67
+ { provide: Router, useValue: { navigate } },
68
+ { provide: NavController, useValue: { setDirection } },
69
+ ],
70
+ });
71
+
72
+ return { navigate, setDirection, onAuthorized, onUnauthenticated };
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // kitRequiredUnauthorizedGuard
77
+ // ---------------------------------------------------------------------------
78
+ describe('kitRequiredUnauthorizedGuard', () => {
79
+ afterEach(() => TestBed.resetTestingModule());
80
+
81
+ it("'user' → navigates whenAuthorized and returns false", async () => {
82
+ const { navigate, setDirection } = setup('user');
83
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequiredUnauthorizedGuard(routeStub, stateStub)));
84
+ expect(result).toBe(false);
85
+ expect(setDirection).toHaveBeenCalledWith('root');
86
+ expect(navigate).toHaveBeenCalledWith([REDIRECTS.whenAuthorized]);
87
+ });
88
+
89
+ it("'confirm' → navigates whenConfirming and returns false (no setDirection)", async () => {
90
+ const { navigate, setDirection } = setup('confirm');
91
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequiredUnauthorizedGuard(routeStub, stateStub)));
92
+ expect(result).toBe(false);
93
+ expect(setDirection).not.toHaveBeenCalled();
94
+ expect(navigate).toHaveBeenCalledWith([REDIRECTS.whenConfirming]);
95
+ });
96
+
97
+ it("'required' → returns true", async () => {
98
+ setup('required');
99
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequiredUnauthorizedGuard(routeStub, stateStub)));
100
+ expect(result).toBe(true);
101
+ });
102
+
103
+ it("'anonymous' → returns true", async () => {
104
+ setup('anonymous');
105
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequiredUnauthorizedGuard(routeStub, stateStub)));
106
+ expect(result).toBe(true);
107
+ });
108
+ });
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // kitRequireConfirmingGuard
112
+ // ---------------------------------------------------------------------------
113
+ describe('kitRequireConfirmingGuard', () => {
114
+ afterEach(() => TestBed.resetTestingModule());
115
+
116
+ it("'confirm' → returns true", async () => {
117
+ setup('confirm');
118
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequireConfirmingGuard(routeStub, stateStub)));
119
+ expect(result).toBe(true);
120
+ });
121
+
122
+ it("'anonymous' → navigates whenAuthorized and returns false", async () => {
123
+ const { navigate, setDirection } = setup('anonymous');
124
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequireConfirmingGuard(routeStub, stateStub)));
125
+ expect(result).toBe(false);
126
+ expect(setDirection).toHaveBeenCalledWith('root');
127
+ expect(navigate).toHaveBeenCalledWith([REDIRECTS.whenAuthorized]);
128
+ });
129
+
130
+ it("'required' → navigates whenNotConfirming and returns false", async () => {
131
+ const { navigate, setDirection } = setup('required');
132
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequireConfirmingGuard(routeStub, stateStub)));
133
+ expect(result).toBe(false);
134
+ expect(setDirection).toHaveBeenCalledWith('root');
135
+ expect(navigate).toHaveBeenCalledWith([REDIRECTS.whenNotConfirming]);
136
+ });
137
+
138
+ it("'user' → navigates whenNotConfirming and returns false", async () => {
139
+ const { navigate, setDirection } = setup('user');
140
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequireConfirmingGuard(routeStub, stateStub)));
141
+ expect(result).toBe(false);
142
+ expect(setDirection).toHaveBeenCalledWith('root');
143
+ expect(navigate).toHaveBeenCalledWith([REDIRECTS.whenNotConfirming]);
144
+ });
145
+ });
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // kitRequireAuthorizedGuard
149
+ // ---------------------------------------------------------------------------
150
+ describe('kitRequireAuthorizedGuard', () => {
151
+ afterEach(() => TestBed.resetTestingModule());
152
+
153
+ it("'user' → calls onAuthorized with state and returns its value (true)", async () => {
154
+ const onAuthorized = vi.fn().mockResolvedValue(true) as unknown as (s: RouterStateSnapshot) => Promise<boolean | UrlTree>;
155
+ setup('user', { onAuthorized });
156
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequireAuthorizedGuard(routeStub, stateStub)));
157
+ expect(result).toBe(true);
158
+ expect(onAuthorized).toHaveBeenCalledWith(stateStub);
159
+ });
160
+
161
+ it("'user' → propagates UrlTree from onAuthorized", async () => {
162
+ const urlTree = { queryParams: {} } as unknown as UrlTree;
163
+ const onAuthorized = vi.fn().mockResolvedValue(urlTree) as unknown as (s: RouterStateSnapshot) => Promise<boolean | UrlTree>;
164
+ setup('user', { onAuthorized });
165
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequireAuthorizedGuard(routeStub, stateStub)));
166
+ expect(result).toBe(urlTree);
167
+ });
168
+
169
+ it("'anonymous' → returns true without calling any hook", async () => {
170
+ const onAuthorized = vi.fn() as unknown as (s: RouterStateSnapshot) => Promise<boolean | UrlTree>;
171
+ const onUnauthenticated = vi.fn() as unknown as (s: RouterStateSnapshot) => Promise<boolean | UrlTree>;
172
+ setup('anonymous', { onAuthorized, onUnauthenticated });
173
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequireAuthorizedGuard(routeStub, stateStub)));
174
+ expect(result).toBe(true);
175
+ expect(onAuthorized).not.toHaveBeenCalled();
176
+ expect(onUnauthenticated).not.toHaveBeenCalled();
177
+ });
178
+
179
+ it("'required' + onUnauthenticated → true → returns true (fallback allows)", async () => {
180
+ const onUnauthenticated = vi.fn().mockResolvedValue(true) as unknown as (s: RouterStateSnapshot) => Promise<boolean | UrlTree>;
181
+ setup('required', { onUnauthenticated });
182
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequireAuthorizedGuard(routeStub, stateStub)));
183
+ expect(result).toBe(true);
184
+ });
185
+
186
+ it("'required' + onUnauthenticated → UrlTree → passes UrlTree through", async () => {
187
+ const urlTree = { queryParams: {} } as unknown as UrlTree;
188
+ const onUnauthenticated = vi.fn().mockResolvedValue(urlTree) as unknown as (s: RouterStateSnapshot) => Promise<boolean | UrlTree>;
189
+ setup('required', { onUnauthenticated });
190
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequireAuthorizedGuard(routeStub, stateStub)));
191
+ expect(result).toBe(urlTree);
192
+ });
193
+
194
+ it("'required' + onUnauthenticated → false → navigates whenUnauthorized and returns false", async () => {
195
+ const onUnauthenticated = vi.fn().mockResolvedValue(false) as unknown as (s: RouterStateSnapshot) => Promise<boolean | UrlTree>;
196
+ const { navigate, setDirection } = setup('required', { onUnauthenticated });
197
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequireAuthorizedGuard(routeStub, stateStub)));
198
+ expect(result).toBe(false);
199
+ expect(setDirection).toHaveBeenCalledWith('root');
200
+ expect(navigate).toHaveBeenCalledWith([REDIRECTS.whenUnauthorized]);
201
+ });
202
+
203
+ it("'confirm' + onUnauthenticated → false → navigates whenUnauthorized and returns false", async () => {
204
+ const onUnauthenticated = vi.fn().mockResolvedValue(false) as unknown as (s: RouterStateSnapshot) => Promise<boolean | UrlTree>;
205
+ const { navigate } = setup('confirm', { onUnauthenticated });
206
+ const result = await runGuard(TestBed.runInInjectionContext(() => kitRequireAuthorizedGuard(routeStub, stateStub)));
207
+ expect(result).toBe(false);
208
+ expect(navigate).toHaveBeenCalledWith([REDIRECTS.whenUnauthorized]);
209
+ });
210
+ });
@@ -0,0 +1,227 @@
1
+ import type { EnvironmentProviders } from '@angular/core';
2
+ import { inject, InjectionToken, makeEnvironmentProviders } from '@angular/core';
3
+ import type { CanActivateFn, RouterStateSnapshot, UrlTree } from '@angular/router';
4
+ import { Router } from '@angular/router';
5
+ import { NavController } from '@ionic/angular/standalone';
6
+ import type { Observable } from 'rxjs';
7
+ import { map, mergeMap } from 'rxjs/operators';
8
+
9
+ /**
10
+ * Discriminated set of authentication states the guards react to.
11
+ *
12
+ * @remarks
13
+ * The application is responsible for emitting these values through {@link KitAuthConfig.authState}.
14
+ * An application that does not use a value (for example email confirmation) simply never emits it.
15
+ *
16
+ * - `user` — fully authenticated and verified.
17
+ * - `confirm` — awaiting email confirmation.
18
+ * - `required` — not authenticated.
19
+ * - `anonymous` — signed in anonymously; the user can still be guided toward full registration.
20
+ */
21
+ export type KitAuthState = 'user' | 'confirm' | 'required' | 'anonymous';
22
+
23
+ /**
24
+ * Redirect targets (route paths) used by the guards when access is denied.
25
+ *
26
+ * @remarks
27
+ * Every field is required and must be provided per application, because the guards have no
28
+ * knowledge of the host application's route layout.
29
+ */
30
+ export interface KitAuthRedirects {
31
+ /** Used by {@link kitRequiredUnauthorizedGuard}: where to navigate when the user is already authenticated (`user`). */
32
+ readonly whenAuthorized: string;
33
+ /** Used by {@link kitRequiredUnauthorizedGuard}: where to navigate when the user is awaiting email confirmation (`confirm`). */
34
+ readonly whenConfirming: string;
35
+ /** Used by {@link kitRequireConfirmingGuard}: where to navigate when the state is not `confirm`. */
36
+ readonly whenNotConfirming: string;
37
+ /** Used by {@link kitRequireAuthorizedGuard}: where to navigate when the state is not `user` and the fallback is not allowed. */
38
+ readonly whenUnauthorized: string;
39
+ }
40
+
41
+ /**
42
+ * Configuration consumed by the authentication guards, injected through {@link provideKitAuth}.
43
+ *
44
+ * @remarks
45
+ * All members are required. The hooks let the host application plug its own auth service and
46
+ * navigation policy into the otherwise fixed guard control flow.
47
+ */
48
+ export interface KitAuthConfig {
49
+ /**
50
+ * Source of the current authentication state.
51
+ *
52
+ * @remarks
53
+ * Typically backed by the application's own auth service (for example `AuthService.isAuth()`).
54
+ *
55
+ * @returns A stream of {@link KitAuthState} values.
56
+ */
57
+ authState(): Observable<KitAuthState>;
58
+ /**
59
+ * Application-specific work that runs in {@link kitRequireAuthorizedGuard} after the state is confirmed to be `user`.
60
+ *
61
+ * @remarks
62
+ * Typical responsibilities include token login, permission checks, terms-of-service acceptance,
63
+ * or restoring a previously requested redirect.
64
+ *
65
+ * @param state - The router state snapshot of the route being activated.
66
+ * @returns `true` to allow activation, or a `UrlTree` to perform a custom redirect.
67
+ */
68
+ onAuthorized(state: RouterStateSnapshot): Promise<boolean | UrlTree>;
69
+ /**
70
+ * Fallback that runs in {@link kitRequireAuthorizedGuard} when the state is `required` (not authenticated).
71
+ *
72
+ * @remarks
73
+ * For example, attempt an anonymous sign-in and allow the route. Applications that do not need
74
+ * this should pass `async () => false` to fall through to the default redirect.
75
+ *
76
+ * @param state - The router state snapshot of the route being activated.
77
+ * @returns `true` to allow activation, a `UrlTree` for a custom redirect, or `false` to use the default redirect.
78
+ */
79
+ onUnauthenticated(state: RouterStateSnapshot): Promise<boolean | UrlTree>;
80
+ /** Redirect targets used by the guards. */
81
+ redirects: KitAuthRedirects;
82
+ }
83
+
84
+ /**
85
+ * Injection token that carries the {@link KitAuthConfig} to the authentication guards.
86
+ */
87
+ export const KIT_AUTH_CONFIG = new InjectionToken<KitAuthConfig>('@rdlabo/ionic-angular-kit:auth');
88
+
89
+ /**
90
+ * Wire the authentication guard configuration into the application's dependency injection.
91
+ *
92
+ * @remarks
93
+ * The factory runs inside an injection context, so it may call `inject()` (for example
94
+ * `inject(AuthService)`) to build the configuration.
95
+ *
96
+ * @param configFactory - Factory that returns the {@link KitAuthConfig} for the application.
97
+ * @returns Environment providers to add to the application bootstrap.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * provideKitAuth(() => {
102
+ * const auth = inject(AuthService);
103
+ * return {
104
+ * authState: () => auth.isAuth(),
105
+ * onAuthorized: async () => true,
106
+ * onUnauthenticated: async () => false,
107
+ * redirects: {
108
+ * whenAuthorized: '/',
109
+ * whenConfirming: '/auth/confirm',
110
+ * whenNotConfirming: '/auth/signin',
111
+ * whenUnauthorized: 'auth',
112
+ * },
113
+ * };
114
+ * });
115
+ * ```
116
+ */
117
+ export const provideKitAuth = (configFactory: () => KitAuthConfig): EnvironmentProviders =>
118
+ makeEnvironmentProviders([{ provide: KIT_AUTH_CONFIG, useFactory: configFactory }]);
119
+
120
+ /**
121
+ * Guard that requires the user to be unauthenticated (for example sign-in or sign-up pages).
122
+ *
123
+ * @remarks
124
+ * Allows the `required` and `anonymous` states (an anonymous user is permitted to proceed to a
125
+ * registration page). An authenticated user (`user`) is sent to `whenAuthorized`, and a user
126
+ * awaiting confirmation (`confirm`) is sent to `whenConfirming`.
127
+ *
128
+ * @returns A stream emitting `true` to allow activation, or `false` after triggering a redirect.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * const routes: Routes = [{ path: 'signin', component: SigninPage, canActivate: [kitRequiredUnauthorizedGuard] }];
133
+ * ```
134
+ */
135
+ export const kitRequiredUnauthorizedGuard: CanActivateFn = () => {
136
+ const { authState, redirects } = inject(KIT_AUTH_CONFIG);
137
+ const router = inject(Router);
138
+ const navCtrl = inject(NavController);
139
+
140
+ return authState().pipe(
141
+ map((data) => {
142
+ if (data === 'user') {
143
+ navCtrl.setDirection('root');
144
+ router.navigate([redirects.whenAuthorized]);
145
+ return false;
146
+ } else if (data === 'confirm') {
147
+ router.navigate([redirects.whenConfirming]);
148
+ return false;
149
+ }
150
+ // 'required' | 'anonymous'
151
+ return true;
152
+ }),
153
+ );
154
+ };
155
+
156
+ /**
157
+ * Guard that requires the user to be awaiting email confirmation (`confirm`).
158
+ *
159
+ * @remarks
160
+ * Any other state triggers a redirect: an `anonymous` user is sent to the authenticated area
161
+ * (`whenAuthorized`), and every remaining state is sent to `whenNotConfirming`.
162
+ *
163
+ * @returns A stream emitting `true` to allow activation, or `false` after triggering a redirect.
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * const routes: Routes = [{ path: 'confirm', component: ConfirmPage, canActivate: [kitRequireConfirmingGuard] }];
168
+ * ```
169
+ */
170
+ export const kitRequireConfirmingGuard: CanActivateFn = () => {
171
+ const { authState, redirects } = inject(KIT_AUTH_CONFIG);
172
+ const router = inject(Router);
173
+ const navCtrl = inject(NavController);
174
+
175
+ return authState().pipe(
176
+ map((data) => {
177
+ if (data === 'confirm') {
178
+ return true;
179
+ }
180
+ navCtrl.setDirection('root');
181
+ router.navigate([data === 'anonymous' ? redirects.whenAuthorized : redirects.whenNotConfirming]);
182
+ return false;
183
+ }),
184
+ );
185
+ };
186
+
187
+ /**
188
+ * Guard that requires the user to be fully authenticated (`user`).
189
+ *
190
+ * @remarks
191
+ * - `user` — runs {@link KitAuthConfig.onAuthorized} (token login, permission checks, and so on).
192
+ * - `anonymous` — allowed as-is, for applications that permit anonymous browsing.
193
+ * - `required` / `confirm` — runs {@link KitAuthConfig.onUnauthenticated}; if it resolves to `false`,
194
+ * the user is redirected to `whenUnauthorized`.
195
+ *
196
+ * @param _route - The activated route snapshot (unused).
197
+ * @param state - The router state snapshot, forwarded to the configuration hooks.
198
+ * @returns A stream emitting the activation result: `true`, a `UrlTree`, or `false` after a redirect.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * const routes: Routes = [{ path: 'home', component: HomePage, canActivate: [kitRequireAuthorizedGuard] }];
203
+ * ```
204
+ */
205
+ export const kitRequireAuthorizedGuard: CanActivateFn = (_route, state) => {
206
+ const { authState, onAuthorized, onUnauthenticated, redirects } = inject(KIT_AUTH_CONFIG);
207
+ const router = inject(Router);
208
+ const navCtrl = inject(NavController);
209
+
210
+ return authState().pipe(
211
+ mergeMap(async (data) => {
212
+ if (data === 'user') {
213
+ return onAuthorized(state);
214
+ }
215
+ if (data === 'anonymous') {
216
+ return true;
217
+ }
218
+ const fallback = await onUnauthenticated(state);
219
+ if (fallback !== false) {
220
+ return fallback;
221
+ }
222
+ navCtrl.setDirection('root');
223
+ router.navigate([redirects.whenUnauthorized]);
224
+ return false;
225
+ }),
226
+ );
227
+ };
@@ -0,0 +1,68 @@
1
+ import type { OnInit } from '@angular/core';
2
+ import { Directive, ElementRef, inject } from '@angular/core';
3
+ import { Capacitor } from '@capacitor/core';
4
+
5
+ /**
6
+ * Work around iOS `ion-input` autofill values not propagating to the Angular form model.
7
+ *
8
+ * On iOS, when the browser autofills an `ion-input` (for example a saved password), the value
9
+ * is written to the underlying native `<input>` element but the corresponding `change` event is
10
+ * not forwarded to the host `ion-input`, so the Angular form control (and `ngModel`) never sees
11
+ * the autofilled value. This directive listens for the first `change` event on the inner input
12
+ * element and mirrors its value back onto the host element, restoring two-way binding.
13
+ *
14
+ * Apply it to any `ion-input` that participates in a form and may be autofilled by attaching the
15
+ * `rdlaboAutofill` attribute.
16
+ *
17
+ * @remarks
18
+ * The directive is a no-op on every platform other than iOS, where the value already propagates
19
+ * correctly. A short timeout is used because `ion-input` creates its inner `<input>` element
20
+ * asynchronously after the host element is initialized.
21
+ *
22
+ * @example
23
+ * ```html
24
+ * <ion-input rdlaboAutofill type="password" [(ngModel)]="password"></ion-input>
25
+ * ```
26
+ */
27
+ @Directive({
28
+ selector: '[rdlaboAutofill]',
29
+ standalone: true,
30
+ })
31
+ export class KitAutofillDirective implements OnInit {
32
+ readonly #el = inject(ElementRef);
33
+
34
+ constructor() {}
35
+
36
+ /**
37
+ * Register the iOS autofill workaround once the directive is initialized.
38
+ *
39
+ * Returns immediately on non-iOS platforms. On iOS, after a short delay it attaches a one-shot,
40
+ * passive `change` listener to the inner `<input>` element that `ion-input` renders, copying the
41
+ * autofilled value back onto the host element so the Angular form model stays in sync. Any error
42
+ * while locating the inner input (for example if the element is not yet present) is swallowed.
43
+ *
44
+ * @returns Nothing.
45
+ */
46
+ ngOnInit(): void {
47
+ if (Capacitor.getPlatform() !== 'ios') {
48
+ return;
49
+ }
50
+ setTimeout(() => {
51
+ try {
52
+ this.#el.nativeElement.children[0].addEventListener(
53
+ 'change',
54
+ (e: Event) => {
55
+ this.#el.nativeElement.value = (e.target as HTMLInputElement).value;
56
+ },
57
+ {
58
+ capture: false,
59
+ once: true,
60
+ passive: true,
61
+ },
62
+ );
63
+ } catch {
64
+ /* empty */
65
+ }
66
+ }, 100); // Need some time for the ion-input to create the input element
67
+ }
68
+ }