@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.
- package/eslint.config.cjs +7 -0
- package/ng-package.json +7 -0
- package/package.json +19 -0
- package/project.json +38 -0
- package/src/index.ts +3 -0
- package/src/lib/components/auth-menu-item/auth-menu-item.component.html +59 -0
- package/src/lib/components/auth-menu-item/auth-menu-item.component.spec.ts +5 -0
- package/src/lib/components/auth-menu-item/auth-menu-item.component.ts +116 -0
- package/src/lib/components/index.ts +3 -0
- package/src/lib/components/user-auth-providers/user-auth-accounts.component.html +127 -0
- package/src/lib/components/user-auth-providers/user-auth-accounts.component.spec.ts +47 -0
- package/src/lib/components/user-auth-providers/user-auth-accounts.component.ts +109 -0
- package/src/lib/components/user-auth-providers/user-auth-provider-status.html +42 -0
- package/src/lib/components/user-auth-providers/user-auth-provider-status.ts +172 -0
- package/src/lib/components/user-required-fields/index.ts +2 -0
- package/src/lib/components/user-required-fields/user-required-fields-modal.component.html +64 -0
- package/src/lib/components/user-required-fields/user-required-fields-modal.component.spec.ts +45 -0
- package/src/lib/components/user-required-fields/user-required-fields-modal.component.ts +141 -0
- package/src/lib/components/user-required-fields/user-required-fields.service.spec.ts +335 -0
- package/src/lib/components/user-required-fields/user-required-fields.service.ts +23 -0
- package/src/lib/pages/login-page/email-login-form/email-login-form.component.html +165 -0
- package/src/lib/pages/login-page/email-login-form/email-login-form.component.spec.ts +59 -0
- package/src/lib/pages/login-page/email-login-form/email-login-form.component.ts +356 -0
- package/src/lib/pages/login-page/index.ts +3 -0
- package/src/lib/pages/login-page/login-page.component.html +146 -0
- package/src/lib/pages/login-page/login-page.component.spec.ts +5 -0
- package/src/lib/pages/login-page/login-page.component.ts +209 -0
- package/src/lib/pages/login-page/login-with-telegram.component.spec.ts +42 -0
- package/src/lib/pages/login-page/login-with-telegram.component.ts +82 -0
- package/src/lib/pages/login-page/sneat-auth-with-telegram.service.spec.ts +31 -0
- package/src/lib/pages/login-page/sneat-auth-with-telegram.service.ts +56 -0
- package/src/lib/pages/sign-in-from-email-link/sign-in-from-email-link-page.component.html +35 -0
- package/src/lib/pages/sign-in-from-email-link/sign-in-from-email-link-page.component.spec.ts +43 -0
- package/src/lib/pages/sign-in-from-email-link/sign-in-from-email-link-page.component.ts +70 -0
- package/src/lib/pages/sneat-auth-routing.module.ts +25 -0
- package/src/lib/pipes/person-names.pipe.spec.ts +317 -0
- package/src/lib/pipes/person-names.pipe.ts +31 -0
- package/src/test-setup.ts +3 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +19 -0
- package/tsconfig.lib.prod.json +7 -0
- package/tsconfig.spec.json +31 -0
- 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,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
|
+
}
|