@sneat/auth-ui 0.1.0

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.
Files changed (43) hide show
  1. package/eslint.config.cjs +7 -0
  2. package/ng-package.json +7 -0
  3. package/package.json +19 -0
  4. package/project.json +38 -0
  5. package/src/index.ts +3 -0
  6. package/src/lib/components/auth-menu-item/auth-menu-item.component.html +59 -0
  7. package/src/lib/components/auth-menu-item/auth-menu-item.component.spec.ts +5 -0
  8. package/src/lib/components/auth-menu-item/auth-menu-item.component.ts +116 -0
  9. package/src/lib/components/index.ts +3 -0
  10. package/src/lib/components/user-auth-providers/user-auth-accounts.component.html +127 -0
  11. package/src/lib/components/user-auth-providers/user-auth-accounts.component.spec.ts +47 -0
  12. package/src/lib/components/user-auth-providers/user-auth-accounts.component.ts +109 -0
  13. package/src/lib/components/user-auth-providers/user-auth-provider-status.html +42 -0
  14. package/src/lib/components/user-auth-providers/user-auth-provider-status.ts +172 -0
  15. package/src/lib/components/user-required-fields/index.ts +2 -0
  16. package/src/lib/components/user-required-fields/user-required-fields-modal.component.html +64 -0
  17. package/src/lib/components/user-required-fields/user-required-fields-modal.component.spec.ts +45 -0
  18. package/src/lib/components/user-required-fields/user-required-fields-modal.component.ts +141 -0
  19. package/src/lib/components/user-required-fields/user-required-fields.service.spec.ts +335 -0
  20. package/src/lib/components/user-required-fields/user-required-fields.service.ts +23 -0
  21. package/src/lib/pages/login-page/email-login-form/email-login-form.component.html +165 -0
  22. package/src/lib/pages/login-page/email-login-form/email-login-form.component.spec.ts +59 -0
  23. package/src/lib/pages/login-page/email-login-form/email-login-form.component.ts +356 -0
  24. package/src/lib/pages/login-page/index.ts +3 -0
  25. package/src/lib/pages/login-page/login-page.component.html +146 -0
  26. package/src/lib/pages/login-page/login-page.component.spec.ts +5 -0
  27. package/src/lib/pages/login-page/login-page.component.ts +209 -0
  28. package/src/lib/pages/login-page/login-with-telegram.component.spec.ts +42 -0
  29. package/src/lib/pages/login-page/login-with-telegram.component.ts +82 -0
  30. package/src/lib/pages/login-page/sneat-auth-with-telegram.service.spec.ts +31 -0
  31. package/src/lib/pages/login-page/sneat-auth-with-telegram.service.ts +56 -0
  32. package/src/lib/pages/sign-in-from-email-link/sign-in-from-email-link-page.component.html +35 -0
  33. package/src/lib/pages/sign-in-from-email-link/sign-in-from-email-link-page.component.spec.ts +43 -0
  34. package/src/lib/pages/sign-in-from-email-link/sign-in-from-email-link-page.component.ts +70 -0
  35. package/src/lib/pages/sneat-auth-routing.module.ts +25 -0
  36. package/src/lib/pipes/person-names.pipe.spec.ts +317 -0
  37. package/src/lib/pipes/person-names.pipe.ts +31 -0
  38. package/src/test-setup.ts +3 -0
  39. package/tsconfig.json +13 -0
  40. package/tsconfig.lib.json +19 -0
  41. package/tsconfig.lib.prod.json +7 -0
  42. package/tsconfig.spec.json +31 -0
  43. package/vite.config.mts +10 -0
@@ -0,0 +1,172 @@
1
+ import {
2
+ Component,
3
+ EventEmitter,
4
+ input,
5
+ Output,
6
+ signal,
7
+ computed,
8
+ OnDestroy,
9
+ inject,
10
+ } from '@angular/core';
11
+ import {
12
+ IonButton,
13
+ IonButtons,
14
+ IonIcon,
15
+ IonItem,
16
+ IonLabel,
17
+ IonSpinner,
18
+ } from '@ionic/angular/standalone';
19
+ import {
20
+ AuthProviderID,
21
+ ISneatAuthUser,
22
+ SneatAuthStateService,
23
+ } from '@sneat/auth-core';
24
+ import { ErrorLogger, IErrorLogger } from '@sneat/core';
25
+ import { Subject, takeUntil } from 'rxjs';
26
+
27
+ type IonicColor =
28
+ | 'primary'
29
+ | 'secondary'
30
+ | 'tertiary'
31
+ | 'success'
32
+ | 'warning'
33
+ | 'danger'
34
+ | 'light'
35
+ | 'medium'
36
+ | 'dark';
37
+
38
+ interface provider {
39
+ readonly id: AuthProviderID;
40
+ readonly title: string;
41
+ readonly icon: string;
42
+ readonly color: IonicColor;
43
+ }
44
+
45
+ @Component({
46
+ selector: 'sneat-user-auth-provider-status',
47
+ templateUrl: './user-auth-provider-status.html',
48
+ imports: [IonItem, IonIcon, IonLabel, IonButtons, IonButton, IonSpinner],
49
+ })
50
+ export class UserAuthAProviderStatusComponent implements OnDestroy {
51
+ private readonly errorLogger = inject<IErrorLogger>(ErrorLogger);
52
+ private readonly authStateService = inject(SneatAuthStateService);
53
+
54
+ private readonly $destroyed = new Subject<void>();
55
+ public providerID = input.required<AuthProviderID>();
56
+ protected readonly provider = computed<provider>(() => {
57
+ const id = this.providerID();
58
+ switch (id) {
59
+ case 'facebook.com':
60
+ return {
61
+ id: id,
62
+ title: 'Facebook',
63
+ icon: 'logo-facebook',
64
+ color: 'primary',
65
+ };
66
+ case 'google.com':
67
+ return {
68
+ id,
69
+ title: 'Google',
70
+ icon: 'logo-google',
71
+ color: 'danger',
72
+ };
73
+ case 'apple.com':
74
+ return {
75
+ id,
76
+ title: 'Apple',
77
+ icon: 'logo-apple',
78
+ color: 'dark',
79
+ };
80
+ case 'microsoft.com':
81
+ return {
82
+ id,
83
+ title: 'Microsoft',
84
+ icon: 'logo-windows',
85
+ color: 'secondary',
86
+ };
87
+ case undefined:
88
+ throw new Error('Undefined provider ID');
89
+ default:
90
+ return {
91
+ id,
92
+ title: id,
93
+ icon: 'help-circle',
94
+ color: 'medium',
95
+ };
96
+ }
97
+ });
98
+
99
+ readonly signingInWith = input.required<AuthProviderID | undefined>();
100
+
101
+ @Output() readonly signingInWithChange = new EventEmitter<
102
+ AuthProviderID | undefined
103
+ >();
104
+
105
+ protected readonly currentUser = signal<ISneatAuthUser | null | undefined>(
106
+ undefined,
107
+ );
108
+
109
+ constructor() {
110
+ this.authStateService.authUser
111
+ .pipe(takeUntil(this.$destroyed))
112
+ .subscribe((authUser) => {
113
+ this.currentUser.set(authUser);
114
+ });
115
+ }
116
+
117
+ protected readonly isSigningIn = computed(
118
+ () => this.signingInWith() === this.providerID(),
119
+ );
120
+
121
+ protected readonly isDisabled = computed(() => {
122
+ const signingInWith = this.signingInWith();
123
+ return !!signingInWith;
124
+ });
125
+
126
+ readonly authProviderUserInfo = computed(() => {
127
+ const providerID = this.providerID();
128
+ return this.currentUser()?.providerData?.find(
129
+ (pd) => pd.providerId == providerID,
130
+ );
131
+ });
132
+
133
+ readonly isConnected = computed(() => !!this.authProviderUserInfo());
134
+
135
+ protected connect(): void {
136
+ const providerID = this.providerID();
137
+ if (!providerID) {
138
+ throw new Error('auth providerID is not set');
139
+ }
140
+ this.signingInWithChange.emit(providerID);
141
+ this.authStateService
142
+ .linkWith(providerID)
143
+ .then(() => {
144
+ this.signingInWithChange.emit(undefined);
145
+ })
146
+ .catch((e) => {
147
+ console.error('Failed to connect', e);
148
+ this.signingInWithChange.emit(undefined);
149
+ });
150
+ }
151
+
152
+ protected disconnect(): void {
153
+ const providerID = this.providerID();
154
+ if (!providerID) {
155
+ this.errorLogger.logError('auth providerID is not set');
156
+ return;
157
+ }
158
+ this.authStateService
159
+ .unlinkAuthProvider(providerID)
160
+ .then(() => this.signingInWithChange.emit(undefined))
161
+ .catch((err) => {
162
+ this.signingInWithChange.emit(undefined);
163
+ this.errorLogger.logError(err);
164
+ });
165
+ this.signingInWithChange.emit(providerID);
166
+ }
167
+
168
+ public ngOnDestroy(): void {
169
+ this.$destroyed.next();
170
+ this.$destroyed.complete();
171
+ }
172
+ }
@@ -0,0 +1,2 @@
1
+ export * from './user-required-fields-modal.component';
2
+ export * from './user-required-fields.service';
@@ -0,0 +1,64 @@
1
+ <ion-header>
2
+ <ion-toolbar color="primary">
3
+ <ion-title>About me</ion-title>
4
+ <ion-buttons slot="end">
5
+ <ion-button (click)="close()">
6
+ <ion-icon name="close-outline" />
7
+ </ion-button>
8
+ </ion-buttons>
9
+ </ion-toolbar>
10
+ </ion-header>
11
+ <ion-content>
12
+ <form (ngSubmit)="save()" [formGroup]="form">
13
+ <ion-item>
14
+ <ion-input
15
+ label="First name"
16
+ name="first_name"
17
+ type="text"
18
+ required
19
+ max="20"
20
+ [formControl]="firstName"
21
+ />
22
+ </ion-item>
23
+ <ion-item>
24
+ <ion-input
25
+ label="Last name"
26
+ name="last_name"
27
+ type="text"
28
+ required
29
+ max="20"
30
+ [formControl]="lastName"
31
+ />
32
+ </ion-item>
33
+ @if (!gender.value) {
34
+ <ion-item-divider>
35
+ <ion-label>Gender</ion-label>
36
+ </ion-item-divider>
37
+ }
38
+ <sneat-select-from-list
39
+ label="Gender"
40
+ [items]="genders"
41
+ [formControl]="gender"
42
+ />
43
+
44
+ @if (gender.value || ageGroup.value) {
45
+ @if (!ageGroup.value) {
46
+ <ion-item-divider>
47
+ <ion-label>Age group</ion-label>
48
+ </ion-item-divider>
49
+ }
50
+ <sneat-select-from-list
51
+ label="Age group"
52
+ [items]="ageGroups"
53
+ [formControl]="ageGroup"
54
+ />
55
+ }
56
+ </form>
57
+ </ion-content>
58
+ <ion-footer>
59
+ <ion-toolbar>
60
+ <ion-buttons slot="end">
61
+ <ion-button [disabled]="submitting" (click)="save()">Save</ion-button>
62
+ </ion-buttons>
63
+ </ion-toolbar>
64
+ </ion-footer>
@@ -0,0 +1,45 @@
1
+ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
2
+ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
3
+ import { ModalController } from '@ionic/angular/standalone';
4
+ import { SneatUserService, UserRecordService } from '@sneat/auth-core';
5
+ import { ErrorLogger } from '@sneat/core';
6
+ import { ClassName } from '@sneat/ui';
7
+ import { of } from 'rxjs';
8
+ import { UserRequiredFieldsModalComponent } from './user-required-fields-modal.component';
9
+
10
+ describe('UserRequiredFieldsModalComponent', () => {
11
+ let component: UserRequiredFieldsModalComponent;
12
+ let fixture: ComponentFixture<UserRequiredFieldsModalComponent>;
13
+
14
+ beforeEach(waitForAsync(async () => {
15
+ await TestBed.configureTestingModule({
16
+ imports: [UserRequiredFieldsModalComponent],
17
+ providers: [
18
+ { provide: ClassName, useValue: 'TestComponent' },
19
+ {
20
+ provide: ErrorLogger,
21
+ useValue: { logError: vi.fn(), logErrorHandler: () => vi.fn() },
22
+ },
23
+ { provide: ModalController, useValue: { dismiss: vi.fn() } },
24
+ { provide: UserRecordService, useValue: { initUserRecord: vi.fn() } },
25
+ {
26
+ provide: SneatUserService,
27
+ useValue: {
28
+ userState: of({ record: undefined }),
29
+ },
30
+ },
31
+ ],
32
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
33
+ })
34
+ .overrideComponent(UserRequiredFieldsModalComponent, {
35
+ set: { imports: [], schemas: [CUSTOM_ELEMENTS_SCHEMA] },
36
+ })
37
+ .compileComponents();
38
+ fixture = TestBed.createComponent(UserRequiredFieldsModalComponent);
39
+ component = fixture.componentInstance;
40
+ }));
41
+
42
+ it('should create', () => {
43
+ expect(component).toBeTruthy();
44
+ });
45
+ });
@@ -0,0 +1,141 @@
1
+ import { Component, Input, inject } from '@angular/core';
2
+ import {
3
+ FormControl,
4
+ FormGroup,
5
+ FormsModule,
6
+ ReactiveFormsModule,
7
+ Validators,
8
+ } from '@angular/forms';
9
+ import {
10
+ IonButton,
11
+ IonButtons,
12
+ IonContent,
13
+ IonFooter,
14
+ IonHeader,
15
+ IonIcon,
16
+ IonInput,
17
+ IonItem,
18
+ IonItemDivider,
19
+ IonLabel,
20
+ IonTitle,
21
+ IonToolbar,
22
+ ModalController,
23
+ } from '@ionic/angular/standalone';
24
+ import {
25
+ IInitUserRecordRequest,
26
+ ISneatUserState,
27
+ SneatUserService,
28
+ UserRecordService,
29
+ } from '@sneat/auth-core';
30
+ import { AgeGroupID, Gender } from '@sneat/core';
31
+ import { ClassName, ISelectItem, SelectFromListComponent } from '@sneat/ui';
32
+ import { ISpaceContext } from '@sneat/space-models';
33
+ import { SneatBaseComponent } from '@sneat/ui';
34
+ import { takeUntil } from 'rxjs';
35
+
36
+ @Component({
37
+ selector: 'sneat-user-required-fields-form',
38
+ templateUrl: './user-required-fields-modal.component.html',
39
+ imports: [
40
+ FormsModule,
41
+ ReactiveFormsModule,
42
+ SelectFromListComponent,
43
+ IonHeader,
44
+ IonToolbar,
45
+ IonTitle,
46
+ IonButtons,
47
+ IonButton,
48
+ IonIcon,
49
+ IonContent,
50
+ IonItem,
51
+ IonInput,
52
+ IonItemDivider,
53
+ IonLabel,
54
+ IonFooter,
55
+ ],
56
+ providers: [
57
+ {
58
+ provide: ClassName,
59
+ useValue: 'UserRequiredFieldsModalComponent',
60
+ },
61
+ ],
62
+ })
63
+ export class UserRequiredFieldsModalComponent extends SneatBaseComponent {
64
+ private readonly modalController = inject(ModalController);
65
+ private readonly userRecordService = inject(UserRecordService);
66
+ private readonly sneatUserService = inject(SneatUserService);
67
+
68
+ @Input({ required: true }) space: ISpaceContext = { id: '' };
69
+
70
+ protected readonly genders: ISelectItem[] = [
71
+ { id: 'male', emoji: '♂️', title: 'Male' },
72
+ { id: 'female', emoji: '♀️', title: 'Female' },
73
+ { id: 'other', emoji: '⚥', title: 'Other' },
74
+ { id: 'undisclosed', iconName: 'body', title: 'Undisclosed' },
75
+ ];
76
+
77
+ protected readonly ageGroups: ISelectItem[] = [
78
+ { id: 'adult', emoji: '🧓', title: 'Adult' },
79
+ { id: 'child', emoji: '🧒', title: 'Child' },
80
+ ];
81
+
82
+ protected readonly firstName = new FormControl('', Validators.required);
83
+ protected readonly lastName = new FormControl('', Validators.required);
84
+ protected readonly gender = new FormControl('', Validators.required);
85
+ protected readonly ageGroup = new FormControl('', Validators.required);
86
+
87
+ protected readonly form = new FormGroup({
88
+ firstName: this.firstName,
89
+ lastName: this.lastName,
90
+ gender: this.gender,
91
+ ageGroup: this.ageGroup,
92
+ });
93
+
94
+ protected submitting = false;
95
+
96
+ protected userState?: ISneatUserState;
97
+
98
+ constructor() {
99
+ super();
100
+ this.sneatUserService.userState.pipe(takeUntil(this.destroyed$)).subscribe({
101
+ next: (userState) => (this.userState = userState),
102
+ });
103
+ }
104
+
105
+ protected save(): void {
106
+ this.submitting = true;
107
+ if (!this.form.valid) {
108
+ setTimeout(() => {
109
+ alert('Please fill in all fields.');
110
+ this.submitting = false;
111
+ });
112
+ return;
113
+ }
114
+
115
+ const gender = this.gender.value as Gender;
116
+ const ageGroup = this.ageGroup.value as AgeGroupID;
117
+
118
+ const request: IInitUserRecordRequest = {
119
+ names: {
120
+ firstName: this.firstName.value || undefined,
121
+ lastName: this.lastName.value || undefined,
122
+ },
123
+ gender,
124
+ ageGroup,
125
+ };
126
+ this.userRecordService.initUserRecord(request).subscribe({
127
+ next: () => this.modalController.dismiss(true).catch(console.error),
128
+ error: (err) => {
129
+ this.errorLogger.logError('Failed to init user record:', err);
130
+ alert('Failed to init user record');
131
+ this.submitting = false;
132
+ },
133
+ });
134
+ }
135
+
136
+ protected close(): void {
137
+ this.modalController
138
+ .dismiss(false)
139
+ .catch(this.errorLogger.logErrorHandler('Failed to close modal'));
140
+ }
141
+ }