diff --git a/package-lock.json b/package-lock.json index cc18208ff138579bbf90d560c2658fc2303ec22e..7aaa57f97844f92d4001ee2177db12a0d709c24d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "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", diff --git a/src/app/account/account.model.ts b/src/app/account/account.model.ts index 3d24135440e1e2873952bc1e403fa8b5ac063ed1..d6df2325df55332ae07188335a9b62093eb00074 100644 --- a/src/app/account/account.model.ts +++ b/src/app/account/account.model.ts @@ -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 { diff --git a/src/app/account/accounts.service.ts b/src/app/account/accounts.service.ts index 0a3bfa24e284a28091edfa48ef00cbe1fe181795..1ac3c20c73ee47178d6717d681fbf884c31bc434 100644 --- a/src/app/account/accounts.service.ts +++ b/src/app/account/accounts.service.ts @@ -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, diff --git a/src/app/account/auth/auth.controller.ts b/src/app/account/auth/auth.controller.ts index e57e0fb0e700a2c5d3c51847a41fb845cb02a174..d352282e05610fa2613a3cf5fa5a9635c8024aac 100644 --- a/src/app/account/auth/auth.controller.ts +++ b/src/app/account/auth/auth.controller.ts @@ -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,44 +38,43 @@ 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 - if (!this._mobile) { - const popover = await this.popoverCtrl.create(<PopoverOptions>{ - event, - backdropDismiss: true, - component: ListPopover, - cssClass: 'login-method-popover', - componentProps: <ListPopoverOptions>{ - title: 'LOGIN.METHOD_POPOVER_TITLE', - items: LoginMethods, - }, - }); - await popover.present(event); - const { data } = await popover.onWillDismiss(); - loginMethod = data; - } else { - const actionSheet = await this.actionSheetCtrl.create({ - ...this.actionSheetOptions, - header: this.translate.instant('LOGIN.METHOD_POPOVER_TITLE'), - buttons: LoginMethods.map((method) => { - return <ActionSheetButton>{ - id: method.value, - data: method.value, - text: this.translate.instant(method.label), - }; - }), - }); - await actionSheet.present(); - const { data } = await actionSheet.onWillDismiss(); - loginMethod = data; - } + 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, + backdropDismiss: true, + component: ListPopover, + cssClass: 'login-method-popover', + componentProps: <ListPopoverOptions>{ + title: 'LOGIN.METHOD_POPOVER_TITLE', + items, + }, + }); + await popover.present(event); + const { data } = await popover.onWillDismiss(); + return data; + } else { + const actionSheet = await this.actionSheetCtrl.create({ + ...this.actionSheetOptions, + header: this.translate.instant('LOGIN.METHOD_POPOVER_TITLE'), + buttons: items.map((method) => { + return <ActionSheetButton>{ + id: method.value, + data: method.value, + text: this.translate.instant(method.label), + }; + }), + }); + await actionSheet.present(); + 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); diff --git a/src/app/account/auth/auth.modal.html b/src/app/account/auth/auth.modal.html index b49b20f30406e95f282c3fc7a398dba923cbc33e..215773d250d1ea1cf5893e553668e8d0f4e7f19d 100644 --- a/src/app/account/auth/auth.modal.html +++ b/src/app/account/auth/auth.modal.html @@ -18,31 +18,63 @@ </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"> - <ion-toolbar> - <ion-row class="ion-no-padding" nowrap> - <ion-col></ion-col> - - <!-- buttons --> - <ion-col size="auto"> - <ion-button fill="clear" color="dark" (click)="cancel()"> - <ion-label translate>COMMON.BTN_CANCEL</ion-label> - </ion-button> - - <ion-button - [fill]="form.invalid ? 'clear' : 'solid'" - [disabled]="loading || form.invalid" - (click)="doSubmit()" - (keyup.enter)="doSubmit()" - [color]="auth ? 'danger' : 'tertiary'" - > - <ion-label translate>{{ auth ? 'AUTH.BTN_AUTH' : 'COMMON.BTN_LOGIN' }}</ion-label> - </ion-button> - </ion-col> - </ion-row> - </ion-toolbar> -</ion-footer> +@if (!mobile) { + <ion-footer> + <ion-toolbar> + <ion-row class="ion-no-padding" nowrap> + <ion-col></ion-col> + + <!-- buttons --> + <ion-col size="auto"> + <ion-button fill="clear" color="dark" (click)="cancel()"> + <ion-label translate>COMMON.BTN_CANCEL</ion-label> + </ion-button> + + <ion-button + [fill]="invalid ? 'clear' : 'solid'" + [disabled]="loading || invalid" + (click)="doSubmit()" + (keyup.enter)="doSubmit()" + [color]="auth ? 'danger' : 'tertiary'" + > + <ion-label translate>{{ auth ? 'AUTH.BTN_AUTH' : 'COMMON.BTN_LOGIN' }}</ion-label> + </ion-button> + </ion-col> + </ion-row> + </ion-toolbar> + </ion-footer> +} diff --git a/src/app/account/auth/auth.modal.ts b/src/app/account/auth/auth.modal.ts index 904938585b88bf06519ec45b821c156e98c5dfc0..33700a946177b8c847f454138f7a66a9a6015854 100644 --- a/src/app/account/auth/auth.modal.ts +++ b/src/app/account/auth/auth.modal.ts @@ -1,10 +1,13 @@ -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(); } diff --git a/src/app/account/auth/auth.model.ts b/src/app/account/auth/auth.model.ts index 6bfe560b0e629fd8b588f75199d1928877067980..ab492263b1cf19f32f9354879695752413e9e00b 100644 --- a/src/app/account/auth/auth.model.ts +++ b/src/app/account/auth/auth.model.ts @@ -1,9 +1,11 @@ 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>; diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index 89cf1f34e1e75eb7ffca77a35bb7db9f7941af1d..792edb98ac9cf5c3fce7a839af6851cf12e1aaa2 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -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> diff --git a/src/app/shared/popover/list.popover.html b/src/app/shared/popover/list.popover.html index 215fe8bda54a6afdb1e4e03514bef0b1da5a0a24..737d2339ed212a30f754cc501dd47f96ed149b5d 100644 --- a/src/app/shared/popover/list.popover.html +++ b/src/app/shared/popover/list.popover.html @@ -1,8 +1,13 @@ <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> - {{ item.label | translate }} - </ion-item> + @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> diff --git a/src/app/shared/popover/list.popover.ts b/src/app/shared/popover/list.popover.ts index 8d975eaddb6c73d625cec8888ff4b66011eb1126..a8876297874f6e3d8f17b6903e75e9770304e75e 100644 --- a/src/app/shared/popover/list.popover.ts +++ b/src/app/shared/popover/list.popover.ts @@ -5,6 +5,7 @@ export interface ListItem { value: string; label: string; disabled?: boolean; + icon?: string; } export interface ListPopoverOptions { diff --git a/src/theme/_cesium.scss b/src/theme/_cesium.scss index a7136bcb543f9dc162764987f59097ef76c7a942..e397052be0b1fc9b290806489dbf6ad03cb40cff 100644 --- a/src/theme/_cesium.scss +++ b/src/theme/_cesium.scss @@ -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; @@ -111,9 +116,9 @@ ion-list { /* -- icons --*/ ion-icon.icon-secondary { - position: absolute; - opacity: 1; - font-size: 16px; - left: 8px; - top: -2px; - } + position: absolute; + opacity: 1; + font-size: 16px; + left: 8px; + top: -2px; +} diff --git a/src/theme/_variables.scss b/src/theme/_variables.scss index fe785e219cb2187f6817adca28eebba07700d533..337644ea992cb8a496486c5628ece0c191bf8a0a 100644 --- a/src/theme/_variables.scss +++ b/src/theme/_variables.scss @@ -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; } /*