@rdlabo/ionic-angular-kit 0.0.1 → 0.0.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.
package/README.md CHANGED
@@ -265,31 +265,34 @@ A fleet-canonical HTTP interceptor with:
265
265
 
266
266
  **Convention:** all app-specific logic (auth headers, error UI) lives in the config factory. The retry policy, bypass evaluation, and error dispatch are fixed in the kit and not overridable per-call.
267
267
 
268
+ **Only `getAuthHeaders` is required.** Every other hook is optional and defaults to a safe no-op (`buildExtraHeaders` → `{}`, `bypass` → `false`, `offlineFallback` → `null`), so a config specifies only the behavior that actually differs from the baseline.
269
+
268
270
  **Setup**
269
271
 
270
272
  ```typescript
271
273
  // app.config.ts
272
274
  import { provideHttpClient, withInterceptors } from '@angular/common/http';
273
- import { kitAuthInterceptor, provideKitHttp } from '@rdlabo/ionic-angular-kit';
275
+ import { kitAuthInterceptor, provideKitHttp, kitPresentReloadAlert } from '@rdlabo/ionic-angular-kit';
274
276
 
275
277
  export const appConfig: ApplicationConfig = {
276
278
  providers: [
277
279
  provideHttpClient(withInterceptors([kitAuthInterceptor])),
278
280
  provideKitHttp(() => {
279
281
  const auth = inject(AuthService);
280
- const router = inject(Router);
281
- const toast = inject(KitOverlayController);
282
+ const overlay = inject(KitOverlayController);
282
283
  return {
283
- bypass: (req) => req.url.startsWith('https://cdn.example.com'),
284
284
  getAuthHeaders: async (req) => ({
285
285
  Authorization: `Bearer ${await auth.getToken()}`,
286
286
  }),
287
- buildExtraHeaders: (req) => ({ 'X-App-Version': '1.0.0' }),
288
- offlineFallback: (req, err) => null, // no offline queue
289
287
  onUnauthorized: (req) => auth.signOut(),
290
- onForbidden: (req) => router.navigate(['/403']),
291
- onNetworkError: (status) => toast.presentToast({ message: 'Network error' }),
292
- onServerError: (message) => toast.presentToast({ message }),
288
+ // Fleet-canonical "network error → offer reload" (see kitPresentReloadAlert).
289
+ onNetworkError: (status) =>
290
+ kitPresentReloadAlert(overlay, {
291
+ header: 'ネットワークエラー',
292
+ message: `通信できませんでした。リフレッシュしますか?(${status})`,
293
+ okText: 'リフレッシュ',
294
+ }),
295
+ // buildExtraHeaders / bypass / offlineFallback / onForbidden / onServerError omitted → kit defaults.
293
296
  };
294
297
  }),
295
298
  ],
@@ -303,6 +306,20 @@ export const appConfig: ApplicationConfig = {
303
306
  4. Non-400/500 status AND device connected → `onNetworkError`
304
307
  5. 400 or 500 with `error.message` → `onServerError`
305
308
 
309
+ ### kitPresentReloadAlert
310
+
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).
312
+
313
+ ```typescript
314
+ import { kitPresentReloadAlert } from '@rdlabo/ionic-angular-kit';
315
+
316
+ await kitPresentReloadAlert(overlay, {
317
+ header: 'ネットワークエラー',
318
+ message: `通信できませんでした。リフレッシュしますか?(${status})`,
319
+ okText: 'リフレッシュ',
320
+ });
321
+ ```
322
+
306
323
  ---
307
324
 
308
325
  ### KitAutofillDirective
@@ -302,6 +302,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
302
302
  }]
303
303
  }] });
304
304
 
305
+ /**
306
+ * Present the fleet's canonical "network error → offer to reload" confirmation.
307
+ *
308
+ * @remarks
309
+ * Folds together the three copy-pasted concerns that every app duplicated around a network failure:
310
+ *
311
+ * 1. Suppress stacking — if an `ion-alert` is already on screen, do nothing.
312
+ * 2. Ask the user to confirm via {@link KitOverlayController.alertConfirm}.
313
+ * 3. Call `location.reload()` when the user confirms.
314
+ *
315
+ * All user-facing text (`header`, `message`, `okText`) is supplied by the caller so the kit stays
316
+ * free of any hardcoded i18n. Typically wired from {@link KitHttpConfig.onNetworkError}, but it can
317
+ * be called from anywhere an app wants the same behavior (e.g. an offline fallback).
318
+ *
319
+ * @param overlay - the kit overlay controller used to present the confirmation
320
+ * @param options - alert content plus the confirm-button text (usually "Refresh")
321
+ * @returns a Promise that resolves once the alert is dismissed (the page reloads on confirm)
322
+ * @example
323
+ * ```ts
324
+ * provideKitHttp(() => {
325
+ * const overlay = inject(KitOverlayController);
326
+ * return {
327
+ * getAuthHeaders: async () => ({ ... }),
328
+ * onNetworkError: (status) =>
329
+ * kitPresentReloadAlert(overlay, {
330
+ * header: 'ネットワークエラー',
331
+ * message: `通信できませんでした。リフレッシュしますか?(${status})`,
332
+ * okText: 'リフレッシュ',
333
+ * }),
334
+ * };
335
+ * });
336
+ * ```
337
+ */
338
+ const kitPresentReloadAlert = async (overlay, options) => {
339
+ // 既にアラート表示中なら多重表示しない。
340
+ if (document.querySelector('ion-alert')) {
341
+ return;
342
+ }
343
+ const reload = await overlay.alertConfirm(options);
344
+ if (reload) {
345
+ location.reload();
346
+ }
347
+ };
348
+
305
349
  /**
306
350
  * Work around iOS `ion-input` autofill values not propagating to the Angular form model.
307
351
  *
@@ -526,16 +570,13 @@ const KIT_HTTP_CONFIG = new InjectionToken('@rdlabo/ionic-angular-kit:http');
526
570
  * provideHttpClient(withInterceptors([kitAuthInterceptor])),
527
571
  * provideKitHttp(() => {
528
572
  * const auth = inject(AuthService);
573
+ * const overlay = inject(KitOverlayController);
529
574
  * return {
575
+ * // Only getAuthHeaders is required; every other hook is optional and defaults to a no-op.
530
576
  * getAuthHeaders: async () => ({ Authorization: `Bearer ${await auth.token()}` }),
531
- * buildExtraHeaders: () => ({}),
532
- * onResponse: () => {},
533
- * bypass: (request) => request.url.startsWith('https://cdn.example.com'),
534
- * offlineFallback: () => null,
535
577
  * onUnauthorized: () => auth.signOut(),
536
- * onForbidden: () => {},
537
- * onNetworkError: () => {},
538
- * onServerError: (message) => console.error(message),
578
+ * onNetworkError: (status) =>
579
+ * kitPresentReloadAlert(overlay, { header: 'Network error', message: `Reload? (${status})`, okText: 'Reload' }),
539
580
  * };
540
581
  * }),
541
582
  * ],
@@ -570,11 +611,11 @@ const provideKitHttp = (configFactory) => makeEnvironmentProviders([{ provide: K
570
611
  */
571
612
  const kitAuthInterceptor = (request, next) => {
572
613
  const config = inject(KIT_HTTP_CONFIG);
573
- if (config.bypass(request)) {
614
+ if (config.bypass?.(request)) {
574
615
  return next(request);
575
616
  }
576
617
  return from(config.getAuthHeaders(request)).pipe(mergeMap((authHeaders) => {
577
- const req = request.clone({ setHeaders: { ...authHeaders, ...config.buildExtraHeaders(request) } });
618
+ const req = request.clone({ setHeaders: { ...authHeaders, ...config.buildExtraHeaders?.(request) } });
578
619
  return next(req).pipe(retry({
579
620
  count: 2,
580
621
  delay: (e, retryCount) => {
@@ -588,28 +629,28 @@ const kitAuthInterceptor = (request, next) => {
588
629
  },
589
630
  }), tap((event) => {
590
631
  if (event instanceof HttpResponse) {
591
- config.onResponse(event);
632
+ config.onResponse?.(event);
592
633
  }
593
634
  }), catchError((e) => {
594
- const fallback = config.offlineFallback(req, e);
635
+ const fallback = config.offlineFallback?.(req, e);
595
636
  if (fallback) {
596
637
  return fallback;
597
638
  }
598
639
  if (e.status === 401) {
599
- config.onUnauthorized(req);
640
+ config.onUnauthorized?.(req);
600
641
  }
601
642
  else if (e.status === 403) {
602
- config.onForbidden(req);
643
+ config.onForbidden?.(req);
603
644
  }
604
645
  else if (![400, 500].includes(e.status)) {
605
646
  void Network.getStatus().then((status) => {
606
647
  if (status.connected) {
607
- config.onNetworkError(e.status);
648
+ config.onNetworkError?.(e.status);
608
649
  }
609
650
  });
610
651
  }
611
652
  else if (e.error?.message) {
612
- config.onServerError(e.error.message);
653
+ config.onServerError?.(e.error.message);
613
654
  }
614
655
  return throwError(() => e);
615
656
  }));
@@ -705,5 +746,5 @@ const arrayConcatById = (arrayOld, arrayNew, key, order = 'DESC') => {
705
746
  * Generated bundle index. Do not edit.
706
747
  */
707
748
 
708
- export { KIT_AUTH_CONFIG, KIT_HTTP_CONFIG, KIT_OVERLAY_CONFIG, KitAutofillDirective, KitOverlayController, KitStorageService, arrayConcatById, kitAuthInterceptor, kitImpact, kitRequireAuthorizedGuard, kitRequireConfirmingGuard, kitRequiredUnauthorizedGuard, provideKitAuth, provideKitHttp, provideKitOverlay };
749
+ export { KIT_AUTH_CONFIG, KIT_HTTP_CONFIG, KIT_OVERLAY_CONFIG, KitAutofillDirective, KitOverlayController, KitStorageService, arrayConcatById, kitAuthInterceptor, kitImpact, kitPresentReloadAlert, kitRequireAuthorizedGuard, kitRequireConfirmingGuard, kitRequiredUnauthorizedGuard, provideKitAuth, provideKitHttp, provideKitOverlay };
709
750
  //# sourceMappingURL=rdlabo-ionic-angular-kit.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rdlabo-ionic-angular-kit.mjs","sources":["../../../projects/kit/src/lib/storage/kit-storage.service.ts","../../../projects/kit/src/lib/overlay/overlay-config.ts","../../../projects/kit/src/lib/utils/haptics.ts","../../../projects/kit/src/lib/overlay/kit-overlay.controller.ts","../../../projects/kit/src/lib/directives/autofill.directive.ts","../../../projects/kit/src/lib/auth/auth-guards.ts","../../../projects/kit/src/lib/http/kit-http.interceptor.ts","../../../projects/kit/src/lib/utils/array.ts","../../../projects/kit/src/public-api.ts","../../../projects/kit/src/rdlabo-ionic-angular-kit.ts"],"sourcesContent":["import { inject, Injectable } from '@angular/core';\nimport { Storage } from '@ionic/storage-angular';\n\n/**\n * Thin, typed wrapper around `@ionic/storage-angular`.\n *\n * Starts `create()` exactly once and stores the resulting ready Promise. Every operation awaits\n * that Promise before touching the underlying store, so calls made before initialization completes\n * are queued rather than dropped.\n *\n * @remarks\n * A naive wrapper that reads the store synchronously would silently no-op (or throw) when invoked\n * before `create()` resolves, losing early writes. Awaiting the one-time ready Promise on every\n * operation removes that race without forcing callers to coordinate initialization themselves.\n *\n * @example\n * ```ts\n * constructor(private readonly storage: KitStorageService) {}\n *\n * async ngOnInit(): Promise<void> {\n * await this.storage.set('token', 'abc123');\n * const token = await this.storage.get<string>('token');\n * }\n * ```\n */\n@Injectable({\n providedIn: 'root',\n})\nexport class KitStorageService {\n /** One-time `create()` ready Promise; awaited before every operation so early calls are not lost. */\n readonly #ready: Promise<Storage> = inject(Storage).create();\n\n /**\n * Persist a value under the given key.\n *\n * @typeParam T - type of the value being stored\n * @param key - key to store the value under\n * @param value - value to persist; overwrites any existing value for the key\n * @returns a Promise that resolves once the value has been written\n * @example\n * ```ts\n * await storage.set('user', { id: 1, name: 'Ada' });\n * ```\n */\n async set<T>(key: string, value: T): Promise<void> {\n await (await this.#ready).set(key, value);\n }\n\n /**\n * Read the value stored under the given key.\n *\n * @typeParam T - expected type of the stored value\n * @param key - key to read\n * @returns the stored value, or `null` when the key is absent\n * @example\n * ```ts\n * const user = await storage.get<{ id: number }>('user');\n * ```\n */\n async get<T>(key: string): Promise<T | null> {\n return (await (await this.#ready).get(key)) ?? null;\n }\n\n /**\n * Remove the value stored under the given key.\n *\n * @param key - key to remove; a no-op when the key is absent\n * @returns a Promise that resolves once the key has been removed\n */\n async remove(key: string): Promise<void> {\n await (await this.#ready).remove(key);\n }\n\n /**\n * Remove every key/value pair from the store.\n *\n * @returns a Promise that resolves once the store has been emptied\n */\n async clear(): Promise<void> {\n await (await this.#ready).clear();\n }\n\n /**\n * List every key currently present in the store.\n *\n * @returns an array of all stored keys\n */\n async keys(): Promise<string[]> {\n return (await this.#ready).keys();\n }\n}\n","import type { EnvironmentProviders } from '@angular/core';\nimport { InjectionToken, makeEnvironmentProviders } from '@angular/core';\n\n/**\n * User-visible button labels consumed by `KitOverlayController`.\n *\n * @remarks\n * The kit deliberately ships no i18n strings of its own and has no hard dependency on\n * `@angular/localize`. The consuming application always provides these labels: multilingual apps\n * pass `$localize`-resolved strings, single-language apps pass plain literals.\n */\nexport interface KitLabels {\n /** Text for the \"close\" button used by alerts and toasts. */\n readonly close: string;\n /** Text for the \"cancel\" button used by confirmation alerts. */\n readonly cancel: string;\n}\n\n/**\n * Overlay configuration injected via `provideKitOverlay()`.\n *\n * @remarks\n * All fields are required; the consuming application must supply a complete configuration.\n */\nexport interface KitOverlayConfig {\n /** Button labels used across the overlays presented by `KitOverlayController`. */\n readonly labels: KitLabels;\n}\n\n/**\n * Injection token carrying the {@link KitOverlayConfig} for `KitOverlayController`.\n *\n * @remarks\n * Provide it through {@link provideKitOverlay} rather than registering it directly.\n */\nexport const KIT_OVERLAY_CONFIG = new InjectionToken<KitOverlayConfig>('@rdlabo/ionic-angular-kit:overlay');\n\n/**\n * Wire `KitOverlayController` into the application by providing its button labels.\n *\n * @param config - overlay configuration, including the button labels to inject\n * @returns environment providers to add to the application's provider list\n * @example\n * ```ts\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideKitOverlay({ labels: { close: $localize`Close`, cancel: $localize`Cancel` } }),\n * ],\n * });\n * ```\n */\nexport const provideKitOverlay = (config: KitOverlayConfig): EnvironmentProviders =>\n makeEnvironmentProviders([{ provide: KIT_OVERLAY_CONFIG, useValue: config }]);\n","import { Capacitor } from '@capacitor/core';\nimport { Haptics, ImpactStyle } from '@capacitor/haptics';\n\n/**\n * Trigger native haptic impact feedback.\n *\n * Delegates to `@capacitor/haptics` `Haptics.impact()` when running on a native platform. On the\n * web (or any non-native platform) it is a no-op and resolves without doing anything.\n *\n * @remarks\n * This haptic side effect was previously bundled implicitly into toast/modal presentation. It has\n * been decoupled into this explicit function so callers opt in to the feedback deliberately, rather\n * than receiving it as a hidden side effect of presenting UI.\n *\n * @param style - The impact intensity to play. Defaults to {@link ImpactStyle.Light}.\n * @returns A promise that resolves once the feedback has been requested (immediately on the web).\n * @example\n * ```ts\n * import { ImpactStyle } from '@capacitor/haptics';\n *\n * // Light tap (default)\n * await kitImpact();\n *\n * // Stronger feedback, e.g. on a confirming action\n * await kitImpact(ImpactStyle.Heavy);\n * ```\n */\nexport const kitImpact = async (style: ImpactStyle = ImpactStyle.Light): Promise<void> => {\n if (Capacitor.isNativePlatform()) {\n await Haptics.impact({ style });\n }\n};\n","import { inject, Injectable } from '@angular/core';\nimport type { ModalOptions, ToastOptions } from '@ionic/angular/standalone';\nimport { AlertController, ModalController, ToastController } from '@ionic/angular/standalone';\nimport type { PluginListenerHandle } from '@capacitor/core';\nimport { Capacitor } from '@capacitor/core';\nimport { Keyboard } from '@capacitor/keyboard';\nimport { KIT_OVERLAY_CONFIG } from './overlay-config';\nimport { kitImpact } from '../utils/haptics';\n\n/**\n * Options for {@link KitOverlayController.presentModal}.\n *\n * @remarks\n * Extends Ionic's `ModalOptions` but omits `component` and `componentProps`, which are passed as\n * dedicated arguments instead.\n */\nexport interface KitModalPresentOptions extends Omit<ModalOptions, 'component' | 'componentProps'> {\n /**\n * When `true`, expand the sheet to its maximum breakpoint while the native keyboard is shown.\n *\n * @remarks\n * Only has an effect on native platforms; ignored on the web.\n */\n watchKeyboard?: boolean;\n}\n\n/**\n * Options for {@link KitOverlayController.alertClose}.\n */\nexport interface KitAlertCloseOptions {\n /** Alert header text. */\n header: string;\n /** Alert body message. */\n message: string;\n /** Optional alert sub-header text shown beneath the header. */\n subHeader?: string;\n}\n\n/**\n * Options for {@link KitOverlayController.alertConfirm}.\n *\n * @remarks\n * Extends {@link KitAlertCloseOptions} with the confirm-button text.\n */\nexport interface KitAlertConfirmOptions extends KitAlertCloseOptions {\n /**\n * Text for the OK (confirm) button.\n *\n * @remarks\n * Action-specific, so it is supplied by the caller rather than taken from the shared labels.\n */\n okText: string;\n}\n\n/**\n * Attach a native keyboard listener that grows the modal to its maximum breakpoint when the\n * keyboard appears.\n *\n * @param modal - the presented modal element to resize\n * @returns a listener handle; on non-native platforms a no-op handle whose `remove()` does nothing\n * @internal\n */\nconst watchModalKeyboard = async (modal: HTMLIonModalElement): Promise<PluginListenerHandle> => {\n if (!Capacitor.isNativePlatform()) {\n return { remove: async () => undefined };\n }\n return Keyboard.addListener('keyboardDidShow', () => modal.setCurrentBreakpoint(1));\n};\n\n/**\n * Ergonomic wrapper that consolidates Ionic's overlay controllers (Modal / Toast / Alert).\n *\n * @remarks\n * Folds the repetitive create → present → onDidDismiss sequence into single calls and returns the\n * relevant result directly. It holds no application-specific policy such as navigation; compose\n * those concerns on the consuming side.\n *\n * @example\n * ```ts\n * constructor(private readonly overlay: KitOverlayController) {}\n *\n * async edit(): Promise<void> {\n * const result = await this.overlay.presentModal<EditResult>(EditPage, { id: 1 });\n * if (result) {\n * await this.overlay.presentToast({ message: 'Saved' });\n * }\n * }\n * ```\n */\n@Injectable({\n providedIn: 'root',\n})\nexport class KitOverlayController {\n readonly #modalCtrl = inject(ModalController);\n readonly #toastCtrl = inject(ToastController);\n readonly #alertCtrl = inject(AlertController);\n readonly #labels = inject(KIT_OVERLAY_CONFIG).labels;\n\n /**\n * Present a modal and resolve with the data passed to its dismissal.\n *\n * @typeParam O - type of the data returned when the modal is dismissed\n * @param component - the component to render inside the modal\n * @param componentProps - props to pass to the modal component\n * @param options - additional modal options, including {@link KitModalPresentOptions.watchKeyboard}\n * @returns the dismiss data, or `undefined` when the modal is dismissed without data\n * @example\n * ```ts\n * const data = await overlay.presentModal<{ saved: boolean }>(EditPage, { id: 1 }, { watchKeyboard: true });\n * ```\n */\n async presentModal<O = unknown>(\n component: ModalOptions['component'],\n componentProps?: ModalOptions['componentProps'],\n options: KitModalPresentOptions = {},\n ): Promise<O | undefined> {\n const { watchKeyboard, ...modalOptions } = options;\n const modal = await this.#modalCtrl.create({ component, componentProps, ...modalOptions });\n await modal.present();\n const handle = watchKeyboard ? await watchModalKeyboard(modal) : null;\n const { data } = await modal.onDidDismiss<O>();\n await handle?.remove();\n return data;\n }\n\n /**\n * Present a toast using kit defaults that the caller may override.\n *\n * @remarks\n * Defaults to a top position, a 2000ms duration, a vertical swipe gesture, and a close button\n * from the configured labels; any of these can be overridden via `options`. Presenting a toast\n * also triggers light native haptic feedback as an intentional kit UX choice.\n *\n * @param options - Ionic toast options that override the kit defaults\n * @returns the presented toast element\n * @example\n * ```ts\n * await overlay.presentToast({ message: 'Copied to clipboard' });\n * ```\n */\n async presentToast(options: ToastOptions): Promise<HTMLIonToastElement> {\n void kitImpact();\n const toast = await this.#toastCtrl.create({\n position: 'top',\n duration: 2000,\n buttons: [this.#labels.close],\n swipeGesture: 'vertical',\n ...options,\n });\n await toast.present();\n return toast;\n }\n\n /**\n * Present a notification alert with a single \"close\" button and wait for it to be dismissed.\n *\n * @param options - alert content (header, message, optional sub-header)\n * @returns a Promise that resolves once the alert has been dismissed\n * @example\n * ```ts\n * await overlay.alertClose({ header: 'Done', message: 'Your changes were saved.' });\n * ```\n */\n async alertClose(options: KitAlertCloseOptions): Promise<void> {\n const alert = await this.#alertCtrl.create({\n header: options.header,\n subHeader: options.subHeader,\n message: options.message,\n buttons: [this.#labels.close],\n });\n await alert.present();\n await alert.onWillDismiss();\n }\n\n /**\n * Present a confirmation alert with cancel and OK buttons.\n *\n * @param options - alert content plus the OK button text via {@link KitAlertConfirmOptions.okText}\n * @returns `true` when the user presses OK, `false` otherwise (cancel or backdrop dismissal)\n * @example\n * ```ts\n * const ok = await overlay.alertConfirm({\n * header: 'Delete item?',\n * message: 'This cannot be undone.',\n * okText: 'Delete',\n * });\n * if (ok) {\n * await remove();\n * }\n * ```\n */\n async alertConfirm(options: KitAlertConfirmOptions): Promise<boolean> {\n const alert = await this.#alertCtrl.create({\n header: options.header,\n subHeader: options.subHeader,\n message: options.message,\n buttons: [\n { text: this.#labels.cancel, role: 'cancel' },\n { text: options.okText, role: 'confirm' },\n ],\n });\n await alert.present();\n const { role } = await alert.onWillDismiss();\n return role === 'confirm';\n }\n}\n","import type { OnInit } from '@angular/core';\nimport { Directive, ElementRef, inject } from '@angular/core';\nimport { Capacitor } from '@capacitor/core';\n\n/**\n * Work around iOS `ion-input` autofill values not propagating to the Angular form model.\n *\n * On iOS, when the browser autofills an `ion-input` (for example a saved password), the value\n * is written to the underlying native `<input>` element but the corresponding `change` event is\n * not forwarded to the host `ion-input`, so the Angular form control (and `ngModel`) never sees\n * the autofilled value. This directive listens for the first `change` event on the inner input\n * element and mirrors its value back onto the host element, restoring two-way binding.\n *\n * Apply it to any `ion-input` that participates in a form and may be autofilled by attaching the\n * `rdlaboAutofill` attribute.\n *\n * @remarks\n * The directive is a no-op on every platform other than iOS, where the value already propagates\n * correctly. A short timeout is used because `ion-input` creates its inner `<input>` element\n * asynchronously after the host element is initialized.\n *\n * @example\n * ```html\n * <ion-input rdlaboAutofill type=\"password\" [(ngModel)]=\"password\"></ion-input>\n * ```\n */\n@Directive({\n selector: '[rdlaboAutofill]',\n standalone: true,\n})\nexport class KitAutofillDirective implements OnInit {\n readonly #el = inject(ElementRef);\n\n constructor() {}\n\n /**\n * Register the iOS autofill workaround once the directive is initialized.\n *\n * Returns immediately on non-iOS platforms. On iOS, after a short delay it attaches a one-shot,\n * passive `change` listener to the inner `<input>` element that `ion-input` renders, copying the\n * autofilled value back onto the host element so the Angular form model stays in sync. Any error\n * while locating the inner input (for example if the element is not yet present) is swallowed.\n *\n * @returns Nothing.\n */\n ngOnInit(): void {\n if (Capacitor.getPlatform() !== 'ios') {\n return;\n }\n setTimeout(() => {\n try {\n this.#el.nativeElement.children[0].addEventListener(\n 'change',\n (e: Event) => {\n this.#el.nativeElement.value = (e.target as HTMLInputElement).value;\n },\n {\n capture: false,\n once: true,\n passive: true,\n },\n );\n } catch {\n /* empty */\n }\n }, 100); // Need some time for the ion-input to create the input element\n }\n}\n","import type { EnvironmentProviders } from '@angular/core';\nimport { inject, InjectionToken, makeEnvironmentProviders } from '@angular/core';\nimport type { CanActivateFn, RouterStateSnapshot, UrlTree } from '@angular/router';\nimport { Router } from '@angular/router';\nimport { NavController } from '@ionic/angular/standalone';\nimport type { Observable } from 'rxjs';\nimport { map, mergeMap } from 'rxjs/operators';\n\n/**\n * Discriminated set of authentication states the guards react to.\n *\n * @remarks\n * The application is responsible for emitting these values through {@link KitAuthConfig.authState}.\n * An application that does not use a value (for example email confirmation) simply never emits it.\n *\n * - `user` — fully authenticated and verified.\n * - `confirm` — awaiting email confirmation.\n * - `required` — not authenticated.\n * - `anonymous` — signed in anonymously; the user can still be guided toward full registration.\n */\nexport type KitAuthState = 'user' | 'confirm' | 'required' | 'anonymous';\n\n/**\n * Redirect targets (route paths) used by the guards when access is denied.\n *\n * @remarks\n * Every field is required and must be provided per application, because the guards have no\n * knowledge of the host application's route layout.\n */\nexport interface KitAuthRedirects {\n /** Used by {@link kitRequiredUnauthorizedGuard}: where to navigate when the user is already authenticated (`user`). */\n readonly whenAuthorized: string;\n /** Used by {@link kitRequiredUnauthorizedGuard}: where to navigate when the user is awaiting email confirmation (`confirm`). */\n readonly whenConfirming: string;\n /** Used by {@link kitRequireConfirmingGuard}: where to navigate when the state is not `confirm`. */\n readonly whenNotConfirming: string;\n /** Used by {@link kitRequireAuthorizedGuard}: where to navigate when the state is not `user` and the fallback is not allowed. */\n readonly whenUnauthorized: string;\n}\n\n/**\n * Configuration consumed by the authentication guards, injected through {@link provideKitAuth}.\n *\n * @remarks\n * All members are required. The hooks let the host application plug its own auth service and\n * navigation policy into the otherwise fixed guard control flow.\n */\nexport interface KitAuthConfig {\n /**\n * Source of the current authentication state.\n *\n * @remarks\n * Typically backed by the application's own auth service (for example `AuthService.isAuth()`).\n *\n * @returns A stream of {@link KitAuthState} values.\n */\n authState(): Observable<KitAuthState>;\n /**\n * Application-specific work that runs in {@link kitRequireAuthorizedGuard} after the state is confirmed to be `user`.\n *\n * @remarks\n * Typical responsibilities include token login, permission checks, terms-of-service acceptance,\n * or restoring a previously requested redirect.\n *\n * @param state - The router state snapshot of the route being activated.\n * @returns `true` to allow activation, or a `UrlTree` to perform a custom redirect.\n */\n onAuthorized(state: RouterStateSnapshot): Promise<boolean | UrlTree>;\n /**\n * Fallback that runs in {@link kitRequireAuthorizedGuard} when the state is `required` (not authenticated).\n *\n * @remarks\n * For example, attempt an anonymous sign-in and allow the route. Applications that do not need\n * this should pass `async () => false` to fall through to the default redirect.\n *\n * @param state - The router state snapshot of the route being activated.\n * @returns `true` to allow activation, a `UrlTree` for a custom redirect, or `false` to use the default redirect.\n */\n onUnauthenticated(state: RouterStateSnapshot): Promise<boolean | UrlTree>;\n /** Redirect targets used by the guards. */\n redirects: KitAuthRedirects;\n}\n\n/**\n * Injection token that carries the {@link KitAuthConfig} to the authentication guards.\n */\nexport const KIT_AUTH_CONFIG = new InjectionToken<KitAuthConfig>('@rdlabo/ionic-angular-kit:auth');\n\n/**\n * Wire the authentication guard configuration into the application's dependency injection.\n *\n * @remarks\n * The factory runs inside an injection context, so it may call `inject()` (for example\n * `inject(AuthService)`) to build the configuration.\n *\n * @param configFactory - Factory that returns the {@link KitAuthConfig} for the application.\n * @returns Environment providers to add to the application bootstrap.\n *\n * @example\n * ```ts\n * provideKitAuth(() => {\n * const auth = inject(AuthService);\n * return {\n * authState: () => auth.isAuth(),\n * onAuthorized: async () => true,\n * onUnauthenticated: async () => false,\n * redirects: {\n * whenAuthorized: '/',\n * whenConfirming: '/auth/confirm',\n * whenNotConfirming: '/auth/signin',\n * whenUnauthorized: 'auth',\n * },\n * };\n * });\n * ```\n */\nexport const provideKitAuth = (configFactory: () => KitAuthConfig): EnvironmentProviders =>\n makeEnvironmentProviders([{ provide: KIT_AUTH_CONFIG, useFactory: configFactory }]);\n\n/**\n * Guard that requires the user to be unauthenticated (for example sign-in or sign-up pages).\n *\n * @remarks\n * Allows the `required` and `anonymous` states (an anonymous user is permitted to proceed to a\n * registration page). An authenticated user (`user`) is sent to `whenAuthorized`, and a user\n * awaiting confirmation (`confirm`) is sent to `whenConfirming`.\n *\n * @returns A stream emitting `true` to allow activation, or `false` after triggering a redirect.\n *\n * @example\n * ```ts\n * const routes: Routes = [{ path: 'signin', component: SigninPage, canActivate: [kitRequiredUnauthorizedGuard] }];\n * ```\n */\nexport const kitRequiredUnauthorizedGuard: CanActivateFn = () => {\n const { authState, redirects } = inject(KIT_AUTH_CONFIG);\n const router = inject(Router);\n const navCtrl = inject(NavController);\n\n return authState().pipe(\n map((data) => {\n if (data === 'user') {\n navCtrl.setDirection('root');\n router.navigate([redirects.whenAuthorized]);\n return false;\n } else if (data === 'confirm') {\n router.navigate([redirects.whenConfirming]);\n return false;\n }\n // 'required' | 'anonymous'\n return true;\n }),\n );\n};\n\n/**\n * Guard that requires the user to be awaiting email confirmation (`confirm`).\n *\n * @remarks\n * Any other state triggers a redirect: an `anonymous` user is sent to the authenticated area\n * (`whenAuthorized`), and every remaining state is sent to `whenNotConfirming`.\n *\n * @returns A stream emitting `true` to allow activation, or `false` after triggering a redirect.\n *\n * @example\n * ```ts\n * const routes: Routes = [{ path: 'confirm', component: ConfirmPage, canActivate: [kitRequireConfirmingGuard] }];\n * ```\n */\nexport const kitRequireConfirmingGuard: CanActivateFn = () => {\n const { authState, redirects } = inject(KIT_AUTH_CONFIG);\n const router = inject(Router);\n const navCtrl = inject(NavController);\n\n return authState().pipe(\n map((data) => {\n if (data === 'confirm') {\n return true;\n }\n navCtrl.setDirection('root');\n router.navigate([data === 'anonymous' ? redirects.whenAuthorized : redirects.whenNotConfirming]);\n return false;\n }),\n );\n};\n\n/**\n * Guard that requires the user to be fully authenticated (`user`).\n *\n * @remarks\n * - `user` — runs {@link KitAuthConfig.onAuthorized} (token login, permission checks, and so on).\n * - `anonymous` — allowed as-is, for applications that permit anonymous browsing.\n * - `required` / `confirm` — runs {@link KitAuthConfig.onUnauthenticated}; if it resolves to `false`,\n * the user is redirected to `whenUnauthorized`.\n *\n * @param _route - The activated route snapshot (unused).\n * @param state - The router state snapshot, forwarded to the configuration hooks.\n * @returns A stream emitting the activation result: `true`, a `UrlTree`, or `false` after a redirect.\n *\n * @example\n * ```ts\n * const routes: Routes = [{ path: 'home', component: HomePage, canActivate: [kitRequireAuthorizedGuard] }];\n * ```\n */\nexport const kitRequireAuthorizedGuard: CanActivateFn = (_route, state) => {\n const { authState, onAuthorized, onUnauthenticated, redirects } = inject(KIT_AUTH_CONFIG);\n const router = inject(Router);\n const navCtrl = inject(NavController);\n\n return authState().pipe(\n mergeMap(async (data) => {\n if (data === 'user') {\n return onAuthorized(state);\n }\n if (data === 'anonymous') {\n return true;\n }\n const fallback = await onUnauthenticated(state);\n if (fallback !== false) {\n return fallback;\n }\n navCtrl.setDirection('root');\n router.navigate([redirects.whenUnauthorized]);\n return false;\n }),\n );\n};\n","import type { EnvironmentProviders } from '@angular/core';\nimport { inject, InjectionToken, makeEnvironmentProviders } from '@angular/core';\nimport type { HttpErrorResponse, HttpEvent, HttpInterceptorFn, HttpRequest } from '@angular/common/http';\nimport { HttpResponse } from '@angular/common/http';\nimport { Network } from '@capacitor/network';\nimport type { Observable } from 'rxjs';\nimport { from, retry, throwError, timer } from 'rxjs';\nimport { catchError, mergeMap, tap } from 'rxjs/operators';\n\n/**\n * HTTP status codes that must never be retried. `401` is handled separately and thrown immediately.\n *\n * @internal\n */\nconst NON_RETRYABLE_STATUSES = [400, 403, 404, 418, 500, 502];\n\n/**\n * Configuration that customizes the behavior of {@link kitAuthInterceptor}, injected through {@link provideKitHttp}.\n *\n * @remarks\n * The interceptor fixes the retry policy (up to 2 retries with a linearly increasing backoff, plus immediate\n * throw on `401` and on every {@link NON_RETRYABLE_STATUSES | non-retryable status}) and the overall\n * control flow. Only the hooks below are application-specific.\n */\nexport interface KitHttpConfig {\n /**\n * Produce authentication and metadata headers for the outgoing request.\n *\n * @param request - The outgoing request about to be sent.\n * @returns A map of header names to values, resolved asynchronously.\n */\n getAuthHeaders(request: HttpRequest<unknown>): Promise<Record<string, string>>;\n /**\n * Produce additional headers for the outgoing request.\n *\n * @param request - The outgoing request about to be sent.\n * @returns A map of header names to values; return `{}` when none are needed.\n */\n buildExtraHeaders(request: HttpRequest<unknown>): Record<string, string>;\n /**\n * Called for every successful response that completed an actual network round trip.\n *\n * @remarks\n * Responses synthesized by {@link KitHttpConfig.offlineFallback} are produced after `catchError`\n * and therefore never reach this hook, so it observes genuine successes only. A typical use is to\n * reset an \"offline\" flag once connectivity is restored. Implement as a no-op when not needed.\n *\n * @param event - The successful `HttpResponse`.\n */\n onResponse(event: HttpResponse<unknown>): void;\n /**\n * Decide whether to pass the request straight through, skipping auth, retry, and error handling.\n *\n * @remarks\n * Useful for external URLs such as S3 or a CDN. Return `false` to always apply the standard pipeline.\n *\n * @param request - The outgoing request.\n * @returns `true` to bypass the interceptor pipeline.\n */\n bypass(request: HttpRequest<unknown>): boolean;\n /**\n * Provide an offline short-circuit when a request fails.\n *\n * @remarks\n * Returning a non-null observable replaces the error with that response (for example a queued\n * offline result). Return `null` to let normal error handling proceed.\n *\n * @param request - The request that failed (after headers were applied).\n * @param error - The error response that triggered the fallback.\n * @returns A replacement event stream, or `null` for no fallback.\n */\n offlineFallback(request: HttpRequest<unknown>, error: HttpErrorResponse): Observable<HttpEvent<unknown>> | null;\n /**\n * Side effect to run on a `401` response (for example an expired token).\n *\n * @param request - The request that received the `401`.\n */\n onUnauthorized(request: HttpRequest<unknown>): void;\n /**\n * Side effect to run on a `403` response (a permission error).\n *\n * @remarks\n * Implement as a no-op when not needed.\n *\n * @param request - The request that received the `403`.\n */\n onForbidden(request: HttpRequest<unknown>): void;\n /**\n * UX hook for network-originated errors while the device is connected.\n *\n * @param status - The HTTP status code, or a string descriptor for non-HTTP failures.\n * @returns Optionally a promise to await before continuing.\n */\n onNetworkError(status: number | string): Promise<void> | void;\n /**\n * UX hook for `400` / `500` responses that carry a server-provided message.\n *\n * @param message - The message extracted from the error body.\n */\n onServerError(message: string): void;\n}\n\n/**\n * Injection token that carries the {@link KitHttpConfig} to {@link kitAuthInterceptor}.\n */\nexport const KIT_HTTP_CONFIG = new InjectionToken<KitHttpConfig>('@rdlabo/ionic-angular-kit:http');\n\n/**\n * Wire the {@link kitAuthInterceptor} configuration into the application's dependency injection.\n *\n * @remarks\n * Register the interceptor itself separately via `provideHttpClient(withInterceptors([kitAuthInterceptor]))`.\n * The factory runs inside an injection context, so it may call `inject()`.\n *\n * @param configFactory - Factory that returns the {@link KitHttpConfig} for the application.\n * @returns Environment providers to add to the application bootstrap.\n *\n * @example\n * ```ts\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideHttpClient(withInterceptors([kitAuthInterceptor])),\n * provideKitHttp(() => {\n * const auth = inject(AuthService);\n * return {\n * getAuthHeaders: async () => ({ Authorization: `Bearer ${await auth.token()}` }),\n * buildExtraHeaders: () => ({}),\n * onResponse: () => {},\n * bypass: (request) => request.url.startsWith('https://cdn.example.com'),\n * offlineFallback: () => null,\n * onUnauthorized: () => auth.signOut(),\n * onForbidden: () => {},\n * onNetworkError: () => {},\n * onServerError: (message) => console.error(message),\n * };\n * }),\n * ],\n * });\n * ```\n */\nexport const provideKitHttp = (configFactory: () => KitHttpConfig): EnvironmentProviders =>\n makeEnvironmentProviders([{ provide: KIT_HTTP_CONFIG, useFactory: configFactory }]);\n\n/**\n * Canonical functional HTTP interceptor that applies authentication, retries, and error handling.\n *\n * @remarks\n * Behavior, driven by the injected {@link KitHttpConfig}:\n *\n * 1. Requests for which `bypass` returns `true` are forwarded untouched.\n * 2. Otherwise the headers from `getAuthHeaders` and `buildExtraHeaders` are merged onto a cloned request.\n * 3. Failed requests are retried up to 2 times with a linearly increasing backoff of `500ms * (retryCount + 5)`,\n * except that `401` and any {@link NON_RETRYABLE_STATUSES | non-retryable status}\n * (`400`, `403`, `404`, `418`, `500`, `502`) are thrown immediately without retrying.\n * 4. On error, `offlineFallback` is consulted first; otherwise `401` calls `onUnauthorized`, `403`\n * calls `onForbidden`, network-class failures (anything other than `400`/`500`) call\n * `onNetworkError` when the device is connected, and `400`/`500` responses carrying a body\n * message call `onServerError`.\n *\n * @param request - The outgoing request.\n * @param next - The next handler in the interceptor chain.\n * @returns A stream of HTTP events for the (possibly modified, retried, or replaced) request.\n *\n * @example\n * ```ts\n * provideHttpClient(withInterceptors([kitAuthInterceptor]));\n * ```\n */\nexport const kitAuthInterceptor: HttpInterceptorFn = (request, next) => {\n const config = inject(KIT_HTTP_CONFIG);\n\n if (config.bypass(request)) {\n return next(request);\n }\n\n return from(config.getAuthHeaders(request)).pipe(\n mergeMap((authHeaders) => {\n const req = request.clone({ setHeaders: { ...authHeaders, ...config.buildExtraHeaders(request) } });\n\n return next(req).pipe(\n retry({\n count: 2,\n delay: (e: HttpErrorResponse, retryCount) => {\n if (e.status === 401) {\n return throwError(() => e);\n }\n if (NON_RETRYABLE_STATUSES.includes(e.status)) {\n return throwError(() => e);\n }\n return timer((retryCount + 5) * 500);\n },\n }),\n tap((event) => {\n if (event instanceof HttpResponse) {\n config.onResponse(event);\n }\n }),\n catchError((e: HttpErrorResponse) => {\n const fallback = config.offlineFallback(req, e);\n if (fallback) {\n return fallback;\n }\n if (e.status === 401) {\n config.onUnauthorized(req);\n } else if (e.status === 403) {\n config.onForbidden(req);\n } else if (![400, 500].includes(e.status)) {\n void Network.getStatus().then((status) => {\n if (status.connected) {\n config.onNetworkError(e.status);\n }\n });\n } else if (e.error?.message) {\n config.onServerError(e.error.message);\n }\n return throwError(() => e);\n }),\n );\n }),\n );\n};\n","/**\n * Merge a newly fetched page of items into an existing list by id, keeping the result sorted.\n *\n * Items are merged by the numeric `key` field. On a duplicate `key` the item from `arrayNew` wins\n * and replaces the one in `arrayOld`. Old items whose `key` falls *inside* the window spanned by\n * `arrayNew` (between its first and last `key`) are dropped, since the freshly fetched page is the\n * authoritative copy of that window; old items *outside* the window are kept. The merged items are\n * finally sorted by `key`, ascending or descending according to `order`.\n *\n * The algorithm is:\n * 1. If both inputs are empty, return an empty array.\n * 2. Read the first (`lead`) and last (`last`) `key` values of `arrayNew` as the bounds of the\n * page's window. Keep the old items whose `key` lies *outside* that window (at or beyond the\n * extremes) and drop those strictly inside it, handling both a high-to-low page (`lead > last`)\n * and a low-to-high page (`lead < last`). A single-value page (`lead === last`) keeps all old items.\n * 3. Remove old items whose `key` already exists in `arrayNew` (new wins on duplicates).\n * 4. Concatenate `arrayNew` with the surviving old items and sort by `key` in the requested\n * direction.\n *\n * @remarks\n * Designed for infinite-scroll / paginated list merging, where each fetched page may overlap the\n * previously held items and the server returns a contiguous, ordered window of records. The `key`\n * field is treated as a number for range comparison and sorting.\n *\n * @typeParam T - Element type of both arrays. Its `key` property must be a numeric value.\n * @param arrayOld - The previously accumulated list. May be empty or nullish-safe (empty when falsy).\n * @param arrayNew - The newly fetched page of items; its `key` range defines the window that its own\n * items replace, and its items take precedence on duplicates.\n * @param key - The property of `T` used as the unique, numeric id for matching, range filtering, and sorting.\n * @param order - Sort direction by `key`: `'ASC'` for ascending or `'DESC'` for descending. Defaults to `'DESC'`.\n * @returns A new array containing `arrayNew` merged with the out-of-window, non-duplicate old items, sorted by `key`.\n * @example\n * ```ts\n * interface Post {\n * id: number;\n * title: string;\n * }\n *\n * const loaded: Post[] = [\n * { id: 30, title: 'c' },\n * { id: 20, title: 'b' },\n * { id: 10, title: 'a' },\n * ];\n * const nextPage: Post[] = [\n * { id: 20, title: 'b (updated)' },\n * { id: 15, title: 'a.5' },\n * ];\n *\n * // Descending merge: id 30 lies above the new page's [15, 20] window so it is kept; id 20 is\n * // inside the window and is replaced by the new value; id 10 lies below the window and is kept.\n * const merged = arrayConcatById(loaded, nextPage, 'id', 'DESC');\n * // => [{ id: 30, title: 'c' }, { id: 20, title: 'b (updated)' }, { id: 15, title: 'a.5' }, { id: 10, title: 'a' }]\n * ```\n */\nexport const arrayConcatById = <T>(arrayOld: T[], arrayNew: T[], key: keyof T, order: 'ASC' | 'DESC' = 'DESC'): T[] => {\n if (!arrayNew.length && !arrayOld.length) {\n return [];\n }\n const lead = arrayNew[0][key] as number;\n const last = arrayNew[arrayNew.length - 1][key] as number;\n\n const filteredOld = (arrayOld || []).filter((vol) => {\n const value = vol[key] as number;\n return (lead > last && (value >= lead || value <= last)) || (lead < last && (value <= lead || value >= last)) || lead === last;\n });\n\n const oldData = filteredOld.filter((vol) => !arrayNew.some((element) => element[key] === vol[key]));\n const data = arrayNew.concat(oldData);\n\n const direction = order === 'ASC' ? 1 : -1;\n return data.sort((a, b) => {\n const x = a[key] as number;\n const y = b[key] as number;\n if (x > y) {\n return direction;\n }\n if (x < y) {\n return direction * -1;\n }\n return 0;\n });\n};\n","/*\n * Public API Surface of @rdlabo/ionic-angular-kit\n */\n\n// Storage: typed wrapper around the platform key/value store.\nexport * from './lib/storage/kit-storage.service';\n\n// Overlay: wrapper around the Ionic Modal / Toast / Alert controllers.\nexport * from './lib/overlay/overlay-config';\nexport * from './lib/overlay/kit-overlay.controller';\n\n// Directives.\nexport * from './lib/directives/autofill.directive';\n\n// Auth: functional route guards.\nexport * from './lib/auth/auth-guards';\n\n// HTTP: functional interceptor.\nexport * from './lib/http/kit-http.interceptor';\n\n// Utils: framework-agnostic pure helpers.\nexport * from './lib/utils/haptics';\nexport * from './lib/utils/array';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"rdlabo-ionic-angular-kit.mjs","sources":["../../../projects/kit/src/lib/storage/kit-storage.service.ts","../../../projects/kit/src/lib/overlay/overlay-config.ts","../../../projects/kit/src/lib/utils/haptics.ts","../../../projects/kit/src/lib/overlay/kit-overlay.controller.ts","../../../projects/kit/src/lib/overlay/reload-alert.ts","../../../projects/kit/src/lib/directives/autofill.directive.ts","../../../projects/kit/src/lib/auth/auth-guards.ts","../../../projects/kit/src/lib/http/kit-http.interceptor.ts","../../../projects/kit/src/lib/utils/array.ts","../../../projects/kit/src/public-api.ts","../../../projects/kit/src/rdlabo-ionic-angular-kit.ts"],"sourcesContent":["import { inject, Injectable } from '@angular/core';\nimport { Storage } from '@ionic/storage-angular';\n\n/**\n * Thin, typed wrapper around `@ionic/storage-angular`.\n *\n * Starts `create()` exactly once and stores the resulting ready Promise. Every operation awaits\n * that Promise before touching the underlying store, so calls made before initialization completes\n * are queued rather than dropped.\n *\n * @remarks\n * A naive wrapper that reads the store synchronously would silently no-op (or throw) when invoked\n * before `create()` resolves, losing early writes. Awaiting the one-time ready Promise on every\n * operation removes that race without forcing callers to coordinate initialization themselves.\n *\n * @example\n * ```ts\n * constructor(private readonly storage: KitStorageService) {}\n *\n * async ngOnInit(): Promise<void> {\n * await this.storage.set('token', 'abc123');\n * const token = await this.storage.get<string>('token');\n * }\n * ```\n */\n@Injectable({\n providedIn: 'root',\n})\nexport class KitStorageService {\n /** One-time `create()` ready Promise; awaited before every operation so early calls are not lost. */\n readonly #ready: Promise<Storage> = inject(Storage).create();\n\n /**\n * Persist a value under the given key.\n *\n * @typeParam T - type of the value being stored\n * @param key - key to store the value under\n * @param value - value to persist; overwrites any existing value for the key\n * @returns a Promise that resolves once the value has been written\n * @example\n * ```ts\n * await storage.set('user', { id: 1, name: 'Ada' });\n * ```\n */\n async set<T>(key: string, value: T): Promise<void> {\n await (await this.#ready).set(key, value);\n }\n\n /**\n * Read the value stored under the given key.\n *\n * @typeParam T - expected type of the stored value\n * @param key - key to read\n * @returns the stored value, or `null` when the key is absent\n * @example\n * ```ts\n * const user = await storage.get<{ id: number }>('user');\n * ```\n */\n async get<T>(key: string): Promise<T | null> {\n return (await (await this.#ready).get(key)) ?? null;\n }\n\n /**\n * Remove the value stored under the given key.\n *\n * @param key - key to remove; a no-op when the key is absent\n * @returns a Promise that resolves once the key has been removed\n */\n async remove(key: string): Promise<void> {\n await (await this.#ready).remove(key);\n }\n\n /**\n * Remove every key/value pair from the store.\n *\n * @returns a Promise that resolves once the store has been emptied\n */\n async clear(): Promise<void> {\n await (await this.#ready).clear();\n }\n\n /**\n * List every key currently present in the store.\n *\n * @returns an array of all stored keys\n */\n async keys(): Promise<string[]> {\n return (await this.#ready).keys();\n }\n}\n","import type { EnvironmentProviders } from '@angular/core';\nimport { InjectionToken, makeEnvironmentProviders } from '@angular/core';\n\n/**\n * User-visible button labels consumed by `KitOverlayController`.\n *\n * @remarks\n * The kit deliberately ships no i18n strings of its own and has no hard dependency on\n * `@angular/localize`. The consuming application always provides these labels: multilingual apps\n * pass `$localize`-resolved strings, single-language apps pass plain literals.\n */\nexport interface KitLabels {\n /** Text for the \"close\" button used by alerts and toasts. */\n readonly close: string;\n /** Text for the \"cancel\" button used by confirmation alerts. */\n readonly cancel: string;\n}\n\n/**\n * Overlay configuration injected via `provideKitOverlay()`.\n *\n * @remarks\n * All fields are required; the consuming application must supply a complete configuration.\n */\nexport interface KitOverlayConfig {\n /** Button labels used across the overlays presented by `KitOverlayController`. */\n readonly labels: KitLabels;\n}\n\n/**\n * Injection token carrying the {@link KitOverlayConfig} for `KitOverlayController`.\n *\n * @remarks\n * Provide it through {@link provideKitOverlay} rather than registering it directly.\n */\nexport const KIT_OVERLAY_CONFIG = new InjectionToken<KitOverlayConfig>('@rdlabo/ionic-angular-kit:overlay');\n\n/**\n * Wire `KitOverlayController` into the application by providing its button labels.\n *\n * @param config - overlay configuration, including the button labels to inject\n * @returns environment providers to add to the application's provider list\n * @example\n * ```ts\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideKitOverlay({ labels: { close: $localize`Close`, cancel: $localize`Cancel` } }),\n * ],\n * });\n * ```\n */\nexport const provideKitOverlay = (config: KitOverlayConfig): EnvironmentProviders =>\n makeEnvironmentProviders([{ provide: KIT_OVERLAY_CONFIG, useValue: config }]);\n","import { Capacitor } from '@capacitor/core';\nimport { Haptics, ImpactStyle } from '@capacitor/haptics';\n\n/**\n * Trigger native haptic impact feedback.\n *\n * Delegates to `@capacitor/haptics` `Haptics.impact()` when running on a native platform. On the\n * web (or any non-native platform) it is a no-op and resolves without doing anything.\n *\n * @remarks\n * This haptic side effect was previously bundled implicitly into toast/modal presentation. It has\n * been decoupled into this explicit function so callers opt in to the feedback deliberately, rather\n * than receiving it as a hidden side effect of presenting UI.\n *\n * @param style - The impact intensity to play. Defaults to {@link ImpactStyle.Light}.\n * @returns A promise that resolves once the feedback has been requested (immediately on the web).\n * @example\n * ```ts\n * import { ImpactStyle } from '@capacitor/haptics';\n *\n * // Light tap (default)\n * await kitImpact();\n *\n * // Stronger feedback, e.g. on a confirming action\n * await kitImpact(ImpactStyle.Heavy);\n * ```\n */\nexport const kitImpact = async (style: ImpactStyle = ImpactStyle.Light): Promise<void> => {\n if (Capacitor.isNativePlatform()) {\n await Haptics.impact({ style });\n }\n};\n","import { inject, Injectable } from '@angular/core';\nimport type { ModalOptions, ToastOptions } from '@ionic/angular/standalone';\nimport { AlertController, ModalController, ToastController } from '@ionic/angular/standalone';\nimport type { PluginListenerHandle } from '@capacitor/core';\nimport { Capacitor } from '@capacitor/core';\nimport { Keyboard } from '@capacitor/keyboard';\nimport { KIT_OVERLAY_CONFIG } from './overlay-config';\nimport { kitImpact } from '../utils/haptics';\n\n/**\n * Options for {@link KitOverlayController.presentModal}.\n *\n * @remarks\n * Extends Ionic's `ModalOptions` but omits `component` and `componentProps`, which are passed as\n * dedicated arguments instead.\n */\nexport interface KitModalPresentOptions extends Omit<ModalOptions, 'component' | 'componentProps'> {\n /**\n * When `true`, expand the sheet to its maximum breakpoint while the native keyboard is shown.\n *\n * @remarks\n * Only has an effect on native platforms; ignored on the web.\n */\n watchKeyboard?: boolean;\n}\n\n/**\n * Options for {@link KitOverlayController.alertClose}.\n */\nexport interface KitAlertCloseOptions {\n /** Alert header text. */\n header: string;\n /** Alert body message. */\n message: string;\n /** Optional alert sub-header text shown beneath the header. */\n subHeader?: string;\n}\n\n/**\n * Options for {@link KitOverlayController.alertConfirm}.\n *\n * @remarks\n * Extends {@link KitAlertCloseOptions} with the confirm-button text.\n */\nexport interface KitAlertConfirmOptions extends KitAlertCloseOptions {\n /**\n * Text for the OK (confirm) button.\n *\n * @remarks\n * Action-specific, so it is supplied by the caller rather than taken from the shared labels.\n */\n okText: string;\n}\n\n/**\n * Attach a native keyboard listener that grows the modal to its maximum breakpoint when the\n * keyboard appears.\n *\n * @param modal - the presented modal element to resize\n * @returns a listener handle; on non-native platforms a no-op handle whose `remove()` does nothing\n * @internal\n */\nconst watchModalKeyboard = async (modal: HTMLIonModalElement): Promise<PluginListenerHandle> => {\n if (!Capacitor.isNativePlatform()) {\n return { remove: async () => undefined };\n }\n return Keyboard.addListener('keyboardDidShow', () => modal.setCurrentBreakpoint(1));\n};\n\n/**\n * Ergonomic wrapper that consolidates Ionic's overlay controllers (Modal / Toast / Alert).\n *\n * @remarks\n * Folds the repetitive create → present → onDidDismiss sequence into single calls and returns the\n * relevant result directly. It holds no application-specific policy such as navigation; compose\n * those concerns on the consuming side.\n *\n * @example\n * ```ts\n * constructor(private readonly overlay: KitOverlayController) {}\n *\n * async edit(): Promise<void> {\n * const result = await this.overlay.presentModal<EditResult>(EditPage, { id: 1 });\n * if (result) {\n * await this.overlay.presentToast({ message: 'Saved' });\n * }\n * }\n * ```\n */\n@Injectable({\n providedIn: 'root',\n})\nexport class KitOverlayController {\n readonly #modalCtrl = inject(ModalController);\n readonly #toastCtrl = inject(ToastController);\n readonly #alertCtrl = inject(AlertController);\n readonly #labels = inject(KIT_OVERLAY_CONFIG).labels;\n\n /**\n * Present a modal and resolve with the data passed to its dismissal.\n *\n * @typeParam O - type of the data returned when the modal is dismissed\n * @param component - the component to render inside the modal\n * @param componentProps - props to pass to the modal component\n * @param options - additional modal options, including {@link KitModalPresentOptions.watchKeyboard}\n * @returns the dismiss data, or `undefined` when the modal is dismissed without data\n * @example\n * ```ts\n * const data = await overlay.presentModal<{ saved: boolean }>(EditPage, { id: 1 }, { watchKeyboard: true });\n * ```\n */\n async presentModal<O = unknown>(\n component: ModalOptions['component'],\n componentProps?: ModalOptions['componentProps'],\n options: KitModalPresentOptions = {},\n ): Promise<O | undefined> {\n const { watchKeyboard, ...modalOptions } = options;\n const modal = await this.#modalCtrl.create({ component, componentProps, ...modalOptions });\n await modal.present();\n const handle = watchKeyboard ? await watchModalKeyboard(modal) : null;\n const { data } = await modal.onDidDismiss<O>();\n await handle?.remove();\n return data;\n }\n\n /**\n * Present a toast using kit defaults that the caller may override.\n *\n * @remarks\n * Defaults to a top position, a 2000ms duration, a vertical swipe gesture, and a close button\n * from the configured labels; any of these can be overridden via `options`. Presenting a toast\n * also triggers light native haptic feedback as an intentional kit UX choice.\n *\n * @param options - Ionic toast options that override the kit defaults\n * @returns the presented toast element\n * @example\n * ```ts\n * await overlay.presentToast({ message: 'Copied to clipboard' });\n * ```\n */\n async presentToast(options: ToastOptions): Promise<HTMLIonToastElement> {\n void kitImpact();\n const toast = await this.#toastCtrl.create({\n position: 'top',\n duration: 2000,\n buttons: [this.#labels.close],\n swipeGesture: 'vertical',\n ...options,\n });\n await toast.present();\n return toast;\n }\n\n /**\n * Present a notification alert with a single \"close\" button and wait for it to be dismissed.\n *\n * @param options - alert content (header, message, optional sub-header)\n * @returns a Promise that resolves once the alert has been dismissed\n * @example\n * ```ts\n * await overlay.alertClose({ header: 'Done', message: 'Your changes were saved.' });\n * ```\n */\n async alertClose(options: KitAlertCloseOptions): Promise<void> {\n const alert = await this.#alertCtrl.create({\n header: options.header,\n subHeader: options.subHeader,\n message: options.message,\n buttons: [this.#labels.close],\n });\n await alert.present();\n await alert.onWillDismiss();\n }\n\n /**\n * Present a confirmation alert with cancel and OK buttons.\n *\n * @param options - alert content plus the OK button text via {@link KitAlertConfirmOptions.okText}\n * @returns `true` when the user presses OK, `false` otherwise (cancel or backdrop dismissal)\n * @example\n * ```ts\n * const ok = await overlay.alertConfirm({\n * header: 'Delete item?',\n * message: 'This cannot be undone.',\n * okText: 'Delete',\n * });\n * if (ok) {\n * await remove();\n * }\n * ```\n */\n async alertConfirm(options: KitAlertConfirmOptions): Promise<boolean> {\n const alert = await this.#alertCtrl.create({\n header: options.header,\n subHeader: options.subHeader,\n message: options.message,\n buttons: [\n { text: this.#labels.cancel, role: 'cancel' },\n { text: options.okText, role: 'confirm' },\n ],\n });\n await alert.present();\n const { role } = await alert.onWillDismiss();\n return role === 'confirm';\n }\n}\n","import type { KitAlertConfirmOptions } from './kit-overlay.controller';\nimport { KitOverlayController } from './kit-overlay.controller';\n\n/**\n * Present the fleet's canonical \"network error → offer to reload\" confirmation.\n *\n * @remarks\n * Folds together the three copy-pasted concerns that every app duplicated around a network failure:\n *\n * 1. Suppress stacking — if an `ion-alert` is already on screen, do nothing.\n * 2. Ask the user to confirm via {@link KitOverlayController.alertConfirm}.\n * 3. Call `location.reload()` when the user confirms.\n *\n * All user-facing text (`header`, `message`, `okText`) is supplied by the caller so the kit stays\n * free of any hardcoded i18n. Typically wired from {@link KitHttpConfig.onNetworkError}, but it can\n * be called from anywhere an app wants the same behavior (e.g. an offline fallback).\n *\n * @param overlay - the kit overlay controller used to present the confirmation\n * @param options - alert content plus the confirm-button text (usually \"Refresh\")\n * @returns a Promise that resolves once the alert is dismissed (the page reloads on confirm)\n * @example\n * ```ts\n * provideKitHttp(() => {\n * const overlay = inject(KitOverlayController);\n * return {\n * getAuthHeaders: async () => ({ ... }),\n * onNetworkError: (status) =>\n * kitPresentReloadAlert(overlay, {\n * header: 'ネットワークエラー',\n * message: `通信できませんでした。リフレッシュしますか?(${status})`,\n * okText: 'リフレッシュ',\n * }),\n * };\n * });\n * ```\n */\nexport const kitPresentReloadAlert = async (\n overlay: KitOverlayController,\n options: KitAlertConfirmOptions,\n): Promise<void> => {\n // 既にアラート表示中なら多重表示しない。\n if (document.querySelector('ion-alert')) {\n return;\n }\n const reload = await overlay.alertConfirm(options);\n if (reload) {\n location.reload();\n }\n};\n","import type { OnInit } from '@angular/core';\nimport { Directive, ElementRef, inject } from '@angular/core';\nimport { Capacitor } from '@capacitor/core';\n\n/**\n * Work around iOS `ion-input` autofill values not propagating to the Angular form model.\n *\n * On iOS, when the browser autofills an `ion-input` (for example a saved password), the value\n * is written to the underlying native `<input>` element but the corresponding `change` event is\n * not forwarded to the host `ion-input`, so the Angular form control (and `ngModel`) never sees\n * the autofilled value. This directive listens for the first `change` event on the inner input\n * element and mirrors its value back onto the host element, restoring two-way binding.\n *\n * Apply it to any `ion-input` that participates in a form and may be autofilled by attaching the\n * `rdlaboAutofill` attribute.\n *\n * @remarks\n * The directive is a no-op on every platform other than iOS, where the value already propagates\n * correctly. A short timeout is used because `ion-input` creates its inner `<input>` element\n * asynchronously after the host element is initialized.\n *\n * @example\n * ```html\n * <ion-input rdlaboAutofill type=\"password\" [(ngModel)]=\"password\"></ion-input>\n * ```\n */\n@Directive({\n selector: '[rdlaboAutofill]',\n standalone: true,\n})\nexport class KitAutofillDirective implements OnInit {\n readonly #el = inject(ElementRef);\n\n constructor() {}\n\n /**\n * Register the iOS autofill workaround once the directive is initialized.\n *\n * Returns immediately on non-iOS platforms. On iOS, after a short delay it attaches a one-shot,\n * passive `change` listener to the inner `<input>` element that `ion-input` renders, copying the\n * autofilled value back onto the host element so the Angular form model stays in sync. Any error\n * while locating the inner input (for example if the element is not yet present) is swallowed.\n *\n * @returns Nothing.\n */\n ngOnInit(): void {\n if (Capacitor.getPlatform() !== 'ios') {\n return;\n }\n setTimeout(() => {\n try {\n this.#el.nativeElement.children[0].addEventListener(\n 'change',\n (e: Event) => {\n this.#el.nativeElement.value = (e.target as HTMLInputElement).value;\n },\n {\n capture: false,\n once: true,\n passive: true,\n },\n );\n } catch {\n /* empty */\n }\n }, 100); // Need some time for the ion-input to create the input element\n }\n}\n","import type { EnvironmentProviders } from '@angular/core';\nimport { inject, InjectionToken, makeEnvironmentProviders } from '@angular/core';\nimport type { CanActivateFn, RouterStateSnapshot, UrlTree } from '@angular/router';\nimport { Router } from '@angular/router';\nimport { NavController } from '@ionic/angular/standalone';\nimport type { Observable } from 'rxjs';\nimport { map, mergeMap } from 'rxjs/operators';\n\n/**\n * Discriminated set of authentication states the guards react to.\n *\n * @remarks\n * The application is responsible for emitting these values through {@link KitAuthConfig.authState}.\n * An application that does not use a value (for example email confirmation) simply never emits it.\n *\n * - `user` — fully authenticated and verified.\n * - `confirm` — awaiting email confirmation.\n * - `required` — not authenticated.\n * - `anonymous` — signed in anonymously; the user can still be guided toward full registration.\n */\nexport type KitAuthState = 'user' | 'confirm' | 'required' | 'anonymous';\n\n/**\n * Redirect targets (route paths) used by the guards when access is denied.\n *\n * @remarks\n * Every field is required and must be provided per application, because the guards have no\n * knowledge of the host application's route layout.\n */\nexport interface KitAuthRedirects {\n /** Used by {@link kitRequiredUnauthorizedGuard}: where to navigate when the user is already authenticated (`user`). */\n readonly whenAuthorized: string;\n /** Used by {@link kitRequiredUnauthorizedGuard}: where to navigate when the user is awaiting email confirmation (`confirm`). */\n readonly whenConfirming: string;\n /** Used by {@link kitRequireConfirmingGuard}: where to navigate when the state is not `confirm`. */\n readonly whenNotConfirming: string;\n /** Used by {@link kitRequireAuthorizedGuard}: where to navigate when the state is not `user` and the fallback is not allowed. */\n readonly whenUnauthorized: string;\n}\n\n/**\n * Configuration consumed by the authentication guards, injected through {@link provideKitAuth}.\n *\n * @remarks\n * All members are required. The hooks let the host application plug its own auth service and\n * navigation policy into the otherwise fixed guard control flow.\n */\nexport interface KitAuthConfig {\n /**\n * Source of the current authentication state.\n *\n * @remarks\n * Typically backed by the application's own auth service (for example `AuthService.isAuth()`).\n *\n * @returns A stream of {@link KitAuthState} values.\n */\n authState(): Observable<KitAuthState>;\n /**\n * Application-specific work that runs in {@link kitRequireAuthorizedGuard} after the state is confirmed to be `user`.\n *\n * @remarks\n * Typical responsibilities include token login, permission checks, terms-of-service acceptance,\n * or restoring a previously requested redirect.\n *\n * @param state - The router state snapshot of the route being activated.\n * @returns `true` to allow activation, or a `UrlTree` to perform a custom redirect.\n */\n onAuthorized(state: RouterStateSnapshot): Promise<boolean | UrlTree>;\n /**\n * Fallback that runs in {@link kitRequireAuthorizedGuard} when the state is `required` (not authenticated).\n *\n * @remarks\n * For example, attempt an anonymous sign-in and allow the route. Applications that do not need\n * this should pass `async () => false` to fall through to the default redirect.\n *\n * @param state - The router state snapshot of the route being activated.\n * @returns `true` to allow activation, a `UrlTree` for a custom redirect, or `false` to use the default redirect.\n */\n onUnauthenticated(state: RouterStateSnapshot): Promise<boolean | UrlTree>;\n /** Redirect targets used by the guards. */\n redirects: KitAuthRedirects;\n}\n\n/**\n * Injection token that carries the {@link KitAuthConfig} to the authentication guards.\n */\nexport const KIT_AUTH_CONFIG = new InjectionToken<KitAuthConfig>('@rdlabo/ionic-angular-kit:auth');\n\n/**\n * Wire the authentication guard configuration into the application's dependency injection.\n *\n * @remarks\n * The factory runs inside an injection context, so it may call `inject()` (for example\n * `inject(AuthService)`) to build the configuration.\n *\n * @param configFactory - Factory that returns the {@link KitAuthConfig} for the application.\n * @returns Environment providers to add to the application bootstrap.\n *\n * @example\n * ```ts\n * provideKitAuth(() => {\n * const auth = inject(AuthService);\n * return {\n * authState: () => auth.isAuth(),\n * onAuthorized: async () => true,\n * onUnauthenticated: async () => false,\n * redirects: {\n * whenAuthorized: '/',\n * whenConfirming: '/auth/confirm',\n * whenNotConfirming: '/auth/signin',\n * whenUnauthorized: 'auth',\n * },\n * };\n * });\n * ```\n */\nexport const provideKitAuth = (configFactory: () => KitAuthConfig): EnvironmentProviders =>\n makeEnvironmentProviders([{ provide: KIT_AUTH_CONFIG, useFactory: configFactory }]);\n\n/**\n * Guard that requires the user to be unauthenticated (for example sign-in or sign-up pages).\n *\n * @remarks\n * Allows the `required` and `anonymous` states (an anonymous user is permitted to proceed to a\n * registration page). An authenticated user (`user`) is sent to `whenAuthorized`, and a user\n * awaiting confirmation (`confirm`) is sent to `whenConfirming`.\n *\n * @returns A stream emitting `true` to allow activation, or `false` after triggering a redirect.\n *\n * @example\n * ```ts\n * const routes: Routes = [{ path: 'signin', component: SigninPage, canActivate: [kitRequiredUnauthorizedGuard] }];\n * ```\n */\nexport const kitRequiredUnauthorizedGuard: CanActivateFn = () => {\n const { authState, redirects } = inject(KIT_AUTH_CONFIG);\n const router = inject(Router);\n const navCtrl = inject(NavController);\n\n return authState().pipe(\n map((data) => {\n if (data === 'user') {\n navCtrl.setDirection('root');\n router.navigate([redirects.whenAuthorized]);\n return false;\n } else if (data === 'confirm') {\n router.navigate([redirects.whenConfirming]);\n return false;\n }\n // 'required' | 'anonymous'\n return true;\n }),\n );\n};\n\n/**\n * Guard that requires the user to be awaiting email confirmation (`confirm`).\n *\n * @remarks\n * Any other state triggers a redirect: an `anonymous` user is sent to the authenticated area\n * (`whenAuthorized`), and every remaining state is sent to `whenNotConfirming`.\n *\n * @returns A stream emitting `true` to allow activation, or `false` after triggering a redirect.\n *\n * @example\n * ```ts\n * const routes: Routes = [{ path: 'confirm', component: ConfirmPage, canActivate: [kitRequireConfirmingGuard] }];\n * ```\n */\nexport const kitRequireConfirmingGuard: CanActivateFn = () => {\n const { authState, redirects } = inject(KIT_AUTH_CONFIG);\n const router = inject(Router);\n const navCtrl = inject(NavController);\n\n return authState().pipe(\n map((data) => {\n if (data === 'confirm') {\n return true;\n }\n navCtrl.setDirection('root');\n router.navigate([data === 'anonymous' ? redirects.whenAuthorized : redirects.whenNotConfirming]);\n return false;\n }),\n );\n};\n\n/**\n * Guard that requires the user to be fully authenticated (`user`).\n *\n * @remarks\n * - `user` — runs {@link KitAuthConfig.onAuthorized} (token login, permission checks, and so on).\n * - `anonymous` — allowed as-is, for applications that permit anonymous browsing.\n * - `required` / `confirm` — runs {@link KitAuthConfig.onUnauthenticated}; if it resolves to `false`,\n * the user is redirected to `whenUnauthorized`.\n *\n * @param _route - The activated route snapshot (unused).\n * @param state - The router state snapshot, forwarded to the configuration hooks.\n * @returns A stream emitting the activation result: `true`, a `UrlTree`, or `false` after a redirect.\n *\n * @example\n * ```ts\n * const routes: Routes = [{ path: 'home', component: HomePage, canActivate: [kitRequireAuthorizedGuard] }];\n * ```\n */\nexport const kitRequireAuthorizedGuard: CanActivateFn = (_route, state) => {\n const { authState, onAuthorized, onUnauthenticated, redirects } = inject(KIT_AUTH_CONFIG);\n const router = inject(Router);\n const navCtrl = inject(NavController);\n\n return authState().pipe(\n mergeMap(async (data) => {\n if (data === 'user') {\n return onAuthorized(state);\n }\n if (data === 'anonymous') {\n return true;\n }\n const fallback = await onUnauthenticated(state);\n if (fallback !== false) {\n return fallback;\n }\n navCtrl.setDirection('root');\n router.navigate([redirects.whenUnauthorized]);\n return false;\n }),\n );\n};\n","import type { EnvironmentProviders } from '@angular/core';\nimport { inject, InjectionToken, makeEnvironmentProviders } from '@angular/core';\nimport type { HttpErrorResponse, HttpEvent, HttpInterceptorFn, HttpRequest } from '@angular/common/http';\nimport { HttpResponse } from '@angular/common/http';\nimport { Network } from '@capacitor/network';\nimport type { Observable } from 'rxjs';\nimport { from, retry, throwError, timer } from 'rxjs';\nimport { catchError, mergeMap, tap } from 'rxjs/operators';\n\n/**\n * HTTP status codes that must never be retried. `401` is handled separately and thrown immediately.\n *\n * @internal\n */\nconst NON_RETRYABLE_STATUSES = [400, 403, 404, 418, 500, 502];\n\n/**\n * Configuration that customizes the behavior of {@link kitAuthInterceptor}, injected through {@link provideKitHttp}.\n *\n * @remarks\n * The interceptor fixes the retry policy (up to 2 retries with a linearly increasing backoff, plus immediate\n * throw on `401` and on every {@link NON_RETRYABLE_STATUSES | non-retryable status}) and the overall\n * control flow. Only the hooks below are application-specific.\n *\n * Only {@link KitHttpConfig.getAuthHeaders} is required — it has no safe default. Every other hook is\n * optional and defaults to a no-op (or `{}` / `false` / `null` as appropriate), so an app configures\n * only the behavior that actually differs from the canonical baseline.\n */\nexport interface KitHttpConfig {\n /**\n * Produce authentication and metadata headers for the outgoing request.\n *\n * @param request - The outgoing request about to be sent.\n * @returns A map of header names to values, resolved asynchronously.\n */\n getAuthHeaders(request: HttpRequest<unknown>): Promise<Record<string, string>>;\n /**\n * Produce additional headers for the outgoing request.\n *\n * @remarks\n * Optional; defaults to adding no extra headers.\n *\n * @param request - The outgoing request about to be sent.\n * @returns A map of header names to values; return `{}` when none are needed.\n */\n buildExtraHeaders?(request: HttpRequest<unknown>): Record<string, string>;\n /**\n * Called for every successful response that completed an actual network round trip.\n *\n * @remarks\n * Responses synthesized by {@link KitHttpConfig.offlineFallback} are produced after `catchError`\n * and therefore never reach this hook, so it observes genuine successes only. A typical use is to\n * reset an \"offline\" flag once connectivity is restored. Optional; defaults to a no-op.\n *\n * @param event - The successful `HttpResponse`.\n */\n onResponse?(event: HttpResponse<unknown>): void;\n /**\n * Decide whether to pass the request straight through, skipping auth, retry, and error handling.\n *\n * @remarks\n * Useful for external URLs such as S3 or a CDN. Optional; defaults to `false` (never bypass).\n *\n * @param request - The outgoing request.\n * @returns `true` to bypass the interceptor pipeline.\n */\n bypass?(request: HttpRequest<unknown>): boolean;\n /**\n * Provide an offline short-circuit when a request fails.\n *\n * @remarks\n * Returning a non-null observable replaces the error with that response (for example a queued\n * offline result). Optional; defaults to `null` (no fallback, normal error handling proceeds).\n *\n * @param request - The request that failed (after headers were applied).\n * @param error - The error response that triggered the fallback.\n * @returns A replacement event stream, or `null` for no fallback.\n */\n offlineFallback?(request: HttpRequest<unknown>, error: HttpErrorResponse): Observable<HttpEvent<unknown>> | null;\n /**\n * Side effect to run on a `401` response (for example an expired token).\n *\n * @remarks\n * Optional; defaults to a no-op.\n *\n * @param request - The request that received the `401`.\n */\n onUnauthorized?(request: HttpRequest<unknown>): void;\n /**\n * Side effect to run on a `403` response (a permission error).\n *\n * @remarks\n * Optional; defaults to a no-op.\n *\n * @param request - The request that received the `403`.\n */\n onForbidden?(request: HttpRequest<unknown>): void;\n /**\n * UX hook for network-originated errors while the device is connected.\n *\n * @remarks\n * Optional; defaults to a no-op. The kit ships {@link kitPresentReloadAlert} as the fleet's\n * canonical implementation of this hook.\n *\n * @param status - The HTTP status code, or a string descriptor for non-HTTP failures.\n * @returns Optionally a promise to await before continuing.\n */\n onNetworkError?(status: number | string): Promise<void> | void;\n /**\n * UX hook for `400` / `500` responses that carry a server-provided message.\n *\n * @remarks\n * Optional; defaults to a no-op.\n *\n * @param message - The message extracted from the error body.\n */\n onServerError?(message: string): void;\n}\n\n/**\n * Injection token that carries the {@link KitHttpConfig} to {@link kitAuthInterceptor}.\n */\nexport const KIT_HTTP_CONFIG = new InjectionToken<KitHttpConfig>('@rdlabo/ionic-angular-kit:http');\n\n/**\n * Wire the {@link kitAuthInterceptor} configuration into the application's dependency injection.\n *\n * @remarks\n * Register the interceptor itself separately via `provideHttpClient(withInterceptors([kitAuthInterceptor]))`.\n * The factory runs inside an injection context, so it may call `inject()`.\n *\n * @param configFactory - Factory that returns the {@link KitHttpConfig} for the application.\n * @returns Environment providers to add to the application bootstrap.\n *\n * @example\n * ```ts\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideHttpClient(withInterceptors([kitAuthInterceptor])),\n * provideKitHttp(() => {\n * const auth = inject(AuthService);\n * const overlay = inject(KitOverlayController);\n * return {\n * // Only getAuthHeaders is required; every other hook is optional and defaults to a no-op.\n * getAuthHeaders: async () => ({ Authorization: `Bearer ${await auth.token()}` }),\n * onUnauthorized: () => auth.signOut(),\n * onNetworkError: (status) =>\n * kitPresentReloadAlert(overlay, { header: 'Network error', message: `Reload? (${status})`, okText: 'Reload' }),\n * };\n * }),\n * ],\n * });\n * ```\n */\nexport const provideKitHttp = (configFactory: () => KitHttpConfig): EnvironmentProviders =>\n makeEnvironmentProviders([{ provide: KIT_HTTP_CONFIG, useFactory: configFactory }]);\n\n/**\n * Canonical functional HTTP interceptor that applies authentication, retries, and error handling.\n *\n * @remarks\n * Behavior, driven by the injected {@link KitHttpConfig}:\n *\n * 1. Requests for which `bypass` returns `true` are forwarded untouched.\n * 2. Otherwise the headers from `getAuthHeaders` and `buildExtraHeaders` are merged onto a cloned request.\n * 3. Failed requests are retried up to 2 times with a linearly increasing backoff of `500ms * (retryCount + 5)`,\n * except that `401` and any {@link NON_RETRYABLE_STATUSES | non-retryable status}\n * (`400`, `403`, `404`, `418`, `500`, `502`) are thrown immediately without retrying.\n * 4. On error, `offlineFallback` is consulted first; otherwise `401` calls `onUnauthorized`, `403`\n * calls `onForbidden`, network-class failures (anything other than `400`/`500`) call\n * `onNetworkError` when the device is connected, and `400`/`500` responses carrying a body\n * message call `onServerError`.\n *\n * @param request - The outgoing request.\n * @param next - The next handler in the interceptor chain.\n * @returns A stream of HTTP events for the (possibly modified, retried, or replaced) request.\n *\n * @example\n * ```ts\n * provideHttpClient(withInterceptors([kitAuthInterceptor]));\n * ```\n */\nexport const kitAuthInterceptor: HttpInterceptorFn = (request, next) => {\n const config = inject(KIT_HTTP_CONFIG);\n\n if (config.bypass?.(request)) {\n return next(request);\n }\n\n return from(config.getAuthHeaders(request)).pipe(\n mergeMap((authHeaders) => {\n const req = request.clone({ setHeaders: { ...authHeaders, ...config.buildExtraHeaders?.(request) } });\n\n return next(req).pipe(\n retry({\n count: 2,\n delay: (e: HttpErrorResponse, retryCount) => {\n if (e.status === 401) {\n return throwError(() => e);\n }\n if (NON_RETRYABLE_STATUSES.includes(e.status)) {\n return throwError(() => e);\n }\n return timer((retryCount + 5) * 500);\n },\n }),\n tap((event) => {\n if (event instanceof HttpResponse) {\n config.onResponse?.(event);\n }\n }),\n catchError((e: HttpErrorResponse) => {\n const fallback = config.offlineFallback?.(req, e);\n if (fallback) {\n return fallback;\n }\n if (e.status === 401) {\n config.onUnauthorized?.(req);\n } else if (e.status === 403) {\n config.onForbidden?.(req);\n } else if (![400, 500].includes(e.status)) {\n void Network.getStatus().then((status) => {\n if (status.connected) {\n config.onNetworkError?.(e.status);\n }\n });\n } else if (e.error?.message) {\n config.onServerError?.(e.error.message);\n }\n return throwError(() => e);\n }),\n );\n }),\n );\n};\n","/**\n * Merge a newly fetched page of items into an existing list by id, keeping the result sorted.\n *\n * Items are merged by the numeric `key` field. On a duplicate `key` the item from `arrayNew` wins\n * and replaces the one in `arrayOld`. Old items whose `key` falls *inside* the window spanned by\n * `arrayNew` (between its first and last `key`) are dropped, since the freshly fetched page is the\n * authoritative copy of that window; old items *outside* the window are kept. The merged items are\n * finally sorted by `key`, ascending or descending according to `order`.\n *\n * The algorithm is:\n * 1. If both inputs are empty, return an empty array.\n * 2. Read the first (`lead`) and last (`last`) `key` values of `arrayNew` as the bounds of the\n * page's window. Keep the old items whose `key` lies *outside* that window (at or beyond the\n * extremes) and drop those strictly inside it, handling both a high-to-low page (`lead > last`)\n * and a low-to-high page (`lead < last`). A single-value page (`lead === last`) keeps all old items.\n * 3. Remove old items whose `key` already exists in `arrayNew` (new wins on duplicates).\n * 4. Concatenate `arrayNew` with the surviving old items and sort by `key` in the requested\n * direction.\n *\n * @remarks\n * Designed for infinite-scroll / paginated list merging, where each fetched page may overlap the\n * previously held items and the server returns a contiguous, ordered window of records. The `key`\n * field is treated as a number for range comparison and sorting.\n *\n * @typeParam T - Element type of both arrays. Its `key` property must be a numeric value.\n * @param arrayOld - The previously accumulated list. May be empty or nullish-safe (empty when falsy).\n * @param arrayNew - The newly fetched page of items; its `key` range defines the window that its own\n * items replace, and its items take precedence on duplicates.\n * @param key - The property of `T` used as the unique, numeric id for matching, range filtering, and sorting.\n * @param order - Sort direction by `key`: `'ASC'` for ascending or `'DESC'` for descending. Defaults to `'DESC'`.\n * @returns A new array containing `arrayNew` merged with the out-of-window, non-duplicate old items, sorted by `key`.\n * @example\n * ```ts\n * interface Post {\n * id: number;\n * title: string;\n * }\n *\n * const loaded: Post[] = [\n * { id: 30, title: 'c' },\n * { id: 20, title: 'b' },\n * { id: 10, title: 'a' },\n * ];\n * const nextPage: Post[] = [\n * { id: 20, title: 'b (updated)' },\n * { id: 15, title: 'a.5' },\n * ];\n *\n * // Descending merge: id 30 lies above the new page's [15, 20] window so it is kept; id 20 is\n * // inside the window and is replaced by the new value; id 10 lies below the window and is kept.\n * const merged = arrayConcatById(loaded, nextPage, 'id', 'DESC');\n * // => [{ id: 30, title: 'c' }, { id: 20, title: 'b (updated)' }, { id: 15, title: 'a.5' }, { id: 10, title: 'a' }]\n * ```\n */\nexport const arrayConcatById = <T>(arrayOld: T[], arrayNew: T[], key: keyof T, order: 'ASC' | 'DESC' = 'DESC'): T[] => {\n if (!arrayNew.length && !arrayOld.length) {\n return [];\n }\n const lead = arrayNew[0][key] as number;\n const last = arrayNew[arrayNew.length - 1][key] as number;\n\n const filteredOld = (arrayOld || []).filter((vol) => {\n const value = vol[key] as number;\n return (lead > last && (value >= lead || value <= last)) || (lead < last && (value <= lead || value >= last)) || lead === last;\n });\n\n const oldData = filteredOld.filter((vol) => !arrayNew.some((element) => element[key] === vol[key]));\n const data = arrayNew.concat(oldData);\n\n const direction = order === 'ASC' ? 1 : -1;\n return data.sort((a, b) => {\n const x = a[key] as number;\n const y = b[key] as number;\n if (x > y) {\n return direction;\n }\n if (x < y) {\n return direction * -1;\n }\n return 0;\n });\n};\n","/*\n * Public API Surface of @rdlabo/ionic-angular-kit\n */\n\n// Storage: typed wrapper around the platform key/value store.\nexport * from './lib/storage/kit-storage.service';\n\n// Overlay: wrapper around the Ionic Modal / Toast / Alert controllers.\nexport * from './lib/overlay/overlay-config';\nexport * from './lib/overlay/kit-overlay.controller';\nexport * from './lib/overlay/reload-alert';\n\n// Directives.\nexport * from './lib/directives/autofill.directive';\n\n// Auth: functional route guards.\nexport * from './lib/auth/auth-guards';\n\n// HTTP: functional interceptor.\nexport * from './lib/http/kit-http.interceptor';\n\n// Utils: framework-agnostic pure helpers.\nexport * from './lib/utils/haptics';\nexport * from './lib/utils/array';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdlabo/ionic-angular-kit",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^21.0.0",
6
6
  "@angular/core": "^21.0.0",
@@ -252,6 +252,41 @@ declare class KitOverlayController {
252
252
  static ɵprov: i0.ɵɵInjectableDeclaration<KitOverlayController>;
253
253
  }
254
254
 
255
+ /**
256
+ * Present the fleet's canonical "network error → offer to reload" confirmation.
257
+ *
258
+ * @remarks
259
+ * Folds together the three copy-pasted concerns that every app duplicated around a network failure:
260
+ *
261
+ * 1. Suppress stacking — if an `ion-alert` is already on screen, do nothing.
262
+ * 2. Ask the user to confirm via {@link KitOverlayController.alertConfirm}.
263
+ * 3. Call `location.reload()` when the user confirms.
264
+ *
265
+ * All user-facing text (`header`, `message`, `okText`) is supplied by the caller so the kit stays
266
+ * free of any hardcoded i18n. Typically wired from {@link KitHttpConfig.onNetworkError}, but it can
267
+ * be called from anywhere an app wants the same behavior (e.g. an offline fallback).
268
+ *
269
+ * @param overlay - the kit overlay controller used to present the confirmation
270
+ * @param options - alert content plus the confirm-button text (usually "Refresh")
271
+ * @returns a Promise that resolves once the alert is dismissed (the page reloads on confirm)
272
+ * @example
273
+ * ```ts
274
+ * provideKitHttp(() => {
275
+ * const overlay = inject(KitOverlayController);
276
+ * return {
277
+ * getAuthHeaders: async () => ({ ... }),
278
+ * onNetworkError: (status) =>
279
+ * kitPresentReloadAlert(overlay, {
280
+ * header: 'ネットワークエラー',
281
+ * message: `通信できませんでした。リフレッシュしますか?(${status})`,
282
+ * okText: 'リフレッシュ',
283
+ * }),
284
+ * };
285
+ * });
286
+ * ```
287
+ */
288
+ declare const kitPresentReloadAlert: (overlay: KitOverlayController, options: KitAlertConfirmOptions) => Promise<void>;
289
+
255
290
  /**
256
291
  * Work around iOS `ion-input` autofill values not propagating to the Angular form model.
257
292
  *
@@ -455,6 +490,10 @@ declare const kitRequireAuthorizedGuard: CanActivateFn;
455
490
  * The interceptor fixes the retry policy (up to 2 retries with a linearly increasing backoff, plus immediate
456
491
  * throw on `401` and on every {@link NON_RETRYABLE_STATUSES | non-retryable status}) and the overall
457
492
  * control flow. Only the hooks below are application-specific.
493
+ *
494
+ * Only {@link KitHttpConfig.getAuthHeaders} is required — it has no safe default. Every other hook is
495
+ * optional and defaults to a no-op (or `{}` / `false` / `null` as appropriate), so an app configures
496
+ * only the behavior that actually differs from the canonical baseline.
458
497
  */
459
498
  interface KitHttpConfig {
460
499
  /**
@@ -467,71 +506,84 @@ interface KitHttpConfig {
467
506
  /**
468
507
  * Produce additional headers for the outgoing request.
469
508
  *
509
+ * @remarks
510
+ * Optional; defaults to adding no extra headers.
511
+ *
470
512
  * @param request - The outgoing request about to be sent.
471
513
  * @returns A map of header names to values; return `{}` when none are needed.
472
514
  */
473
- buildExtraHeaders(request: HttpRequest<unknown>): Record<string, string>;
515
+ buildExtraHeaders?(request: HttpRequest<unknown>): Record<string, string>;
474
516
  /**
475
517
  * Called for every successful response that completed an actual network round trip.
476
518
  *
477
519
  * @remarks
478
520
  * Responses synthesized by {@link KitHttpConfig.offlineFallback} are produced after `catchError`
479
521
  * and therefore never reach this hook, so it observes genuine successes only. A typical use is to
480
- * reset an "offline" flag once connectivity is restored. Implement as a no-op when not needed.
522
+ * reset an "offline" flag once connectivity is restored. Optional; defaults to a no-op.
481
523
  *
482
524
  * @param event - The successful `HttpResponse`.
483
525
  */
484
- onResponse(event: HttpResponse<unknown>): void;
526
+ onResponse?(event: HttpResponse<unknown>): void;
485
527
  /**
486
528
  * Decide whether to pass the request straight through, skipping auth, retry, and error handling.
487
529
  *
488
530
  * @remarks
489
- * Useful for external URLs such as S3 or a CDN. Return `false` to always apply the standard pipeline.
531
+ * Useful for external URLs such as S3 or a CDN. Optional; defaults to `false` (never bypass).
490
532
  *
491
533
  * @param request - The outgoing request.
492
534
  * @returns `true` to bypass the interceptor pipeline.
493
535
  */
494
- bypass(request: HttpRequest<unknown>): boolean;
536
+ bypass?(request: HttpRequest<unknown>): boolean;
495
537
  /**
496
538
  * Provide an offline short-circuit when a request fails.
497
539
  *
498
540
  * @remarks
499
541
  * Returning a non-null observable replaces the error with that response (for example a queued
500
- * offline result). Return `null` to let normal error handling proceed.
542
+ * offline result). Optional; defaults to `null` (no fallback, normal error handling proceeds).
501
543
  *
502
544
  * @param request - The request that failed (after headers were applied).
503
545
  * @param error - The error response that triggered the fallback.
504
546
  * @returns A replacement event stream, or `null` for no fallback.
505
547
  */
506
- offlineFallback(request: HttpRequest<unknown>, error: HttpErrorResponse): Observable<HttpEvent<unknown>> | null;
548
+ offlineFallback?(request: HttpRequest<unknown>, error: HttpErrorResponse): Observable<HttpEvent<unknown>> | null;
507
549
  /**
508
550
  * Side effect to run on a `401` response (for example an expired token).
509
551
  *
552
+ * @remarks
553
+ * Optional; defaults to a no-op.
554
+ *
510
555
  * @param request - The request that received the `401`.
511
556
  */
512
- onUnauthorized(request: HttpRequest<unknown>): void;
557
+ onUnauthorized?(request: HttpRequest<unknown>): void;
513
558
  /**
514
559
  * Side effect to run on a `403` response (a permission error).
515
560
  *
516
561
  * @remarks
517
- * Implement as a no-op when not needed.
562
+ * Optional; defaults to a no-op.
518
563
  *
519
564
  * @param request - The request that received the `403`.
520
565
  */
521
- onForbidden(request: HttpRequest<unknown>): void;
566
+ onForbidden?(request: HttpRequest<unknown>): void;
522
567
  /**
523
568
  * UX hook for network-originated errors while the device is connected.
524
569
  *
570
+ * @remarks
571
+ * Optional; defaults to a no-op. The kit ships {@link kitPresentReloadAlert} as the fleet's
572
+ * canonical implementation of this hook.
573
+ *
525
574
  * @param status - The HTTP status code, or a string descriptor for non-HTTP failures.
526
575
  * @returns Optionally a promise to await before continuing.
527
576
  */
528
- onNetworkError(status: number | string): Promise<void> | void;
577
+ onNetworkError?(status: number | string): Promise<void> | void;
529
578
  /**
530
579
  * UX hook for `400` / `500` responses that carry a server-provided message.
531
580
  *
581
+ * @remarks
582
+ * Optional; defaults to a no-op.
583
+ *
532
584
  * @param message - The message extracted from the error body.
533
585
  */
534
- onServerError(message: string): void;
586
+ onServerError?(message: string): void;
535
587
  }
536
588
  /**
537
589
  * Injection token that carries the {@link KitHttpConfig} to {@link kitAuthInterceptor}.
@@ -554,16 +606,13 @@ declare const KIT_HTTP_CONFIG: InjectionToken<KitHttpConfig>;
554
606
  * provideHttpClient(withInterceptors([kitAuthInterceptor])),
555
607
  * provideKitHttp(() => {
556
608
  * const auth = inject(AuthService);
609
+ * const overlay = inject(KitOverlayController);
557
610
  * return {
611
+ * // Only getAuthHeaders is required; every other hook is optional and defaults to a no-op.
558
612
  * getAuthHeaders: async () => ({ Authorization: `Bearer ${await auth.token()}` }),
559
- * buildExtraHeaders: () => ({}),
560
- * onResponse: () => {},
561
- * bypass: (request) => request.url.startsWith('https://cdn.example.com'),
562
- * offlineFallback: () => null,
563
613
  * onUnauthorized: () => auth.signOut(),
564
- * onForbidden: () => {},
565
- * onNetworkError: () => {},
566
- * onServerError: (message) => console.error(message),
614
+ * onNetworkError: (status) =>
615
+ * kitPresentReloadAlert(overlay, { header: 'Network error', message: `Reload? (${status})`, okText: 'Reload' }),
567
616
  * };
568
617
  * }),
569
618
  * ],
@@ -680,5 +729,5 @@ declare const kitImpact: (style?: ImpactStyle) => Promise<void>;
680
729
  */
681
730
  declare const arrayConcatById: <T>(arrayOld: T[], arrayNew: T[], key: keyof T, order?: "ASC" | "DESC") => T[];
682
731
 
683
- export { KIT_AUTH_CONFIG, KIT_HTTP_CONFIG, KIT_OVERLAY_CONFIG, KitAutofillDirective, KitOverlayController, KitStorageService, arrayConcatById, kitAuthInterceptor, kitImpact, kitRequireAuthorizedGuard, kitRequireConfirmingGuard, kitRequiredUnauthorizedGuard, provideKitAuth, provideKitHttp, provideKitOverlay };
732
+ export { KIT_AUTH_CONFIG, KIT_HTTP_CONFIG, KIT_OVERLAY_CONFIG, KitAutofillDirective, KitOverlayController, KitStorageService, arrayConcatById, kitAuthInterceptor, kitImpact, kitPresentReloadAlert, kitRequireAuthorizedGuard, kitRequireConfirmingGuard, kitRequiredUnauthorizedGuard, provideKitAuth, provideKitHttp, provideKitOverlay };
684
733
  export type { KitAlertCloseOptions, KitAlertConfirmOptions, KitAuthConfig, KitAuthRedirects, KitAuthState, KitHttpConfig, KitLabels, KitModalPresentOptions, KitOverlayConfig };