@rdlabo/ionic-angular-kit 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,11 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, Injectable, InjectionToken, makeEnvironmentProviders, ElementRef, Directive } from '@angular/core';
2
+ import { inject, Injectable, InjectionToken, makeEnvironmentProviders, ElementRef, Injector, input, computed, HostListener, Directive } from '@angular/core';
3
3
  import { Storage } from '@ionic/storage-angular';
4
4
  import { ModalController, PopoverController, ToastController, AlertController, LoadingController, NavController } from '@ionic/angular/standalone';
5
5
  import { Capacitor } from '@capacitor/core';
6
6
  import { Keyboard } from '@capacitor/keyboard';
7
7
  import { ImpactStyle, Haptics } from '@capacitor/haptics';
8
+ import { FORM_FIELD } from '@angular/forms/signals';
8
9
  import { Router } from '@angular/router';
9
10
  import { map, mergeMap, catchError, timeout, tap } from 'rxjs/operators';
10
11
  import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
@@ -100,6 +101,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
100
101
  }]
101
102
  }] });
102
103
 
104
+ /**
105
+ * Remember the email a user last entered on the sign-in form so the field can be prefilled next
106
+ * time (the password is never stored).
107
+ *
108
+ * @remarks
109
+ * Storage-agnostic: the helpers take a minimal async key/value store that {@link KitStorageService}
110
+ * satisfies structurally, so they stay pure and unit-testable without DI. `kitRememberEmail`
111
+ * **validates the address before persisting** — a malformed or partial string is silently ignored,
112
+ * so a garbage entry (or a fat-fingered attempt) never becomes the next prefill. A well-formed email
113
+ * that later fails to sign in is still remembered by design: the user simply re-enters/corrects it.
114
+ *
115
+ * These live in the main entry (next to {@link KitStorageService}) rather than in `auth-firebase`,
116
+ * so the `KitAutofillDirective` can consume them without the main entry depending on the Firebase
117
+ * entry.
118
+ */
119
+ /** Storage key under which the last entered sign-in email is kept. */
120
+ const KIT_LAST_AUTH_EMAIL_KEY = 'kit:last-auth-email';
121
+ /**
122
+ * Angular's `Validators.email` pattern — a pragmatic, RFC-5322-inspired address check. Kept in sync
123
+ * with `@angular/forms` so a value we persist would also pass the form's own email validation.
124
+ */
125
+ const EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(?:\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
126
+ /** Whether `email` is a well-formed address (matches `@angular/forms` `Validators.email`). */
127
+ const kitIsValidEmail = (email) => EMAIL_REGEXP.test(email);
128
+ /**
129
+ * Persist the entered email for next time — but only when it is a well-formed address.
130
+ *
131
+ * @param store - the app's storage (e.g. `KitStorageService`)
132
+ * @param email - the entered email; ignored (not stored) when malformed
133
+ * @returns `true` when the value passed validation and was stored, `false` when it was ignored
134
+ */
135
+ const kitRememberEmail = async (store, email) => {
136
+ if (!kitIsValidEmail(email)) {
137
+ return false;
138
+ }
139
+ await store.set(KIT_LAST_AUTH_EMAIL_KEY, email);
140
+ return true;
141
+ };
142
+ /**
143
+ * Recall the last remembered email.
144
+ *
145
+ * @param store - the app's storage (e.g. `KitStorageService`)
146
+ * @returns the stored email, or `null` when none has been remembered
147
+ */
148
+ const kitRecallEmail = (store) => store.get(KIT_LAST_AUTH_EMAIL_KEY);
149
+ /**
150
+ * Forget the remembered email.
151
+ *
152
+ * @remarks
153
+ * Called when the user intentionally clears or invalidates the field, so a stale prefill is not
154
+ * resurrected next time.
155
+ *
156
+ * @param store - the app's storage (e.g. `KitStorageService`)
157
+ */
158
+ const kitForgetEmail = (store) => store.remove(KIT_LAST_AUTH_EMAIL_KEY);
159
+
103
160
  /**
104
161
  * Injection token carrying the {@link KitOverlayConfig} for `KitOverlayController`.
105
162
  *
@@ -661,41 +718,73 @@ const kitPresentLanguageActionSheet = async (actionSheetCtrl, options) => {
661
718
  };
662
719
 
663
720
  /**
664
- * Work around iOS `ion-input` autofill values not propagating to the Angular form model.
665
- *
666
- * On iOS, when the browser autofills an `ion-input` (for example a saved password), the value
667
- * is written to the underlying native `<input>` element but the corresponding `change` event is
668
- * not forwarded to the host `ion-input`, so the Angular form control (and `ngModel`) never sees
669
- * the autofilled value. This directive listens for the first `change` event on the inner input
670
- * element and mirrors its value back onto the host element, restoring two-way binding.
671
- *
672
- * Apply it to any `ion-input` that participates in a form and may be autofilled by attaching the
673
- * `rdlaboAutofill` attribute.
674
- *
675
- * @remarks
676
- * The directive is a no-op on every platform other than iOS, where the value already propagates
677
- * correctly. A short timeout is used because `ion-input` creates its inner `<input>` element
678
- * asynchronously after the host element is initialized.
721
+ * Input conveniences for an `ion-input` in a sign-in / sign-up form, applied via the `kitAuthInput`
722
+ * attribute. The mode is a typed union so a typo is a compile-time error.
723
+ *
724
+ * 1. **iOS autofill propagation (always on, every mode).** On iOS, when the browser autofills an
725
+ * `ion-input` (e.g. a saved password) the value is written to the underlying native `<input>`
726
+ * but the `change` event is not forwarded to the host `ion-input`, so the Angular form model
727
+ * never sees it. This directive mirrors the first inner-input `change` back onto the host,
728
+ * restoring two-way binding. It is a no-op on every other platform.
729
+ *
730
+ * 2. **Remember / prefill the email (`'email'` and `'email-remember'`).**
731
+ * - `'email'` (sign-in) — on init recalls the last entered email and, *only while the field is
732
+ * still empty*, seeds it via the bound Signal Forms field, so a browser/OS autofill the user
733
+ * actually picks always wins over the prefill. On every committed change (`ionChange`) it
734
+ * remembers a well-formed address, or **forgets** the stored one when the field is cleared
735
+ * (empty after trim) or holds an invalid address — the user intentionally removing the prefill.
736
+ * - `'email-remember'` (sign-up) — remembers a well-formed address on change, but never prefills
737
+ * and never forgets. So the first-sign-up address is captured without pre-populating a
738
+ * stranger's email into a new-account form, and clearing the field does not wipe an email
739
+ * remembered elsewhere.
740
+ *
741
+ * A well-formed email is persisted even if it later fails to sign in — by design; the user simply
742
+ * re-enters it. A malformed/partial entry is never stored (see {@link kitRememberEmail}).
679
743
  *
680
744
  * @example
681
745
  * ```html
682
- * <ion-input rdlaboAutofill type="password" [(ngModel)]="password"></ion-input>
746
+ * <!-- sign-in email: iOS autofill + prefill + remember/forget -->
747
+ * <ion-input type="email" autocomplete="email" kitAuthInput="email" [formField]="form.email" />
748
+ * <!-- sign-up email: remember only -->
749
+ * <ion-input type="email" autocomplete="email" kitAuthInput="email-remember" [formField]="form.email" />
750
+ * <!-- password: iOS autofill propagation only -->
751
+ * <ion-input type="password" autocomplete="current-password" kitAuthInput="autofill" [formField]="form.password" />
683
752
  * ```
753
+ *
754
+ * @remarks
755
+ * Prefill writes through the Signal Forms `FORM_FIELD` bound on the same element; with no such field
756
+ * (e.g. `ngModel` / reactive forms) prefill is skipped — remember/forget still work off the DOM
757
+ * event. Storage is resolved lazily so the iOS mirror still works in apps without `@ionic/storage`.
684
758
  */
685
- class KitAutofillDirective {
686
- #el = inject(ElementRef);
687
- constructor() { }
759
+ class KitAuthInputDirective {
760
+ #el;
761
+ #injector;
762
+ /** The Signal Forms field bound on this same element, if any (used to seed the prefill). */
763
+ #field;
764
+ constructor() {
765
+ this.#el = inject(ElementRef);
766
+ this.#injector = inject(Injector);
767
+ /** The Signal Forms field bound on this same element, if any (used to seed the prefill). */
768
+ this.#field = inject(FORM_FIELD, { optional: true, self: true });
769
+ /** Mode selector; see {@link KitAuthInputMode}. */
770
+ this.kitAuthInput = input('autofill', ...(ngDevMode ? [{ debugName: "kitAuthInput" }] : /* istanbul ignore next */ []));
771
+ /** Whether this instance persists the entered email (`'email'` or `'email-remember'`). */
772
+ this.#remembers = computed(() => {
773
+ const mode = this.kitAuthInput();
774
+ return mode === 'email' || mode === 'email-remember';
775
+ }, ...(ngDevMode ? [{ debugName: "#remembers" }] : /* istanbul ignore next */ []));
776
+ /** Whether this instance prefills the field from storage (`'email'` only). */
777
+ this.#prefills = computed(() => this.kitAuthInput() === 'email', ...(ngDevMode ? [{ debugName: "#prefills" }] : /* istanbul ignore next */ []));
778
+ /** Whether clearing/invalidating the field forgets the stored email (`'email'` only). */
779
+ this.#forgetsOnClear = computed(() => this.kitAuthInput() === 'email', ...(ngDevMode ? [{ debugName: "#forgetsOnClear" }] : /* istanbul ignore next */ []));
780
+ }
688
781
  /**
689
- * Register the iOS autofill workaround once the directive is initialized.
690
- *
691
- * Returns immediately on non-iOS platforms. On iOS, after a short delay it attaches a one-shot,
692
- * passive `change` listener to the inner `<input>` element that `ion-input` renders, copying the
693
- * autofilled value back onto the host element so the Angular form model stays in sync. Any error
694
- * while locating the inner input (for example if the element is not yet present) is swallowed.
695
- *
696
- * @returns Nothing.
782
+ * Register the iOS autofill workaround and, in `'email'` mode, seed the field from storage.
697
783
  */
698
784
  ngOnInit() {
785
+ if (this.#prefills()) {
786
+ void this.#prefillEmail();
787
+ }
699
788
  if (Capacitor.getPlatform() !== 'ios') {
700
789
  return;
701
790
  }
@@ -714,16 +803,75 @@ class KitAutofillDirective {
714
803
  }
715
804
  }, 100); // Need some time for the ion-input to create the input element
716
805
  }
717
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: KitAutofillDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
718
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.17", type: KitAutofillDirective, isStandalone: true, selector: "[rdlaboAutofill]", ngImport: i0 }); }
806
+ /**
807
+ * On every committed change (`ionChange`): remember a well-formed email, or only in the
808
+ * prefilling `'email'` (sign-in) mode — forget the stored one when the field is cleared or invalid,
809
+ * which is the user intentionally removing the prefill. `'email-remember'` never forgets, so
810
+ * editing the sign-up field cannot wipe an email remembered from sign-in.
811
+ */
812
+ onIonChange(event) {
813
+ if (!this.#remembers()) {
814
+ return;
815
+ }
816
+ const storage = this.#resolveStorage();
817
+ if (!storage) {
818
+ return;
819
+ }
820
+ const value = event.detail?.value ?? '';
821
+ if (value.trim().length > 0 && kitIsValidEmail(value)) {
822
+ void kitRememberEmail(storage, value);
823
+ }
824
+ else if (this.#forgetsOnClear()) {
825
+ void kitForgetEmail(storage);
826
+ }
827
+ }
828
+ /** Whether this instance persists the entered email (`'email'` or `'email-remember'`). */
829
+ #remembers;
830
+ /** Whether this instance prefills the field from storage (`'email'` only). */
831
+ #prefills;
832
+ /** Whether clearing/invalidating the field forgets the stored email (`'email'` only). */
833
+ #forgetsOnClear;
834
+ /** Resolve `KitStorageService` lazily; returns `null` when storage is not configured. */
835
+ #resolveStorage() {
836
+ try {
837
+ return this.#injector.get(KitStorageService);
838
+ }
839
+ catch {
840
+ return null;
841
+ }
842
+ }
843
+ /**
844
+ * Seed the bound field with the remembered email, but only while it is still empty — so a value
845
+ * the browser/OS autofills during the async recall is not clobbered.
846
+ */
847
+ async #prefillEmail() {
848
+ const field = this.#field;
849
+ if (!field) {
850
+ return;
851
+ }
852
+ const storage = this.#resolveStorage();
853
+ if (!storage) {
854
+ return;
855
+ }
856
+ const last = await kitRecallEmail(storage);
857
+ const state = field.state();
858
+ if (last && !state.value()) {
859
+ state.value.set(last);
860
+ }
861
+ }
862
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: KitAuthInputDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
863
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.17", type: KitAuthInputDirective, isStandalone: true, selector: "[kitAuthInput]", inputs: { kitAuthInput: { classPropertyName: "kitAuthInput", publicName: "kitAuthInput", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "ionChange": "onIonChange($event)" } }, ngImport: i0 }); }
719
864
  }
720
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: KitAutofillDirective, decorators: [{
865
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: KitAuthInputDirective, decorators: [{
721
866
  type: Directive,
722
867
  args: [{
723
- selector: '[rdlaboAutofill]',
868
+ selector: '[kitAuthInput]',
724
869
  standalone: true,
725
870
  }]
726
- }], ctorParameters: () => [] });
871
+ }], ctorParameters: () => [], propDecorators: { kitAuthInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "kitAuthInput", required: false }] }], onIonChange: [{
872
+ type: HostListener,
873
+ args: ['ionChange', ['$event']]
874
+ }] } });
727
875
 
728
876
  const keyboardWillShow = (elementRef, type) => Keyboard.addListener('keyboardWillShow', (info) => {
729
877
  if (Capacitor.getPlatform() === 'android') {
@@ -1386,5 +1534,5 @@ const kitCreateDidEnter = (el) => {
1386
1534
  * Generated bundle index. Do not edit.
1387
1535
  */
1388
1536
 
1389
- export { KIT_AUTH_CONFIG, KIT_HTTP_CONFIG, KIT_OVERLAY_CONFIG, KitAutofillDirective, KitLoadingController, KitOverlayController, KitReloadAlertController, KitStorageService, arrayConcatById, disableHandler, kitAuthInterceptor, kitChangeEventDisabled, kitCreateDidEnter, kitImpact, kitKeyboardInit, kitPresentAuthFailedAlert, kitPresentLanguageActionSheet, kitRequireAuthorizedGuard, kitRequireConfirmingGuard, kitRequiredUnauthorizedGuard, objectEqual, provideKitAuth, provideKitHttp, provideKitOverlay };
1537
+ export { KIT_AUTH_CONFIG, KIT_HTTP_CONFIG, KIT_LAST_AUTH_EMAIL_KEY, KIT_OVERLAY_CONFIG, KitAuthInputDirective, KitLoadingController, KitOverlayController, KitReloadAlertController, KitStorageService, arrayConcatById, disableHandler, kitAuthInterceptor, kitChangeEventDisabled, kitCreateDidEnter, kitForgetEmail, kitImpact, kitIsValidEmail, kitKeyboardInit, kitPresentAuthFailedAlert, kitPresentLanguageActionSheet, kitRecallEmail, kitRememberEmail, kitRequireAuthorizedGuard, kitRequireConfirmingGuard, kitRequiredUnauthorizedGuard, objectEqual, provideKitAuth, provideKitHttp, provideKitOverlay };
1390
1538
  //# sourceMappingURL=rdlabo-ionic-angular-kit.mjs.map