Skip to content
Snippets Groups Projects
Commit 6dd530a2 authored by Benoit Lavenier's avatar Benoit Lavenier
Browse files

Merge branch 'feature/26-login-allow-change-login-method' into 'develop'

Allow to change login method inside auth modal - closes #26

See merge request clients/cesium-grp/cesium2s!20
parents 0a9c192f 3bb49f43
No related branches found
No related tags found
1 merge request!20Allow to change login method inside auth modal - closes #26
Pipeline #37626 failed
{
"name": "cesium",
"version": "2.0.0-alpha37",
"version": "2.0.0-alpha38",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cesium",
"version": "2.0.0-alpha37",
"version": "2.0.0-alpha38",
"license": "AGPL-3.0",
"dependencies": {
"@angular/animations": "^17.3.5",
......@@ -54,12 +54,12 @@ export interface AccountData {
* Parse the base64 encoded json data from squid to an AddressSquid object
*/
export function parseAddressSquid(data: string): AddressSquid {
const decodedArray: any[] = JSON.parse(atob(data));
if (decodedArray.length !== 4) {
const decodedArray = JSON.parse(atob(data));
if (!Array.isArray(decodedArray) || decodedArray.length !== 4) {
throw new Error('Invalid account data');
}
return {
index: decodedArray[0] as number,
index: +decodedArray[0],
visibility: decodedArray[1] as string,
type: decodedArray[2] as string,
address: decodedArray[3] as string,
......@@ -96,12 +96,16 @@ export interface SelectAccountOptions {
}
export declare type LoginMethodType = 'v1' | 'v2' | 'keyfile-v1';
export const LoginMethods: ListItem[] = [
{ value: 'v1', label: 'LOGIN.METHOD.SCRYPT_DEFAULT' },
{ value: 'v2', label: 'LOGIN.METHOD.MNEMONIC' },
{ value: 'pubkey-v1', label: 'LOGIN.METHOD.PUBKEY' },
{ value: 'address', label: 'LOGIN.METHOD.ADDRESS' },
{ value: 'keyfile-v1', label: 'LOGIN.METHOD.FILE', disabled: true },
export interface LoginMethodItem extends ListItem {
auth?: boolean;
}
export const LoginMethods: LoginMethodItem[] = [
{ value: 'v1', label: 'LOGIN.METHOD.SCRYPT_DEFAULT', auth: true, icon: 'shuffle' },
{ value: 'v2', label: 'LOGIN.METHOD.MNEMONIC', auth: true, icon: 'infinite' },
{ value: 'pubkey-v1', label: 'LOGIN.METHOD.PUBKEY', auth: false, icon: 'key' },
{ value: 'address', label: 'LOGIN.METHOD.ADDRESS', auth: false, icon: 'key' },
{ value: 'keyfile-v1', label: 'LOGIN.METHOD.FILE', disabled: true, auth: true, icon: 'document-text' },
];
export interface LoginOptions {
......
......@@ -45,7 +45,7 @@ export interface WatchAccountDataOptions extends LoadAccountDataOptions {}
export interface AccountsState {
accounts: Account[];
password: string;
password?: string;
}
@Injectable({ providedIn: 'root' })
......@@ -80,7 +80,7 @@ export class AccountsService extends RxStartableService<AccountsState> {
this.start();
}
protected async ngOnStart(): Promise<any> {
protected async ngOnStart(): Promise<AccountsState> {
// Wait crypto to be loaded by browser
await cryptoWaitReady();
......@@ -93,7 +93,7 @@ export class AccountsService extends RxStartableService<AccountsState> {
// Restoring accounts
const accounts = await this.restoreAccounts(currency);
return {
return <AccountsState>{
accounts,
};
}
......@@ -200,7 +200,7 @@ export class AccountsService extends RxStartableService<AccountsState> {
// Set password to AAAAA (or those defined in environment)
this._password = data?.password || 'AAAAA';
if (!data?.v1 && !data.v2) return; // Skip if no dev account defined
if (!data || (!data.v1 && !data.v2)) return; // Skip if no dev account defined
data.meta = {
isTesting: true,
default: true,
......
......@@ -2,18 +2,18 @@ import { ActionSheetButton, ActionSheetController, ActionSheetOptions, ModalCont
import { Injectable } from '@angular/core';
import { PlatformService } from '@app/shared/services/platform.service';
import { PopoverOptions } from '@ionic/core';
import { ListPopover, ListPopoverOptions } from '@app/shared/popover/list.popover';
import { TranslateService } from '@ngx-translate/core';
import { AuthModal, AuthModalOptions } from '@app/account/auth/auth.modal';
import { Router } from '@angular/router';
import { RegisterModal, RegisterModalOptions } from '@app/account/register/register.modal';
import { Account, LoginMethods, LoginOptions, SelectAccountOptions, UnlockOptions } from '@app/account/account.model';
import { Account, LoginMethods, LoginMethodType, LoginOptions, SelectAccountOptions, UnlockOptions } from '@app/account/account.model';
import { AuthV2Modal } from '@app/account/auth/authv2.modal';
import { UnlockModal } from '@app/account/unlock/unlock.modal';
import { AccountListComponent, AccountListComponentInputs } from '@app/account/list/account-list.component';
import { setTimeout } from '@rx-angular/cdk/zone-less/browser';
import { AppEvent } from '@app/shared/types';
import { IAuthController } from '@app/account/auth/auth.model';
import { ListPopover, ListPopoverOptions } from '@app/shared/popover/list.popover';
@Injectable()
export class AuthController implements IAuthController {
......@@ -38,12 +38,9 @@ export class AuthController implements IAuthController {
private router: Router
) {}
async login(event?: AppEvent, opts?: LoginOptions): Promise<Account> {
let loginMethod = opts?.loginMethod;
// Ask login method
if (!loginMethod) {
// ...using popover
async selectLoginMethod(event?: AppEvent, opts?: { auth?: boolean }): Promise<LoginMethodType> {
const items = opts?.auth ? LoginMethods.filter((m) => m.auth === true) : LoginMethods;
// If desktop, then use popover
if (!this._mobile) {
const popover = await this.popoverCtrl.create(<PopoverOptions>{
event,
......@@ -52,17 +49,17 @@ export class AuthController implements IAuthController {
cssClass: 'login-method-popover',
componentProps: <ListPopoverOptions>{
title: 'LOGIN.METHOD_POPOVER_TITLE',
items: LoginMethods,
items,
},
});
await popover.present(event);
const { data } = await popover.onWillDismiss();
loginMethod = data;
return data;
} else {
const actionSheet = await this.actionSheetCtrl.create({
...this.actionSheetOptions,
header: this.translate.instant('LOGIN.METHOD_POPOVER_TITLE'),
buttons: LoginMethods.map((method) => {
buttons: items.map((method) => {
return <ActionSheetButton>{
id: method.value,
data: method.value,
......@@ -71,11 +68,13 @@ export class AuthController implements IAuthController {
}),
});
await actionSheet.present();
const { data } = await actionSheet.onWillDismiss();
loginMethod = data;
const { data } = await actionSheet.onDidDismiss();
return data;
}
}
if (!loginMethod) return undefined; // User cancelled
async login(event?: AppEvent, opts?: LoginOptions): Promise<Account> {
const loginMethod = opts?.loginMethod || 'v1';
console.info('[auth] Selected login method: ' + loginMethod);
......
......@@ -18,11 +18,42 @@
</ion-toolbar>
</ion-header>
<ion-content>
<app-auth-form #form [canRegister]="!auth" (validate)="doSubmit($event)" (cancel)="cancel()"></app-auth-form>
<ion-content class="ion-padding-top">
<!-- login method -->
@if (!mobile) {
<div class="ion-text-end">
<ion-button slot="end" fill="clear" color="medium" (click)="changeAuthMethod($event)">
<ion-icon slot="start" name="build"></ion-icon>
<ion-text class="text-help" translate>LOGIN.BTN_METHODS</ion-text>
</ion-button>
<ion-button slot="end" fill="clear" (click)="showHelpModal('login-method')">
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
</ion-button>
</div>
}
@switch (loginMethod) {
@case ('v1') {
<ion-item lines="none"><ion-text class="text-help" [innerHTML]="'LOGIN.SCRYPT_FORM_HELP' | translate"></ion-text></ion-item>
<app-auth-form #formV1 [canRegister]="!auth" (validate)="doSubmit($event)" (cancel)="cancel()" (ngInit)="setForm(formV1)"></app-auth-form>
}
@case ('v2') {
<app-authv2-form #formV2 [canRegister]="!auth" (validate)="doSubmit($event)" (cancel)="cancel()" (ngInit)="setForm(formV2)"></app-authv2-form>
}
}
@if (mobile) {
<div class="ion-text-center">
<ion-button color="light" size="medium" (click)="changeAuthMethod($event)">
<ion-icon slot="start" name="build"></ion-icon>
<ion-text translate>LOGIN.BTN_METHODS_DOTS</ion-text>
</ion-button>
</div>
}
</ion-content>
<ion-footer *ngIf="!mobile">
@if (!mobile) {
<ion-footer>
<ion-toolbar>
<ion-row class="ion-no-padding" nowrap>
<ion-col></ion-col>
......@@ -34,8 +65,8 @@
</ion-button>
<ion-button
[fill]="form.invalid ? 'clear' : 'solid'"
[disabled]="loading || form.invalid"
[fill]="invalid ? 'clear' : 'solid'"
[disabled]="loading || invalid"
(click)="doSubmit()"
(keyup.enter)="doSubmit()"
[color]="auth ? 'danger' : 'tertiary'"
......@@ -46,3 +77,4 @@
</ion-row>
</ion-toolbar>
</ion-footer>
}
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, OnInit } from '@angular/core';
import { ModalController } from '@ionic/angular';
import { AccountsService } from '@app/account/accounts.service';
import { AuthForm } from './auth.form';
import { firstNotNilPromise } from '@app/shared/observables';
import { AuthData } from '@app/account/auth/auth.model';
import { APP_AUTH_CONTROLLER, AuthData, IAuthController } from '@app/account/auth/auth.model';
import { AppEvent } from '@app/shared/types';
import { LoginMethodType } from '@app/account/account.model';
import { SettingsService } from '@app/settings/settings.service';
export interface AuthModalOptions {
auth?: boolean;
......@@ -20,28 +23,35 @@ export declare type AuthModalRole = 'CANCEL' | 'VALIDATE';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AuthModal implements OnInit, AuthModalOptions {
protected mobile = this.settingsService.mobile;
protected form: AuthForm;
get loading() {
return this.form?.loading;
}
get mobile(): boolean {
return this.form.mobile;
get invalid() {
return this.form?.invalid;
}
@Input() auth = false; // false for login, true for auth
@Input() title: string = null;
@ViewChild('form', { static: true }) private form: AuthForm;
@Input() loginMethod: LoginMethodType = 'v1';
constructor(
private settingsService: SettingsService,
private accountService: AccountsService,
private viewCtrl: ModalController,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
@Inject(APP_AUTH_CONTROLLER) private authController: IAuthController
) {}
ngOnInit() {
this.title = this.title || (this.auth ? 'AUTH.TITLE' : 'LOGIN.TITLE');
}
setForm(form: AuthForm) {
this.form = form;
this.form.markAsReady({ emitEvent: false });
this.form.markAsLoaded();
}
......@@ -84,6 +94,18 @@ export class AuthModal implements OnInit, AuthModalOptions {
}
}
async changeAuthMethod(event: AppEvent) {
const loginMethod = await this.authController.selectLoginMethod(event, { auth: this.auth });
if (!loginMethod) return; // Cancelled
this.loginMethod = loginMethod;
this.markForCheck();
}
showHelpModal(anchor?: string) {
console.info('TODO Opening help modal to anchor: ' + anchor);
}
protected markForCheck() {
this.cd.markForCheck();
}
......
import { AppEvent } from '@app/shared/types';
import { InjectionToken } from '@angular/core';
import { ScryptParams } from '@app/account/crypto.utils';
import { Account, AccountMeta, LoginOptions, SelectAccountOptions, UnlockOptions } from '@app/account/account.model';
import { Account, AccountMeta, LoginMethodType, LoginOptions, SelectAccountOptions, UnlockOptions } from '@app/account/account.model';
export interface IAuthController {
selectLoginMethod(event?: AppEvent, opts?: { auth?: boolean }): Promise<LoginMethodType>;
login(event?: AppEvent, opts?: LoginOptions): Promise<Account>;
createNew(opts?: { redirectToWalletPage?: boolean }): Promise<Account>;
......
......@@ -73,9 +73,8 @@
<!-- login -->
<p class="ion-padding-top" translate>LOGIN.HAVE_ACCOUNT_QUESTION</p>
<ion-button color="light" expand="block" (click)="login($event)">
<ion-button expand="block" (click)="login($event)">
<ion-label translate>COMMON.BTN_LOGIN</ion-label>
<ion-icon slot="end" name="chevron-down"></ion-icon>
</ion-button>
</ng-template>
</ion-card-content>
......
<ion-content>
<ion-list>
<ion-list-header *ngIf="title">{{ title | translate }}</ion-list-header>
<ion-item *ngFor="let item of items" [disabled]="item.disabled" (click)="click(item.value)" tappable>
@for (item of items; track item.value) {
<ion-item [disabled]="item.disabled" (click)="click(item.value)" tappable>
@if (item.icon) {
<ion-icon slot="start" [name]="item.icon"></ion-icon>
}
{{ item.label | translate }}
</ion-item>
}
</ion-list>
</ion-content>
......@@ -5,6 +5,7 @@ export interface ListItem {
value: string;
label: string;
disabled?: boolean;
icon?: string;
}
export interface ListPopoverOptions {
......
......@@ -87,7 +87,12 @@ ion-list {
.text-italic {
font-style: italic;
}
.text-bold {
font-weight: bold;
}
.text-help {
color: var(--ion-color-step-700) !important;
}
.barcode-scanner-square {
position: absolute;
z-index: 99999;
......
......@@ -3,6 +3,26 @@
/** Ionic CSS Variables **/
:root {
@include css-variables-to-root();
--ion-color-step-50: #f3f3f3;
--ion-color-step-100: #e7e7e7;
--ion-color-step-150: #dbdbdb;
--ion-color-step-200: #d0d0d0;
--ion-color-step-250: #c4c4c4;
--ion-color-step-300: #b8b8b8;
--ion-color-step-350: #acacac;
--ion-color-step-400: #a0a0a0;
--ion-color-step-450: #949494;
--ion-color-step-500: #898989;
--ion-color-step-550: #7d7d7d;
--ion-color-step-600: #717171;
--ion-color-step-650: #656565;
--ion-color-step-700: #595959;
--ion-color-step-750: #4d4d4d;
--ion-color-step-800: #414141;
--ion-color-step-850: #363636;
--ion-color-step-900: #2a2a2a;
--ion-color-step-950: #1e1e1e;
}
/*
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment