diff --git a/angular.json b/angular.json index ae1dc865367ff75c2bfd702eba2844a73cdde45e..aff02da09db3a24586474ff7c4fd24ffc9f5a605 100644 --- a/angular.json +++ b/angular.json @@ -33,7 +33,8 @@ "bn.js", "ip-regexp", "eventemitter3", - "qrious" + "qrious", + "localforage-cordovasqlitedriver" ], "assets": [ { @@ -78,6 +79,18 @@ "with": "src/environments/environment.prod.ts" } ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "6mb", + "maximumError": "8mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb", + "maximumError": "10kb" + } + ], "optimization": true, "outputHashing": "all", "sourceMap": false, @@ -85,14 +98,7 @@ "aot": true, "extractLicenses": true, "vendorChunk": false, - "buildOptimizer": true, - "budgets": [ - { - "type": "initial", - "maximumWarning": "2mb", - "maximumError": "5mb" - } - ] + "buildOptimizer": true }, "ci": { "progress": false @@ -107,17 +113,12 @@ "configurations": { "production": { "browserTarget": "app:build:production" - }, + } "ci": { + "progress": false } } }, - "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n", - "options": { - "browserTarget": "app:build" - } - }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { diff --git a/package.json b/package.json index 16fe5ad37069f8dd8c6a73ecf5bff1d3452ac625..af6c3d8e0031e00da6786b2e8313b1dddccc333d 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,10 @@ "scripts": { "ng": "ng", "start": "ng serve", - "build": "ng build", + "start.android": "ionic capacitor run android -l --external", + "build": "ng build --configuration production", + "build.android": "ionic capacitor build android --configuration production", + "sync.android": "npx cap sync android", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", @@ -30,20 +33,21 @@ "@angular/platform-browser-dynamic": "^14.2.1", "@angular/router": "^14.2.1", "@capacitor-community/barcode-scanner": "3.0.0", - "@capacitor-community/sqlite": "^4.0.1", - "@capacitor/android": "4.1.0", - "@capacitor/app": "4.0.1", - "@capacitor/clipboard": "^4.0.1", - "@capacitor/core": "4.1.0", - "@capacitor/haptics": "4.0.1", - "@capacitor/keyboard": "4.0.1", - "@capacitor/splash-screen": "4.0.1", - "@capacitor/status-bar": "4.0.1", + "@capacitor/android": "~4.2.0", + "@capacitor/app": "~4.0.1", + "@capacitor/browser": "~4.0.1", + "@capacitor/camera": "~4.1.1", + "@capacitor/clipboard": "~4.0.1", + "@capacitor/core": "~4.1.0", + "@capacitor/haptics": "~4.0.1", + "@capacitor/keyboard": "~4.0.1", + "@capacitor/splash-screen": "~4.0.1", + "@capacitor/status-bar": "~4.0.1", "@ionic/angular": "^6.2.6", + "@ionic/pwa-elements": "^3.1.1", "@ionic/storage-angular": "^3.0.6", "@ngx-translate/core": "^14.0.0", "@ngx-translate/http-loader": "^7.0.0", - "@ionic/pwa-elements": "~3.1.1", "@polkadot/api": "^9.2.4", "@polkadot/keyring": "^10.1.6", "@polkadot/networks": "^10.1.6", @@ -54,6 +58,8 @@ "angular2-qrcode": "^2.0.3", "crypto-browserify": "^3.12.0", "jdenticon": "^3.1.1", + "localforage": "~1.10.0", + "localforage-cordovasqlitedriver": "~1.8.0", "moment": "^2.29.4", "moment-timezone": "^0.5.37", "ngx-jdenticon": "^1.0.4", @@ -63,11 +69,6 @@ "zone.js": "~0.11.8" }, "devDependencies": { - "ngx-color-picker": "^12.0.1", - "ngx-jdenticon": "^1.0.4", - "ngx-markdown": "^14.0.1", - "ngx-material-timepicker": "5.5.3", - "ngx-quicklink": "^0.3.0", "@angular-devkit/build-angular": "^14.2.2", "@angular-eslint/builder": "~13.5.0", "@angular-eslint/eslint-plugin": "~13.5.0", @@ -78,13 +79,13 @@ "@angular/compiler-cli": "^14.2.1", "@angular/language-service": "^14.2.1", "@capacitor/cli": "4.1.0", - "@ionic/cli": "^6.20.1", "@ionic/angular-toolkit": "^6.1.0", + "@ionic/cli": "^6.20.1", "@polkadot/typegen": "^9.2.4", "@polkadot/types": "^9.2.4", "@types/jasmine": "~4.0.3", "@types/jasminewd2": "~2.0.10", - "@types/node": "^12.20.55", + "@types/node": "^14.18.28", "@typescript-eslint/eslint-plugin": "4.33.0", "@typescript-eslint/parser": "4.33.0", "eslint": "^7.32.0", @@ -99,8 +100,13 @@ "karma-coverage-istanbul-reporter": "~3.0.3", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "^2.0.0", + "ngx-color-picker": "^12.0.1", + "ngx-jdenticon": "^1.0.4", + "ngx-markdown": "^14.0.1", + "ngx-material-timepicker": "5.5.3", + "ngx-quicklink": "^0.3.0", "protractor": "~7.0.0", - "ts-node": "^8.6.2", + "ts-node": "^8.10.2", "typescript": "~4.6.4" }, "engines": { diff --git a/scripts/get-definitions.sh b/scripts/get-definitions.sh index 4fa84af48dc3f38c67b199d862e34f1af5905ff9..52b41e87c6894f2ad8e0d2728f1d160e506d083d 100755 --- a/scripts/get-definitions.sh +++ b/scripts/get-definitions.sh @@ -1,3 +1,5 @@ #!/bin/bash -curl -H "Content-Type: application/json" -d '{"id":"1", "jsonrpc":"2.0", "method": "state_getMetadata", "params":[]}' http://localhost:9933 > ../src/interfaces/duniter-types.json +NODE=http://localhost:9933 + +curl -H "Content-Type: application/json" -d '{"id":"1", "jsonrpc":"2.0", "method": "state_getMetadata", "params":[]}' ${NODE} > ../src/interfaces/types.json diff --git a/src/app/app.component.html b/src/app/app.component.html index 93b19d50ee8e532d1e778449d156bbac7c103bb3..3685daf745cbc27dd12b5a54ee807849ea20ee9a 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -2,9 +2,10 @@ <ion-split-pane contentId="main-content"> <ion-menu contentId="main-content" type="overlay"> <ion-content> - <ion-list id="inbox-list"> - <ion-list-header [innerHTML]="appName"></ion-list-header> - <ion-note>profile name</ion-note> + <ion-list id="main-menu-list"> + <ion-list-header [innerHTML]="'COMMON.APP_NAME'|translate"></ion-list-header> + + <ion-note></ion-note> <ion-menu-toggle auto-hide="false" *ngFor="let p of appPages; let i = index"> <ion-item routerDirection="root" *ngIf="p.url" @@ -13,7 +14,7 @@ <ion-label [color]="p.color">{{ p.title | translate }}</ion-label> </ion-item> <ion-item routerDirection="root" *ngIf="p.handle && p.enable()" - (click)="p.handle()" lines="none" detail="false" routerLinkActive="selected"> + (click)="p.handle($event)" lines="none" detail="false" routerLinkActive="selected"> <ion-icon slot="start" [color]="p.color" [ios]="p.icon + '-outline'" [md]="p.icon + '-sharp'"></ion-icon> <ion-label [color]="p.color">{{ p.title | translate }}</ion-label> </ion-item> diff --git a/src/app/app.component.scss b/src/app/app.component.scss index a893dff7bf27fb35a829b4722afe45cb3d063032..38e7fad34e398b3a884fc1ac74d1c28e70d20af6 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -43,11 +43,11 @@ ion-menu.md ion-note { padding-left: 10px; } -ion-menu.md ion-list#inbox-list { +ion-menu.md ion-list#main-menu-list { border-bottom: 1px solid var(--ion-color-step-150, #d7d8da); } -ion-menu.md ion-list#inbox-list ion-list-header { +ion-menu.md ion-list#main-menu-list ion-list-header { font-size: 22px; font-weight: 600; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7094125d21af9bdefca802b594d5ea5c9f7ba5ae..45a0e0cecc327348ac6dfb1e546fbd1142653829 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,6 +3,8 @@ import {PlatformService} from "./shared/services/platform.service"; import {environment} from "@environments/environment"; import {AccountService} from "@app/wallet/account.service"; import {Router} from "@angular/router"; +import {App} from "@capacitor/app"; +import {isNotNilOrBlank} from "@app/shared/functions"; @Component({ selector: 'app-root', @@ -11,7 +13,7 @@ import {Router} from "@angular/router"; }) export class AppComponent { - appName = environment.name; + appName = 'COMMON.APP_NAME'; appPages = [ { title: 'MENU.HOME', url: '/home', icon: 'home' }, @@ -24,10 +26,10 @@ export class AppComponent { { title: 'MENU.SETTINGS', url: '/settings', icon: 'settings' }, - { title: 'HOME.BTN_CHANGE_ACCOUNT', icon: 'log-out', color: 'danger', + { title: 'COMMON.BTN_LOGOUT', icon: 'log-out', color: 'danger', handle: (event) => this.logout(event), - enable: () => this.accountService.isLogin + enable: () => this.accountService.isLogin && this.platform.mobile }, ]; @@ -41,9 +43,13 @@ export class AppComponent { var now = Date.now(); console.info('[app] Starting...'); + // Start all stuff (services, plugins, etc.) await this.platform.start(); console.info(`[app] Starting [OK] in ${Date.now()-now}ms`); + + // Detecting deep link + await this.detectDeepLink(); } async logout(event) { @@ -52,4 +58,28 @@ export class AppComponent { replaceUrl: true }); } + + async detectDeepLink(){ + try { + const {url} = await App.getLaunchUrl(); + if (isNotNilOrBlank(url)) { + + const slashIndex = url.indexOf('/'); + if (slashIndex !== -1) { + const relativeUrl = url.substring(slashIndex+1); + console.info('[app] Detected a deep link: ' + relativeUrl); + + // TODO: call the router ? + await this.router.navigateByUrl(relativeUrl); + } + else { + console.warn(`[app] Detected a INVALID deep link: ${url} - missing slash`); + } + } + } + catch(err) { + console.error(`[platform] Cannot get launch URL: ${err.message||err}\n${err?.originalStack || JSON.stringify(err)}`); + // Continue + } + } } diff --git a/src/app/auth/auth.controller.ts b/src/app/auth/auth.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..5bcd6e7fa1897c7909845bc9cdd0c62f7d371782 --- /dev/null +++ b/src/app/auth/auth.controller.ts @@ -0,0 +1,141 @@ +import { + ActionSheetButton, + ActionSheetController, + ActionSheetOptions, + IonModal, + ModalController, + PopoverController +} from "@ionic/angular"; +import {Injectable} from "@angular/core"; +import {PlatformService} from "@app/shared/services/platform.service"; +import {PopoverOptions} from "@ionic/core"; +import {ListItem, ListPopover, ListPopoverOptions} from "@app/shared/popover/list.popover"; +import {TranslateService} from "@ngx-translate/core"; +import {AuthModal, AuthModalOptions} from "@app/auth/auth.modal"; +import {Router} from "@angular/router"; +import {RegisterModal, RegisterModalOptions} from "@app/register/register.modal"; + +export declare type LoginMethodType = 'v1' | 'v2' | 'keyfile-v1'; +export const LoginMethods: ListItem[] = [ + {value: 'v1', label: 'Compte Duniter v1'}, + {value: 'v2', label: 'Phrase de restauration'}, + {value: 'keyfile-v1', label: 'Fichier de clef Duniter v1', disabled: true} +]; + +@Injectable() +export class AuthController { + + private _mobile = this.platform.mobile; + + protected actionSheetOptions: Partial<ActionSheetOptions> = { + backdropDismiss: true, + cssClass: 'select-login-action-sheet' + }; + protected popoverOptions: Partial<PopoverOptions> = { + backdropDismiss: true, + cssClass: 'select-login-popover', + reference: 'event' + }; + + constructor( + private platform: PlatformService, + private translate: TranslateService, + private popoverCtrl: PopoverController, + private actionSheetCtrl: ActionSheetController, + private modalCtrl: ModalController, + private router: Router + ) { + } + + async login(event, opts?: { + loginMethod?: LoginMethodType, + auth?: boolean, + redirectToWalletPage?: boolean + }) { + + 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, + 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; + } + } + if (!loginMethod) return undefined; // User cancelled + + console.info('[auth] Selected login method: ' + loginMethod); + + let modal: HTMLIonModalElement; + switch (loginMethod) { + case 'v1': + modal = await this.modalCtrl.create({ + component: AuthModal, + componentProps: <AuthModalOptions>{ + auth: opts?.auth, + scrollY: false // TODO remove this ! + } + }); + break; + default: + console.warn('[home] Unknown login method: ' + loginMethod); + } + if (!modal) return; // User cancelled of method not found + + await modal.present(); + const {data} = await modal.onWillDismiss(); + + if (data?.address && opts?.redirectToWalletPage === true) { + setTimeout(() => this.router.navigate(['/wallet', data.address])); + } + + return data; + } + + async register(opts?: { redirectToWalletPage?: boolean; }) { + const modal = await this.modalCtrl.create({ + component: RegisterModal, + componentProps: <RegisterModalOptions>{ + scrollY: false // TODO remove this ! + } + }); + + await modal.present(); + + const {data} = await modal.onWillDismiss(); + + if (data?.address && opts.redirectToWalletPage === true) { + setTimeout(() => this.router.navigate(['/wallet', data.address])); + } + + return data; + } +} diff --git a/src/app/auth/auth.form.ts b/src/app/auth/auth.form.ts index ec03debab67fd4e7f6eecdc6da08f17f25489bfd..fa85892ed137fee8974d635f745fb82fe152aa0f 100644 --- a/src/app/auth/auth.form.ts +++ b/src/app/auth/auth.form.ts @@ -7,7 +7,7 @@ import {AppForm} from "@app/shared/form.class"; import {AuthData} from "@app/auth/auth.model"; import {SettingsService} from "@app/settings/settings.service"; import {NetworkService} from "@app/network/network.service"; -import {environment} from "@duniter/core-types/environments/environment"; +import {environment} from "@environments/environment"; import {FormUtils} from "@app/shared/forms"; @@ -64,7 +64,7 @@ export class AuthForm extends AppForm<AuthData> implements OnInit { this.onCancel.emit(); } - async doSubmit(event?: UIEvent) { + async doSubmit(event?: Event) { if (event) { event.preventDefault(); event.stopPropagation(); diff --git a/src/app/auth/auth.modal.html b/src/app/auth/auth.modal.html index 55f2ea09ca66e5ec382178acda30b0ed4098b10e..1c8b70f8517e487ddb2f69c6697375e6fa462aba 100644 --- a/src/app/auth/auth.modal.html +++ b/src/app/auth/auth.modal.html @@ -6,7 +6,7 @@ </ion-button> </ion-buttons> - <ion-title [innerHTML]="auth?'AUTH.TITLE': 'LOGIN.TITLE'|translate"> + <ion-title [innerHTML]="title|translate"> </ion-title> <ion-buttons slot="end"> @@ -20,7 +20,7 @@ </ion-toolbar> </ion-header> -<ion-content style="height: 100%"> +<ion-content style="height: 100%" [scrollY]="scrollY"> <app-auth-form #form (onSubmit)="doSubmit($event)" (onCancel)="cancel()"> diff --git a/src/app/auth/auth.modal.ts b/src/app/auth/auth.modal.ts index 31cf3c46be3dc0d95fabfcfdd4f73f23e6fb68cd..088c0d8965bc3e075aa894786202b0be07c3f8dc 100644 --- a/src/app/auth/auth.modal.ts +++ b/src/app/auth/auth.modal.ts @@ -5,15 +5,19 @@ import {AuthForm} from './auth.form'; import {firstNotNilPromise} from '@app/shared/observables'; import {AuthData} from "@app/auth/auth.model"; +export interface AuthModalOptions { + auth?: boolean; + scrollY?: boolean; + title?: string; +} @Component({ selector: 'app-auth-modal', templateUrl: 'auth.modal.html', styleUrls: ['./auth.modal.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class AuthModal implements OnInit { +export class AuthModal implements OnInit, AuthModalOptions { - title: string = null; get loading() { return this.form?.loading; } @@ -22,7 +26,9 @@ export class AuthModal implements OnInit { return this.form?.mobile; } - @Input() auth = false; + @Input() auth = false; // false for login, true for auth + @Input() scrollY = false; + @Input() title: string = null; @ViewChild('form', { static: true }) private form: AuthForm; @@ -34,7 +40,7 @@ export class AuthModal implements OnInit { ngOnInit() { - this.title = this.auth ? 'AUTH.TITLE' : 'LOGIN.TITLE'; + this.title = this.title || (this.auth ? 'AUTH.TITLE' : 'LOGIN.TITLE'); this.form.markAsReady({emitEvent: false}); this.form.markAsLoaded(); diff --git a/src/app/auth/auth.module.ts b/src/app/auth/auth.module.ts index ffa85109d7267836759ed97330b6e5ecda79efd9..a37e4a98b80452148e262da4a9e0458bb4ab590b 100644 --- a/src/app/auth/auth.module.ts +++ b/src/app/auth/auth.module.ts @@ -7,19 +7,31 @@ import {AuthForm} from "./auth.form"; import {AuthModal} from "./auth.modal"; import {AppSharedModule} from "@app/shared/shared.module"; import {TranslateModule} from "@ngx-translate/core"; +import {AuthController} from "@app/auth/auth.controller"; +import {AppRegisterModule} from "@app/register/register.module"; @NgModule({ - imports: [ - CommonModule, - FormsModule, - ReactiveFormsModule, - IonicModule, - AppSharedModule, - TranslateModule - ], - exports: [ - AuthForm, AuthModal - ], - declarations: [AuthForm, AuthModal] + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + IonicModule, + TranslateModule, + + // App modules + AppSharedModule, + AppRegisterModule + ], + declarations: [ + AuthForm, AuthModal + ], + exports: [ + AuthForm, + AuthModal, + TranslateModule + ], + providers: [ + AuthController + ] }) export class AppAuthModule {} diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index 75f792e4692f59f6a2b21cdfe04c139e3409bb23..7d658f00b731ffdbda65ea1b5271de70a67b2953 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -90,41 +90,6 @@ </div> </ion-content> -<ion-popover #loginMethodPopover> - <ng-template> - <ion-content> - <ion-list> - <ion-item *ngFor="let item of loginMethods" - [disabled]="item.disabled" - (click)="loginMethodPopover.dismiss(item.value)" - tappable> - {{item.label|translate}} - </ion-item> - </ion-list> - </ion-content> - </ng-template> -</ion-popover> - -<ion-modal - #loginModal - [backdropDismiss]="false"> - <ng-template> - <ion-content scrollY="false"> - <app-auth-modal></app-auth-modal> - </ion-content> - </ng-template> -</ion-modal> - -<ion-modal - #registerModal - [backdropDismiss]="false"> - <ng-template> - <ion-content scrollY="false"> - <app-register-modal></app-register-modal> - </ion-content> - </ng-template> -</ion-modal> - <ng-template #localeButton let-buttonColor> <!-- locale button --> @@ -140,7 +105,7 @@ <ion-content> <ion-list> <ion-item *ngFor="let item of locales" - (click)="changeLocale(item.key) || popover.dismiss()" + (click)="changeLocale(item.key) && popover.dismiss()" tappable> <ion-label>{{item.value}}</ion-label> <ion-icon slot="end" name="checkmark" *ngIf="data.locale===item.key"></ion-icon> diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index 9a37e0b57ba5eab060d7344229de4cb87a6efca9..6cf6c3b7a3cecfef202e9794155d0d11671908a8 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -1,29 +1,13 @@ -import {Component, Inject, Injector, OnInit, ViewChild} from '@angular/core'; -import {SettingsService} from "@app/settings/settings.service"; +import {Component, Inject, Injector, OnInit} from '@angular/core'; import {APP_LOCALES, LocaleConfig, Settings} from "@app/settings/settings.model"; import {BasePage} from "@app/shared/pages/base.page"; import {NetworkService} from "@app/network/network.service"; -import {AbbreviatePipe} from "@app/shared/pipes/string.pipes"; import {AccountService} from "@app/wallet/account.service"; import {Account} from "@app/wallet/account.model"; import {fadeInAnimation} from "@app/shared/animations"; -import {AuthModal} from "@app/auth/auth.modal"; -import {RegisterModal} from "@app/register/register.modal"; -import { - ActionSheetButton, - ActionSheetController, - ActionSheetOptions, - IonModal, - IonPopover, - PopoverOptions -} from "@ionic/angular"; import {Router} from "@angular/router"; +import {AuthController} from "@app/auth/auth.controller"; -export interface LoginMethod { - value: string; - label: string; - disabled?: boolean; -} @Component({ selector: 'app-home', templateUrl: './home.page.html', @@ -32,22 +16,6 @@ export interface LoginMethod { }) export class HomePage extends BasePage<Settings> implements OnInit { - - protected actionSheetOptions: Partial<ActionSheetOptions> = { - backdropDismiss: true, - cssClass: 'select-login-action-sheet' - }; - protected popoverOptions: Partial<PopoverOptions> = { - backdropDismiss: true, - cssClass: 'select-login-popover', - reference: 'event' - }; - protected loginMethods: LoginMethod[] = [ - {value: 'v1', label: 'Compte Duniter v1'}, - {value: 'v2', label: 'Phrase de restauration'}, - {value: 'keyfile-v1', label: 'Fichier de clef Duniter v1', disabled: true} - ]; - currency: string = null; defaultAccount: Account = null; @@ -55,16 +23,12 @@ export class HomePage extends BasePage<Settings> implements OnInit { return this.accountService.isLogin } - @ViewChild('loginModal') loginModal: IonModal; - @ViewChild('registerModal') registerModal: IonModal; - @ViewChild('loginMethodPopover') loginMethodPopover: IonPopover; - constructor( injector: Injector, public networkService: NetworkService, public accountService: AccountService, + public authController: AuthController, public router: Router, - public actionSheetCtrl: ActionSheetController, @Inject(APP_LOCALES) public locales: LocaleConfig[] ) { super(injector, {name: 'home'}) @@ -89,62 +53,25 @@ export class HomePage extends BasePage<Settings> implements OnInit { } - changeLocale(locale: string) { + changeLocale(locale: string): boolean { this.settings.patchValue({locale}); this.data.locale = locale; this.markForCheck(); + return true; } - async login(event) { - - let loginMethod: string; - if (!this.mobile) { - await this.loginMethodPopover.present(event); - const {data} = await this.loginMethodPopover.onWillDismiss(); - loginMethod = data; - } - else { - const actionSheet = await this.actionSheetCtrl.create({ - ...this.actionSheetOptions, - header: this.translate.instant('Select login method'), - buttons: this.loginMethods.map(method => { - return <ActionSheetButton>{ - data: method.value, - text: this.translate.instant(method.label), - id: method.value - } - }) - }); - await actionSheet.present(); - const {data} = await actionSheet.onWillDismiss(); - loginMethod = data; - } - if (!loginMethod) return; - console.info('[home] Selected login method: ' + loginMethod); - - let modal: IonModal; - switch (loginMethod) { - case 'v1': - modal = this.loginModal; - break; - default: - console.warn('[home] Unknown login method: ' + loginMethod); - } - if (!modal) return; // User cancelled of method not found - - await modal.present(); - const {data} = await modal.onWillDismiss(); + async login(event: UIEvent) { + const data = await this.authController.login(event, { + auth: true + }); if (data?.address) { this.defaultAccount = data; setTimeout(() => this.router.navigate(['/wallet', data.address])); } } - async register(event) { - await this.registerModal.present(); - - const {data} = await this.registerModal.onWillDismiss(); - + async register() { + const data = await this.authController.register(); if (data?.address) { this.defaultAccount = data; setTimeout(() => this.router.navigate(['/wallet', data.address])); diff --git a/src/app/network/network.service.ts b/src/app/network/network.service.ts index 84acf88ea69216fc73d6534c45f463a67b7e8859..59eba32d8a2407b9870152ee15b8bf6d0bc7525c 100644 --- a/src/app/network/network.service.ts +++ b/src/app/network/network.service.ts @@ -5,7 +5,6 @@ import {Peer, Peers} from "./peer.model"; import {StartableService} from "@app/shared/services/startable-service.class"; import {abbreviate} from "@app/shared/currencies"; import {Currency} from "@app/network/currency.model"; -//import * as definitions from '@duniter/core-types/interfaces' const WELL_KNOWN_CURRENCIES = Object.freeze({ 'Ğdev': <Partial<Currency>>{ diff --git a/src/app/register/register.modal.html b/src/app/register/register.modal.html index 645a8b136fc248d206f0b669aeb6f1ec1a3e0e01..f719fc3495e31305cda63833cf86ade7d8a66b6e 100644 --- a/src/app/register/register.modal.html +++ b/src/app/register/register.modal.html @@ -26,12 +26,12 @@ </ion-toolbar> </ion-header> -<ion-content > +<ion-content [scrollY]="scrollY"> + <app-register-form (onSubmit)="doSubmit()" (onCancel)="cancel()" [class]="mobile ? '': 'has-footer'" #form> - <ion-row *ngIf="form.debug" codeConfirmation> <ion-text color="primary" class="ion-padding"> <small>loading: {{loading}}<br/> diff --git a/src/app/register/register.modal.ts b/src/app/register/register.modal.ts index 963efb4db1ed3af5fea0a8b653dd67a322d21368..319bd833b72858ec22b62d66332c3b845a7c7cc5 100644 --- a/src/app/register/register.modal.ts +++ b/src/app/register/register.modal.ts @@ -1,21 +1,21 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, ViewChild} from '@angular/core'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, ViewChild} from '@angular/core'; import {ModalController} from '@ionic/angular'; import {REGISTER_FORM_SLIDES, RegisterForm} from "@app/register/register.form"; import {AccountService} from "@app/wallet/account.service"; import {FormUtils} from "@app/shared/forms"; import {RegisterData} from "@app/register/register.model"; import {environment} from "@environments/environment"; - +export interface RegisterModalOptions { + scrollY?: boolean; +} @Component({ selector: 'app-register-modal', templateUrl: 'register.modal.html', styleUrls: ['./register.modal.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class RegisterModal implements OnInit{ - +export class RegisterModal implements OnInit, RegisterModalOptions { - @ViewChild('form', { static: true }) private form: RegisterForm; get loading() { return this.form.loading; @@ -25,6 +25,9 @@ export class RegisterModal implements OnInit{ return this.form.mobile; } + @Input() scrollY = false; + @ViewChild('form', { static: true }) private form: RegisterForm; + constructor( private accountService: AccountService, public viewCtrl: ModalController, diff --git a/src/app/register/register.module.ts b/src/app/register/register.module.ts index e7ceb82c397ae47fc88da4af48d8f1132313e563..9c68966f32f2ec0e19f52544b11f5707948bbcb9 100644 --- a/src/app/register/register.module.ts +++ b/src/app/register/register.module.ts @@ -10,17 +10,22 @@ import {AppSharedModule} from "@app/shared/shared.module"; import {AppUnlockModule} from "@app/unlock/unlock.module"; @NgModule({ - imports: [ - CommonModule, - FormsModule, - IonicModule, - TranslateModule, - AppSharedModule, - AppUnlockModule - ], - exports: [ - RegisterForm, RegisterModal - ], - declarations: [RegisterForm, RegisterModal] + imports: [ + CommonModule, + FormsModule, + IonicModule, + TranslateModule, + AppSharedModule, + AppUnlockModule + ], + declarations: [ + RegisterForm, + RegisterModal + ], + exports: [ + RegisterForm, + RegisterModal, + TranslateModule + ] }) export class AppRegisterModule {} diff --git a/src/app/shared/colors/colors.utils.ts b/src/app/shared/colors/colors.utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..fda2b2391423077235d7886e39b21c2f9fa6e971 --- /dev/null +++ b/src/app/shared/colors/colors.utils.ts @@ -0,0 +1,86 @@ +export type PredefinedColors = + | 'primary' + | 'secondary' + | 'tertiary' + | 'success' + | 'warning' + | 'danger' + | 'light' + | 'medium' + | 'dark'; + + +export function rgbToHex(r: number, g: number, b: number): string { + return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b); +} + +export function rgbArrayToHex(rgb: number[]): string { + return '#' + componentToHex(rgb[0]) + componentToHex(rgb[1]) + componentToHex(rgb[2]); +} + +export function hexToRgbArray(hex: string): number[] { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? [ + parseInt(result[1], 16), + parseInt(result[2], 16), + parseInt(result[3], 16) + ] : null; +} + +export function hexToRgb(hex: string): {r: number; g: number; b: number} { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +} + +function componentToHex(c: number): string { + const hex = c.toString(16); + return hex.length === 1 ? ('0' + hex) : hex; +} + +// See mix in file ionic.functions.color.scss +export function mixHex(color1: string, color2: string, weight?: number) { + + weight = weight ? (weight / 100) : 0.5; + + const rgb1 = hexToRgbArray(color1); + if (!rgb1) throw Error('Invalid hex color:' + color1); + + const rgb2 = hexToRgbArray(color2); + if (!rgb2) throw Error('Invalid hex color:' + color2); + + const rgbAverage = rgb1.map((v, index) => Math.round((v * weight + rgb2[index] * (1 - weight))) ); + return rgbArrayToHex(rgbAverage); +} + +// 12% darker version of the base color (mix with black) +export function getColorShade(color: string) { + return mixHex('#000000', color, 12); +} + +// 10% lighter version of the base color (mix with white) +export function getColorTint(color: string) { + return mixHex('#ffffff', color, 10); +} + +/** + * + * @param color + * @param bw if true, will use black or white color, instead of the exact inverse + */ +export function getColorContrast(color: string, bw?: boolean) { + const rgb = hexToRgbArray(color); + if (!rgb) throw Error('Invalid hex color:' + color); + + if (bw === true) { + // http://stackoverflow.com/a/3943023/112731 + return (rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114) > 186 + ? '#000000' + : '#FFFFFF'; + } + + return rgbArrayToHex(rgb.map(v => 255 - v)); +} diff --git a/src/app/shared/colors/graph-colors.ts b/src/app/shared/colors/graph-colors.ts new file mode 100644 index 0000000000000000000000000000000000000000..e0cf365cf8b6b018c5960fd45ffe1ff062cefe98 --- /dev/null +++ b/src/app/shared/colors/graph-colors.ts @@ -0,0 +1,339 @@ +import {PredefinedColors} from '@ionic/core'; +import {isNil, isNotNil} from '../functions'; + +export declare type ColorName = PredefinedColors | + 'white' + | 'red' + | 'green' + | 'blue'; + + +/** + * Define here theme colors + */ +const rgbArrayMap = { + white: [255, 255, 255], + primary: [20, 67, 145], // ok + secondary: [117, 196, 253], // ok + tertiary: [91, 94, 244], // ok + danger: [245, 61, 61], // ok + light: [244, 245, 248], // ok + medium: [152, 154, 162], // ok + dark: [34, 36, 40], // ok + red: [255, 0, 0], + green: [0, 255, 0], + blue: [0, 0, 255] +}; + +// Fill a map of Color objects +const colorsMap: { [key: string]: Color } = {}; + + +/** + * Useful class for color conversion + */ +// @dynamic +export class Color { + + // Helper method, to retrieve a color + static get(name: ColorName): Color { + return colorsMap[name] as Color; + } + + static parseRgba(rgba: string): Color|null{ + if (!rgba || (!rgba.startsWith('rgb(') && !rgba.startsWith('rgba('))) return null; + + // Parse parts + const parts = rgba + .replace('rgb(', '') + .replace('rgba(', '') + .replace(')', '') + .split(','); + + if (parts.length !== 3 && parts.length !== 4) return null; + + return new Color([+parts[0], +parts[1], +parts[2]], + parts.length === 4 && +parts[3] || 1, + 'custom'); + } + + static transparent = function() { + return new Color([0,0,0], 0, 'translucent'); + }; + + constructor( + private _rgbArray: number[], + private _opacity?: number, + private _name: string = 'custom' + ) { + } + get name(): string { + return this._name; + } + get opacity(): number{ + return isNotNil(this._opacity) ? this._opacity : 1; + } + get rgb(): number[] { + return this._rgbArray; + } + + get r(): number { + return this._rgbArray[0]; + } + + get g(): number { + return this._rgbArray[1]; + } + + get b(): number { + return this._rgbArray[2]; + } + + rgba(opacity?: number): string { + opacity = isNotNil(opacity) ? opacity : this._opacity; + if (isNil(opacity) || opacity < 0 || opacity > 1) { + return 'rgb(' + this._rgbArray.join(',') + ')'; + } + return 'rgba(' + this._rgbArray.join(',') + ',' + opacity + ')'; + } + +} + +export declare interface ColorGradientOptions { + opacity?: number; + startColor?: number[]; + mainColor?: number[]; + mainColorIndex?: number; + endColor?: number[]; + format?: 'rgb'|'hex'|'array'; +} + +export declare interface ColorScaleOptions extends ColorGradientOptions{ + min?: number; + max?: number; + upperMax?: boolean; +} + +export declare interface ColorScaleLegendItem { + label: string; + color: string; +} + +export declare interface ColorScaleLegend { + items: ColorScaleLegendItem[]; +} + +/** + * Helper class for colors scale + */ +// @dynamic +export class ColorScale { + + static custom = (count: number, options?: ColorScaleOptions) => { + options = options || {}; + return new ColorScale( + linearColorGradientWithIntermediate(count, { + opacity: options.opacity, + startColor: options.startColor || undefined, + mainColor: options.mainColor || undefined, + mainColorIndex: isNotNil(options.mainColorIndex) ? options.mainColorIndex : undefined, + endColor: options.endColor || undefined, + format: options.format + }) as string[], + options + ); + }; + + static default() { + return ColorScale.custom(25); + } + + /** + * Create a array with the given color + **/ + static fix(length?: number, colorName?: ColorName): any[] { + return Array.apply(null, Array(length || 25)) + .map(String.prototype.valueOf, Color.get(colorName || 'primary').rgba(0.5)); + } + + private _min: number; + private _max: number; + private _rangeSize: number; + private _legendItems: ColorScaleLegendItem[]; + + get min(): number { + return this._min; + } + get max(): number { + return this._max; + } + + constructor(private colorArray: string[], options?: ColorScaleOptions) { + options = options || {}; + // reserved last colors for value > max + const nbIntervalBeforeUpper = !options.upperMax ? colorArray.length : (colorArray.length - 1); + this._min = options.min || 0; + this._max = options.max || nbIntervalBeforeUpper; + this._rangeSize = Math.round((this._max - this._min) / nbIntervalBeforeUpper); + this._legendItems = this.computeLegend(); + } + + get legend(): ColorScaleLegend { + return { + items: this._legendItems + }; + } + + getValueColor(value: number): string { + const index = Math.floor(value * (this.colorArray.length - 1) / this._max); + return this.colorArray[index]; + } + + protected computeLegend(): ColorScaleLegendItem[] { + return this.colorArray.map((color, index) => { + const start = index * this._rangeSize; + const end = start + this._rangeSize; + return { + color, + label: (end < this._max) ? `${start.toLocaleString()} - ${end.toLocaleString()}` : ` >= ${start}` + }; + }); + } +} + + +// Fill colorsMap +Object.getOwnPropertyNames(rgbArrayMap) + .forEach((key) => { + colorsMap[key] = new Color(rgbArrayMap[key], 1, key); + }); + +// Internal function +function state2side(state) { + switch (state) { + case 0: + return 0; + case 1: + return -1; + case 2: + return 0; + case 3: + return 1; + } +} + + +const SCALE_OPTIONS_DEFAULT = { + startColor: rgbArrayMap.red, + startStates: [0, 2, 3], // R=keep, V=keep, B=increase + startStepsFn: (defaultStateSize: number) => [ + Math.round((rgbArrayMap.red[0] - 50) / defaultStateSize), + Math.round((255 - rgbArrayMap.red[1]) / defaultStateSize), + Math.round((255 - rgbArrayMap.red[2]) / defaultStateSize) + ] +}; + +/** + * Internal function, that create a colors scale, using iteration + * + * @param count + * @param opacity + * @param startColor + * @param startState + * @returns + */ +function linearColorGradientWithIntermediate(count: number, + options?: ColorGradientOptions): any[] { + options = options || {}; + + // From [0,1] + options.opacity = (options.opacity > 0 && options.opacity < 1) ? options.opacity : 1; + options.startColor = options.startColor || [255, 255, 190]; // default start = creme + options.mainColorIndex = options.mainColorIndex && options.mainColorIndex < count - 1 ? options.mainColorIndex : count - 1; + options.endColor = options.endColor || [255, 0, 0]; // default main = red + options.format = options.format || 'rgb'; + + if (!options.mainColor) { + return linearColorGradient(count, { + opacity: options.opacity, + startColor: options.startColor, + endColor: options.endColor, + format: options.format + }); + } + + else { + // Step 1: startColor -> mainColor + const result = linearColorGradient(options.mainColorIndex + 1, { + opacity: options.opacity, + startColor: options.startColor, + endColor: options.mainColor, + format: options.format + }); + + // Step 2: mainColor -> endColor + if (options.mainColorIndex < count - 1) { + return result.concat( + linearColorGradient(count - options.mainColorIndex, { + opacity: options.opacity, + startColor: options.mainColor, + endColor: options.endColor, + format: options.format + })); + } + else { + return result; + } + } +} + +function linearColorGradient(count: number, + options?: ColorGradientOptions): any[] { + + options = options || {}; + + // From [0,1] + options.opacity = (options.opacity > 0 && options.opacity < 1) ? options.opacity : 1; + options.startColor = options.startColor || [255, 255, 255]; // default start = white + options.endColor = options.endColor || [255, 0, 0]; // default end = red + options.format = options.format || 'rgb'; + + const result: number[][] = []; + const color = options.startColor.slice(); // copy the start color + const delta = [ + Math.round((options.endColor[0] - options.startColor[0]) / count), + Math.round((options.endColor[1] - options.startColor[1]) / count), + Math.round((options.endColor[2] - options.startColor[2]) / count) + ]; + + for (let i = 0; i < count - 1; i++) { + for (let j = 0; j < 3; j++) { + color[j] += delta[j]; + } + result.push(color.slice()); + } + + // Force last color = end color + result.push(options.endColor.slice()); + + // Output as array + if (options.format === 'array') { + return result; + } + + // Output as rgb(r,g,b) string + if (options.format === 'rgb') { + if (options.opacity >= 1) { + return result.map(color => 'rgb(' + color.join(',') + ')'); + } else { + return result.map(color => 'rgba(' + color.concat(options.opacity).join(',') + ')'); + } + } + + // Output as hex + // TODO + // return result.map(color => { + // return "rgb(" + color.join(',') + ")"; + // }); + return result; +} diff --git a/src/app/shared/currencies.ts b/src/app/shared/currencies.ts index 4a8108a01335150fc036ad92176aee1d037cc475..95a20bcde18d869ba3af1f9232660351dc5045e6 100644 --- a/src/app/shared/currencies.ts +++ b/src/app/shared/currencies.ts @@ -27,7 +27,7 @@ export function abbreviate(currency: string): string { return currency; } -export function formatAddress(value: string, withChecksum?: boolean ): string { +export function formatAddress(value: string): string { if (!value) return ''; if (value.length < 12) return '?'; return value.substring(0,6) + '\u2026' + value.substring(value.length - 6); diff --git a/src/app/shared/pages/base.page.ts b/src/app/shared/pages/base.page.ts index 1c1b7858580bd6c43dad73cdb1e8708d7bbbecf2..0b1c3f4ceea9c340c54243acec704202038ce033 100644 --- a/src/app/shared/pages/base.page.ts +++ b/src/app/shared/pages/base.page.ts @@ -5,7 +5,7 @@ import {changeCaseToUnderscore, isNotNilOrBlank} from "@app/shared/functions"; import {environment} from "@environments/environment"; import {waitIdle} from "@app/shared/forms"; import {WaitForOptions} from "@app/shared/observables"; -import {ToastController, ToastOptions} from "@ionic/angular"; +import {IonRouterOutlet, ToastController, ToastOptions} from "@ionic/angular"; import {TranslateService} from "@ngx-translate/core"; import {Subscription} from "rxjs"; @@ -26,6 +26,7 @@ export abstract class BasePage< protected translate: TranslateService; protected settings: SettingsService; + protected readonly routerOutlet: IonRouterOutlet; protected readonly activatedRoute: ActivatedRoute; protected toastController: ToastController; protected readonly _debug = !environment.production; @@ -49,6 +50,7 @@ export abstract class BasePage< this._cd = injector.get(ChangeDetectorRef); this.settings = injector.get(SettingsService); this.translate = injector.get(TranslateService); + this.routerOutlet = injector.get(IonRouterOutlet); this.activatedRoute = injector.get(ActivatedRoute); this.toastController = injector.get(ToastController); this.mobile = this.settings.mobile; @@ -167,4 +169,5 @@ export abstract class BasePage< protected unregisterSubscription(sub: Subscription) { this._subscription?.remove(sub); } + } diff --git a/src/app/shared/pipes/account.pipes.ts b/src/app/shared/pipes/account.pipes.ts index a6b2591c1753d13df726cd73daf40d62d21f6a45..9e45744f2e9b91d79a02b587c9b3c1b8b8248b5f 100644 --- a/src/app/shared/pipes/account.pipes.ts +++ b/src/app/shared/pipes/account.pipes.ts @@ -1,34 +1,126 @@ -import {Pipe, PipeTransform} from '@angular/core'; +import {ChangeDetectorRef, Directive, Pipe, PipeTransform} from '@angular/core'; import {NumberFormatPipe} from "@app/shared/pipes/number-format.pipe"; import {NetworkService} from "@app/network/network.service"; -import {Account, AccountUtils} from "@app/wallet/account.model"; -import {isNotNilOrBlank} from "@app/shared/functions"; +import {Account, AccountData, AccountUtils} from "@app/wallet/account.model"; +import {equals, getPropertyByPath, isNotNilOrBlank} from "@app/shared/functions"; import {AddressFormatPipe} from "@app/shared/pipes/address.pipes"; +import {Subscription} from "rxjs"; +import {formatAddress} from "@app/shared/currencies"; + +// @dynamic +/** + * A common pipe, that will subscribe to all account changes, to refresh its value + */ +export abstract class AccountAbstractPipe<T = any, O = any> implements PipeTransform { + + private value: T = null; + private _lastAccount: Partial<Account> | null = null; + private _lastOptions: O = null; + private _changesSubscription: Subscription = null; + + protected constructor(private _ref: ChangeDetectorRef) { + } + + transform(account: Partial<Account>, opts: O): T { + if (!account || (!account.data && !account.dataSubject)) { + this._dispose(); + return undefined; + } + + // if we ask another time for the same account and opts, return the last value + if (account === this._lastAccount && equals(opts, this._lastOptions)) { + return this.value; + } + + // store the query, in case it changes + this._lastAccount = account; + + // store the params, in case they change + this._lastOptions = opts; + + // set the value + this._updateValue(account, opts); + + // if there is a subscription to onLangChange, clean it + this._dispose(); + + // subscribe to onTranslationChange event, in case the translations change + if (!this._changesSubscription && account.dataSubject) { + this._changesSubscription = account.dataSubject.subscribe((status) => { + this.value = this._transform(account, opts); + this._ref.markForCheck(); + }); + } + return this.value; + } + + ngOnDestroy(): void { + this._dispose(); + } + + private _updateValue(account: Partial<Account>, opts?: O) { + this.value = this._transform(account, opts); + this._ref.markForCheck(); + } + + protected abstract _transform(account: Partial<Account>, opts?: O): T; + + /** + * Clean any existing subscription to change events + */ + private _dispose(): void { + this._changesSubscription?.unsubscribe(); + this._changesSubscription = undefined; + } +} + +export declare type AccountPropertyPipeOptions<T> = string | {key?: string; defaultValue?: T}; @Pipe({ - name: 'balance' + name: 'accountProperty', + pure: false }) -export class AccountBalancePipe implements PipeTransform { +export class AccountPropertyPipe<T = any, O extends AccountPropertyPipeOptions<T> = AccountPropertyPipeOptions<T>> extends AccountAbstractPipe<T, O> { + + constructor(_ref: ChangeDetectorRef) { + super(_ref); + } - delegate = new NumberFormatPipe(); + protected _transform(account: Partial<Account>, opts?: O): T { + return getPropertyByPath(account, + // Path + opts && (typeof opts === 'string' ? opts : opts.key), + // Default value + opts && (opts as any).defaultValue); + } +} - constructor(private networkService: NetworkService) { +@Pipe({ + name: 'balance', + pure: false +}) +export class AccountBalancePipe extends AccountAbstractPipe<number, void> { + + constructor(_ref: ChangeDetectorRef) { + super(_ref); } - transform(account: Partial<Account>, opts?: Intl.NumberFormatOptions & {fixedDecimals?: number}): number | undefined { - if (!account?.data) return undefined; + protected _transform(account: Partial<Account>): number { return AccountUtils.getBalance(account); } } @Pipe({ - name: 'accountName' + name: 'accountName', + pure: false }) -export class AccountNamePipe implements PipeTransform { +export class AccountNamePipe extends AccountAbstractPipe<string, void> { - private addressFormatter = new AddressFormatPipe(); + constructor(_ref: ChangeDetectorRef) { + super(_ref); + } - transform(account: Partial<Account>): string { - return account?.meta?.name || this.addressFormatter.transform(account?.address, true); + protected _transform(account: Partial<Account>): string { + return account?.meta?.name || formatAddress(account?.address); } } diff --git a/src/app/shared/pipes/address.pipes.ts b/src/app/shared/pipes/address.pipes.ts index 4e64eeef080c06619780663dca00c21346db579c..a1de75333f9d3a09ae963dce2630e8360c0fbe2d 100644 --- a/src/app/shared/pipes/address.pipes.ts +++ b/src/app/shared/pipes/address.pipes.ts @@ -6,7 +6,7 @@ import {formatAddress} from "@app/shared/currencies"; }) export class AddressFormatPipe implements PipeTransform { - transform(value: string, withChecksum?: boolean ): string { - return formatAddress(value, withChecksum); + transform(value: string): string { + return formatAddress(value); } } diff --git a/src/app/shared/pipes/pipes.module.ts b/src/app/shared/pipes/pipes.module.ts index 81e39fac7105921f2988ab1f3097112cd75c2843..9d28b2abc4d62ae406db49d1cd5ff3e2b95d0596 100644 --- a/src/app/shared/pipes/pipes.module.ts +++ b/src/app/shared/pipes/pipes.module.ts @@ -34,7 +34,7 @@ import {FormGetArrayPipe, FormGetControlPipe, FormGetGroupPipe, FormGetPipe, For import {PropertyGetPipe} from './property.pipes'; import {AmountFormatPipe} from "@app/shared/pipes/amount.pipe"; import {AddressFormatPipe} from "@app/shared/pipes/address.pipes"; -import {AccountBalancePipe, AccountNamePipe} from "@app/shared/pipes/account.pipes"; +import {AccountBalancePipe, AccountNamePipe, AccountPropertyPipe} from "@app/shared/pipes/account.pipes"; @NgModule({ imports: [ @@ -81,6 +81,7 @@ import {AccountBalancePipe, AccountNamePipe} from "@app/shared/pipes/account.pip AddressFormatPipe, AbbreviatePipe, // Account pipes + AccountPropertyPipe, AccountBalancePipe, AccountNamePipe ], @@ -123,6 +124,7 @@ import {AccountBalancePipe, AccountNamePipe} from "@app/shared/pipes/account.pip AddressFormatPipe, AbbreviatePipe, // Account pipes + AccountPropertyPipe, AccountBalancePipe, AccountNamePipe ] diff --git a/src/app/shared/popover/list.popover.html b/src/app/shared/popover/list.popover.html new file mode 100644 index 0000000000000000000000000000000000000000..1cfcef6f7773016639950b19da0f9b9fc4c85a5c --- /dev/null +++ b/src/app/shared/popover/list.popover.html @@ -0,0 +1,11 @@ +<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> + </ion-list> +</ion-content> diff --git a/src/app/shared/popover/list.popover.module.ts b/src/app/shared/popover/list.popover.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..9aac0ec264ac389b5788e98f10df54985c9d35e4 --- /dev/null +++ b/src/app/shared/popover/list.popover.module.ts @@ -0,0 +1,22 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import {IonicModule} from '@ionic/angular'; +import {TranslateModule} from "@ngx-translate/core"; +import {ListPopover} from "./list.popover"; + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + TranslateModule + ], + declarations: [ + ListPopover + ], + exports: [ + ListPopover + ] + +}) +export class ListPopoverModule {} diff --git a/src/app/shared/popover/list.popover.scss b/src/app/shared/popover/list.popover.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/shared/popover/list.popover.ts b/src/app/shared/popover/list.popover.ts new file mode 100644 index 0000000000000000000000000000000000000000..9367becf37cb9c3768b27c1c048074ca2d853353 --- /dev/null +++ b/src/app/shared/popover/list.popover.ts @@ -0,0 +1,32 @@ +import {Component, Input} from "@angular/core"; +import {PopoverController} from "@ionic/angular"; + +export interface ListItem { + value: string; + label: string; + disabled?: boolean; +} + +export interface ListPopoverOptions { + title?: string; + items: ListItem[]; +} + +@Component({ + selector: 'app-list-popover', + templateUrl: './list.popover.html', + styleUrls: ['./list.popover.scss'] +}) +export class ListPopover { + + @Input() title: string = null; + @Input() items: ListItem[] = null; + + constructor(protected popoverCtrl: PopoverController) { + + } + + click(value: string){ + this.popoverCtrl.dismiss(value); + } +} diff --git a/src/app/shared/services/platform.service.ts b/src/app/shared/services/platform.service.ts index 4d38cf0bef6591d1a6142dc888f54955987be2a3..3298fca7e6695f60113f8d191275c6969a9e7552 100644 --- a/src/app/shared/services/platform.service.ts +++ b/src/app/shared/services/platform.service.ts @@ -7,8 +7,9 @@ import {StorageService} from "@app/shared/services/storage/storage.service"; import {environment} from "@environments/environment.prod"; import {TranslateService} from "@ngx-translate/core"; import * as momentImported from 'moment'; -import {Subject} from "rxjs"; -import {Settings} from "@app/settings/settings.model"; +import {StatusBar} from "@capacitor/status-bar"; +import {Keyboard} from "@capacitor/keyboard"; + const moment = momentImported; @Injectable({ @@ -18,7 +19,8 @@ export class PlatformService extends StartableService { private _mobile: boolean = null; private _touchUi: boolean = null; - + private _cordova: boolean = null; + private _capacitor: boolean = null; get mobile(): boolean { return this._mobile != null ? this._mobile : this.ionicPlatform.is('mobile'); @@ -29,6 +31,14 @@ export class PlatformService extends StartableService { (this.mobile || this.ionicPlatform.is('tablet') || this.ionicPlatform.is('phablet')); } + get capacitor(): boolean { + return this._capacitor != null ? this._capacitor : this.ionicPlatform.is('capacitor'); + } + + get cordova(): boolean { + return this._cordova != null ? this._cordova : this.ionicPlatform.is('cordova'); + } + constructor( protected ionicPlatform: Platform, protected translate: TranslateService, @@ -45,6 +55,11 @@ export class PlatformService extends StartableService { this._mobile = this.mobile; this._touchUi = this.touchUi; + this._cordova = this.cordova; + this._capacitor = this.capacitor; + + // Configure Capacitor plugins + await this.configureCapacitorPlugins(); // Configure translation await this.configureTranslate(); @@ -56,6 +71,24 @@ export class PlatformService extends StartableService { ]); } + protected async configureCapacitorPlugins() { + if (!this._capacitor) return; // Skip + + console.info('[platform] Configuring Cordova plugins...'); + + let plugin: string; + try { + plugin = 'StatusBar'; + await StatusBar.setOverlaysWebView({overlay: false}); + + plugin = 'Keyboard'; + await Keyboard.setAccessoryBarVisible({isVisible: false}); + } + catch(err) { + console.error(`[platform] Error while configuring ${plugin} plugin: ${err?.originalStack || JSON.stringify(err)}`); + } + } + protected configureTranslate() { console.info('[platform] Configuring i18n ...'); diff --git a/src/app/shared/services/plugins.ts b/src/app/shared/services/plugins.ts new file mode 100644 index 0000000000000000000000000000000000000000..c9c76b2d5d35a002aff8af8de5c76aae62e41c58 --- /dev/null +++ b/src/app/shared/services/plugins.ts @@ -0,0 +1,4 @@ +export const CapacitorPlugins = Object.freeze({ + Camera: 'Camera', + BarcodeScanner: 'BarcodeScanner' +}); diff --git a/src/app/shared/services/storage/storage.service.ts b/src/app/shared/services/storage/storage.service.ts index f2f043844f31cd3a9d31bd54d89f7f71c1b0bfad..ef2608bf6675a05c68e4fe29fe230274d548aa14 100644 --- a/src/app/shared/services/storage/storage.service.ts +++ b/src/app/shared/services/storage/storage.service.ts @@ -1,12 +1,12 @@ -import {Injectable} from '@angular/core'; +import {ENVIRONMENT_INITIALIZER, Injectable} from '@angular/core'; import {Storage} from '@ionic/storage-angular'; import {StartableService} from "@app/shared/services/startable-service.class"; import {IStorage} from "@app/shared/services/storage/storage.utils"; import {Platform} from '@ionic/angular'; +import {environment} from "@environments/environment"; +import cordovaSQLiteDriver from 'localforage-cordovasqlitedriver'; -@Injectable({ - providedIn: 'root' -}) +@Injectable({providedIn: 'root'}) export class StorageService extends StartableService<Storage> implements IStorage<Storage> { @@ -21,10 +21,21 @@ export class StorageService extends StartableService<Storage> } protected async ngOnStart(): Promise<Storage> { - await this.platform.ready(); - const storage = await this.storage.create(); - //console.info(`[storage-service] Started using driver=${storage?.driver}`); - return storage; + try { + console.debug(`[storage-service] Starting... {driverOrder: ${environment.storage?.driverOrder}}`); + + // Define Cordova SQLLite driver + await this.storage.defineDriver(cordovaSQLiteDriver); + + // Create the storage instance + const storage = await this.storage.create(); + + console.info(`[storage-service] Started using driver: ${storage?.driver}`); + return storage; + } + catch (err) { + console.error('[storage-service] Cannot create storage: ' + (err?.message || err), err); + } } async set(key: string, value: any) { diff --git a/src/app/shared/services/storage/storage.utils.ts b/src/app/shared/services/storage/storage.utils.ts index 085ab55faa48f830976ca1bcdc9187128bf9a14c..749ae87852ac77d7f16647bb10400dc6bd2639c9 100644 --- a/src/app/shared/services/storage/storage.utils.ts +++ b/src/app/shared/services/storage/storage.utils.ts @@ -2,6 +2,7 @@ import {InjectionToken} from "@angular/core"; import {Drivers} from "@ionic/storage"; import * as LocalForage from "localforage"; +import * as CordovaSQLiteDriver from 'localforage-cordovasqlitedriver'; export interface IStorage<T = any> { readonly driver: string; @@ -15,7 +16,7 @@ export interface IStorage<T = any> { } export const StorageDrivers = { - //SQLLite: CordovaSQLiteDriver._driver, + SQLLite: CordovaSQLiteDriver._driver, SecureStorage: Drivers.SecureStorage, WebSQL: LocalForage.WEBSQL, IndexedDB: Drivers.IndexedDB, diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 9043ba7f671e07ace2ad75627a4c051effec9299..06bc3af22dafaed6aa0188e517c06bed154c3404 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -5,8 +5,8 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {IonicModule} from '@ionic/angular'; import {TranslateModule} from "@ngx-translate/core"; import {SharedPipesModule} from "@app/shared/pipes/pipes.module"; -import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; import {QRCodeModule} from "angular2-qrcode"; +import {ListPopoverModule} from "@app/shared/popover/list.popover.module"; @NgModule({ imports: [ @@ -17,8 +17,9 @@ import {QRCodeModule} from "angular2-qrcode"; TranslateModule, QRCodeModule, - // App modules - SharedPipesModule + // Sub modules + SharedPipesModule, + ListPopoverModule ], exports: [ CommonModule, @@ -28,8 +29,9 @@ import {QRCodeModule} from "angular2-qrcode"; TranslateModule, QRCodeModule, - // App modules - SharedPipesModule + // Sub modules + SharedPipesModule, + ListPopoverModule ] }) export class AppSharedModule {} diff --git a/src/app/transfer/transfer.page.html b/src/app/transfer/transfer.page.html index a9fdbfeae1989e340301a8a914d5d4f7cef841f0..ec60681b2d4353370a2262c5cec6d6446c17bc97 100644 --- a/src/app/transfer/transfer.page.html +++ b/src/app/transfer/transfer.page.html @@ -26,19 +26,22 @@ <!-- TO --> <ion-item> <ion-label color="medium" translate>TRANSFER.TO</ion-label> - <ion-textarea *ngIf="data|async|isNotEmptyArray; else inputSkeleton" + <ion-textarea *ngIf="data|async|isNotEmptyArray; else inputSkeleton" #fromInput [rows]="mobile ? 2 : 1" - class="ion-text-wrap ion-text-end" - [(ngModel)]="recipient.address" (ionFocus)="wotModal.present()"> + class="ion-text-wrap ion-padding-start" + [(ngModel)]="recipient.address" + (ionFocus)="focusFrom($event, fromInput)"> </ion-textarea> - <ion-button slot="end" fill="clear" id="open-modal-trigger" - [class.cdk-visually-hidden]="loading || mobile" + <ion-button slot="end" fill="clear" + (click)="showWotModal($event, 0.75)" [disabled]="loading" [title]="'COMMON.BTN_SEARCH'|translate"> <ion-icon slot="icon-only" name="search"></ion-icon> </ion-button> + + <!-- Scan QR code--> <ion-button slot="end" fill="clear" color="dark" - *ngIf="_capacitor" + *ngIf="_enableScan" (click)="scanRecipient($event)" [disabled]="loading"> <ion-icon slot="icon-only" name="qr-code"></ion-icon> @@ -107,20 +110,22 @@ <ion-modal #wotModal - trigger="open-modal-trigger" - [initialBreakpoint]="0.25" + [initialBreakpoint]="_initialWotModalBreakpoint" [breakpoints]="[0.25, 0.5, 0.75]" - [backdropDismiss]="false" + [backdropDismiss]="true" [backdropBreakpoint]="0.5" - [keepContentsMounted]="true" > <ng-template> <app-wot-lookup - [showToolbar]="false" + showToolbar="true" + toolbarColor="secondary" [showSearchBar]="true" [showItemActions]="false" - (itemClick)="setRecipient($event) || wotModal.dismiss()" + (itemClick)="setRecipient($event) && hideWotModal()" (searchClick)="wotModal.setCurrentBreakpoint(0.75)"> + + <ion-button toolbar-end fill="clear" (click)="hideWotModal($event)" translate>COMMON.BTN_CLOSE</ion-button> + </app-wot-lookup> </ng-template> </ion-modal> diff --git a/src/app/transfer/transfer.page.ts b/src/app/transfer/transfer.page.ts index 15e151ed2ed402a6b4cbfe61089b9ddb79fb0a09..c685257c6716f4a0e38fe5c1258df1d4204c4df7 100644 --- a/src/app/transfer/transfer.page.ts +++ b/src/app/transfer/transfer.page.ts @@ -10,7 +10,7 @@ import { import {AccountService} from "../wallet/account.service"; import {BasePage} from "@app/shared/pages/base.page"; import {Account} from "@app/wallet/account.model"; -import {ActionSheetOptions, IonModal, Platform, PopoverOptions} from "@ionic/angular"; +import {ActionSheetOptions, IonModal, IonRouterOutlet, IonTextarea, Platform, PopoverOptions} from "@ionic/angular"; import {BehaviorSubject, firstValueFrom, Observable} from "rxjs"; import {isNotEmptyArray, isNotNilOrBlank} from "@app/shared/functions"; import {filter} from "rxjs/operators"; @@ -20,6 +20,8 @@ import {Currency} from "@app/network/currency.model"; import {Router} from "@angular/router"; import {BarcodeScanner} from "@capacitor-community/barcode-scanner"; import {BarcodeScannerWeb} from "@capacitor-community/barcode-scanner/dist/esm/web"; +import {Capacitor} from "@capacitor/core"; +import {CapacitorPlugins} from "@app/shared/services/plugins"; @Component({ selector: 'app-transfer', @@ -34,7 +36,7 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI recipient: Account = {address: null, meta: null}; amount: number; fee: number; - protected _capacitor: boolean; + protected _enableScan: boolean = false; protected actionSheetOptions: Partial<ActionSheetOptions> = { cssClass: 'select-account-action-sheet' @@ -79,22 +81,16 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI protected cd: ChangeDetectorRef ) { super(injector, {name: 'transfer', loadDueTime: 250}); - } ngOnInit() { super.ngOnInit(); + } + + ionViewWillLeave() { // Hide modal when leave page - this.registerSubscription( - this.router.events - .pipe(filter( - (value, index) => { - console.log(value); - return true; - } - )).subscribe() - ) + this.hideWotModal(); } async ngOnDestroy() { @@ -103,14 +99,62 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI await this.qrCodeModal?.dismiss(); } + protected _autoOpenWotModal = true; + + protected async focusFrom(event: UIEvent, textarea?: IonTextarea) { + + if (this._autoOpenWotModal) { + await this.showWotModal(event); + + if (textarea) { + const el = await textarea.getInputElement(); + setTimeout( () => el.focus(), 250); + } + } + + } + + protected _initialWotModalBreakpoint = 0.25 + + protected async showWotModal(event: UIEvent, breakpoint?: number) { + breakpoint = breakpoint || 0.25; + + this._initialWotModalBreakpoint = breakpoint; + + if (!this.wotModal.isCmpOpen) { + await this.wotModal.present(); + } + + // Set breakpoint + if (breakpoint > 0.25){ + const currentBreakpoint = await this.wotModal.getCurrentBreakpoint(); + if (breakpoint > currentBreakpoint) { + await this.wotModal.setCurrentBreakpoint(breakpoint); + } + } + } + + protected hideWotModal(event?: UIEvent) { + if (this.wotModal && this.wotModal.isCmpOpen) { + this.wotModal.dismiss(); + this._autoOpenWotModal = false; + } + } + protected async ngOnLoad(): Promise<Observable<Account[]>> { + + this._enableScan = this.ionicPlatform.is('capacitor') && Capacitor.isPluginAvailable(CapacitorPlugins.BarcodeScanner); + await this.accountService.ready(); const subject = new BehaviorSubject<Account[]>(null); this.registerSubscription( this.accountService.watchAll({positiveBalanceFirst: true}) .pipe(filter(isNotEmptyArray)) - .subscribe((value) => subject.next(value)) + .subscribe((value) => { + subject.next(value); + if (this.loaded) this.cd.markForCheck(); + }) ); const accounts = await firstValueFrom(subject); @@ -133,12 +177,11 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI this.fee = (this.networkService.currency?.fees.tx || 0) / Math.pow(10, this.networkService.currency?.decimals || 0); - this._capacitor = this.ionicPlatform.is('capacitor'); return subject; } - setRecipient(recipient: string|Account) { + setRecipient(recipient: string|Account): boolean { if (typeof recipient === 'object') { this.recipient = recipient; } @@ -146,6 +189,7 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI this.recipient = {address: recipient, meta: null}; } this.markForCheck(); + return true; } cancel(event?: UIEvent) { @@ -165,6 +209,10 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI await this.showToast({message: 'INFO.TRANSFER_SENT'}); this.reset(); + + if (this.routerOutlet.canGoBack()) { + await this.routerOutlet.pop(); + } } catch (err) { this.setError(err); @@ -173,7 +221,7 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI } async scanRecipient(event: UIEvent) { - if (!this._capacitor) return; // SKip + if (!this._enableScan) return; // SKip await BarcodeScanner.hideBackground(); // make background of WebView transparent diff --git a/src/app/wallet/account.model.ts b/src/app/wallet/account.model.ts index cb89529c8b0aa39a1a199ecef5841a4564bc604d..682d6d38e3f9a4959a921e8921329203972a560f 100644 --- a/src/app/wallet/account.model.ts +++ b/src/app/wallet/account.model.ts @@ -1,4 +1,5 @@ import {KeypairType} from "@polkadot/util-crypto/types"; +import {Subject} from "rxjs"; export interface Account { address: string; @@ -8,6 +9,7 @@ export interface Account { meta: AccountMeta; data?: AccountData; + dataSubject?: Subject<AccountData>; } export interface AccountMeta { name: string; diff --git a/src/app/wallet/account.service.ts b/src/app/wallet/account.service.ts index 3871f7a5b3bcf6ae99398c62b9a11869a495670e..8a056e8e080e9cb4ed2a58a26f17c834358d8d84 100644 --- a/src/app/wallet/account.service.ts +++ b/src/app/wallet/account.service.ts @@ -1,7 +1,7 @@ import {Inject, Injectable} from "@angular/core"; import {NetworkService} from "../network/network.service"; import {ApiPromise} from "@polkadot/api"; -import {Account, AccountMeta, AccountUtils} from "./account.model"; +import {Account, AccountData, AccountMeta, AccountUtils} from "./account.model"; import {StartableService} from "@app/shared/services/startable-service.class"; import {AuthData} from "@app/auth/auth.model"; import {keyring} from "@polkadot/ui-keyring"; @@ -26,7 +26,7 @@ import { firstValueFrom, from, map, - Observable, + Observable, Subject, Subscription, switchMap, timer @@ -132,7 +132,8 @@ export class AccountService extends StartableService { meta: { name: ka.meta.name, genesisHash: ka.meta.genesisHash - } + }, + dataSubject: new Subject<AccountData>() } }); @@ -492,7 +493,7 @@ export class AccountService extends StartableService { try { const now = Date.now(); - let loaded = false; + let changed = false; // Load balance (free + reserved) if (opts.withBalance === true && (isNil(account.data?.free) || opts.reload === true)) { @@ -502,7 +503,7 @@ export class AccountService extends StartableService { ...account.data, ...JSON.parse(data.toString()) }; - loaded = true; + changed = true; } // Load TX @@ -513,7 +514,10 @@ export class AccountService extends StartableService { //somethingLoaded = true; } - if (loaded) { + // Emit change event + if (changed) { + account.dataSubject = account.dataSubject || new Subject(); + account.dataSubject.next(account.data); console.debug(`${this._logPrefix} Loading ${formatAddress(account.address)} data [OK] in ${Date.now()-now}ms`, account.data); } diff --git a/src/app/wallet/wallet.page.html b/src/app/wallet/wallet.page.html index 7e2f43d829f769df6ce7e4cefa52a090d96d5eeb..024a1a11146013a33b34799dc22db36d579a594e 100644 --- a/src/app/wallet/wallet.page.html +++ b/src/app/wallet/wallet.page.html @@ -4,10 +4,9 @@ <ion-menu-button></ion-menu-button> </ion-buttons> <ion-title translate>ACCOUNT.TITLE</ion-title> - - <ng-container *ngIf="$account|async; let accounts"> - <ion-select slot="end" - *ngIf="accounts|isNotEmptyArray" + <ion-buttons slot="end"> + <ion-select *ngIf="$account|async; let accounts" + [class.cdk-visually-hidden]="accounts|isEmptyArray" [(ngModel)]="data" [interface]="mobile ? 'action-sheet' : 'popover'" [okText]="'COMMON.BTN_OK'|translate" @@ -16,8 +15,13 @@ [value]="account"> {{account|accountName}} </ion-select-option> + + <ion-select-option (click)="addNewWallet($event)" + [value]="null" + translate>ACCOUNT.WALLET_LIST.BTN_NEW</ion-select-option> </ion-select> - </ng-container> + </ion-buttons> + </ion-toolbar> <ion-progress-bar type="indeterminate" *ngIf="loading"></ion-progress-bar> </ion-header> @@ -31,9 +35,9 @@ <ion-icon slot="icon-only" name="qr-code"></ion-icon> </ion-button> </ion-buttons> - <ion-label slot="end" *ngIf="loaded" class="ion-text-end ion-margin-end"> - <b>{{ balance | amountFormat }}</b> - </ion-label> + <ion-title slot="end" *ngIf="loaded" class="balance"> + {{ data|balance|amountFormat }} + </ion-title> </ion-toolbar> </ion-header> diff --git a/src/app/wallet/wallet.page.scss b/src/app/wallet/wallet.page.scss index 8b137891791fe96927ad78e64b0aad7bded08bdc..e91c444f25c3053d3258bd164187683305806d98 100644 --- a/src/app/wallet/wallet.page.scss +++ b/src/app/wallet/wallet.page.scss @@ -1 +1,6 @@ +ion-toolbar { + ion-buttons[slot="end"] { + padding-inline-end: var(--ion-padding); + } +} diff --git a/src/app/wallet/wallet.page.ts b/src/app/wallet/wallet.page.ts index e6de42b2ba154651ab62131f59b39279a690a134..a71b014168f10deca589c33949fa0833e0f1c587 100644 --- a/src/app/wallet/wallet.page.ts +++ b/src/app/wallet/wallet.page.ts @@ -22,21 +22,11 @@ export class WalletPage extends BasePage<Account> implements OnInit, AfterViewCh $account = new BehaviorSubject<Account[]>(null); - get loaded(): boolean { - return !this.loading; - } - - get balance(): number { - if (!this.data?.data) return undefined; - return (this.data.data.free || 0) + (this.data.data.reserved || 0); - } - get account(): Account { return this.data; } @ViewChild('authModal') authModal: IonModal; - @ViewChild('qrCodeModal') qrCodeModal: IonModal; constructor( @@ -117,6 +107,10 @@ export class WalletPage extends BasePage<Account> implements OnInit, AfterViewCh this.qrCodeModal.present(); } + addNewWallet(event: UIEvent) { + + } + async openAuthModal(): Promise<Account|null> { if (!this.authModal.isOpen) { await this.authModal.present(); diff --git a/src/app/wot/wot-details.page.html b/src/app/wot/wot-details.page.html index 153114a1465f32f29d75b4b8488fcde8273a78ae..4c61f824af81db0c4a492e672abc8b10eb51ddc2 100644 --- a/src/app/wot/wot-details.page.html +++ b/src/app/wot/wot-details.page.html @@ -22,7 +22,7 @@ <ion-avatar slot="start" *ngIf="data?.meta?.avatar"> <ion-img [src]="data.meta.avatar"></ion-img> </ion-avatar> - <ion-title size="large" *ngIf="loaded; else loadingText"> + <ion-title size="large" *ngIf="loaded; else loadingText" class="balance"> {{ data|balance|amountFormat }} </ion-title> <ng-template #loadingText> diff --git a/src/app/wot/wot-lookup.page.html b/src/app/wot/wot-lookup.page.html index f17c45f88d9dd0db80f536cbd84d62582476101e..985d3071dfbeb009c163d776a438f1ba23b6446b 100644 --- a/src/app/wot/wot-lookup.page.html +++ b/src/app/wot/wot-lookup.page.html @@ -1,13 +1,16 @@ <ion-header [translucent]="true" *ngIf="showToolbar"> - <ion-toolbar color="primary"> + <ion-toolbar [color]="toolbarColor"> <ion-buttons slot="start"> <ion-menu-button></ion-menu-button> </ion-buttons> <ion-title translate>MENU.WOT</ion-title> + <ion-buttons slot="end"> + <ng-content select="[toolbar-end]"></ng-content> + </ion-buttons> </ion-toolbar> </ion-header> -<ion-content [fullscreen]="true"> +<ion-content [fullscreen]="showSearchBar"> <ion-header collapse="condense" *ngIf="showToolbar"> <ion-toolbar> <ion-title size="large" translate>MENU.WOT</ion-title> diff --git a/src/app/wot/wot-lookup.page.ts b/src/app/wot/wot-lookup.page.ts index 1304a9c8a9fdd942c8f7739c4045ccd9aa6f8829..0c02edcbb1fd2fd7bb2dab5fd777ccc505647b5d 100644 --- a/src/app/wot/wot-lookup.page.ts +++ b/src/app/wot/wot-lookup.page.ts @@ -6,6 +6,7 @@ import {Router} from "@angular/router"; import {WotService} from "@app/wot/wot.service"; import {WotSearchFilter} from "@app/wot/wot.model"; import {toBoolean} from "@app/shared/functions"; +import {PredefinedColors} from "@app/shared/colors/colors.utils"; @Component({ selector: 'app-wot-lookup', @@ -22,6 +23,7 @@ export class WotLookupPage extends BasePage<Account[]> implements OnInit { @Output() itemClick = new EventEmitter<Account>(); @Input() showItemActions: boolean; + @Input() toolbarColor: PredefinedColors = 'primary'; constructor(injector: Injector, private router: Router, @@ -80,7 +82,7 @@ export class WotLookupPage extends BasePage<Account[]> implements OnInit { super.markAsLoading(); } - searchChanged(event: CustomEvent<any>, value: string) { + searchChanged(event: Event, value: string) { if (!event || event.defaultPrevented) return; event.preventDefault(); event.stopPropagation(); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index a7bdf553bf0ecb2006a035a40291c2168894b94a..7e2b18563fd844c83c6f7e93fc8779e9a1a0cd28 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -438,7 +438,7 @@ "ASSOCIATED_PUBKEY": "Public key :", "BTN_METHODS": "Other methods", "BTN_METHODS_DOTS": "Change method...", - "METHOD_POPOVER_TITLE": "Methods", + "METHOD_POPOVER_TITLE": "Connection methods", "MEMORIZE_AUTH_FILE": "Memorize this keychain during the navigation session", "SCRYPT_PARAMETERS": "Paramètres (Scrypt) :", "AUTO_LOGOUT": { diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index 24cb1c6f8f60d3dd3a71840c98a0d382d1f47078..1e7c46c0711da04e460437365baa397129945033 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -1,6 +1,6 @@ { "COMMON": { - "APP_NAME": "Cesium", + "APP_NAME": "Cesium²", "APP_VERSION": "v{{version}}", "APP_BUILD": "date : {{build}}", "PUBKEY": "Clé publique", @@ -438,7 +438,7 @@ "ASSOCIATED_PUBKEY": "Clé publique du trousseau :", "BTN_METHODS": "Autres méthodes", "BTN_METHODS_DOTS": "Changer de méthode...", - "METHOD_POPOVER_TITLE": "Méthodes", + "METHOD_POPOVER_TITLE": "Méthodes de connexion :", "MEMORIZE_AUTH_FILE": "Mémoriser ce trousseau le temps de la session de navigation", "SCRYPT_PARAMETERS": "Paramètres (Scrypt) :", "AUTO_LOGOUT": { diff --git a/src/polyfills.ts b/src/polyfills.ts index 27f5e432a014fa4fa7b8ba93744eab9dfea054e3..e97a8b57e4ca9903b86bf06b2350e7f46d983875 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -53,6 +53,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ +// Polkadot API augment import '@polkadot/api-augment' (window as any).global = window; diff --git a/src/theme/_cesium.scss b/src/theme/_cesium.scss index 4eb8f3541b6839e0d8ab0a4a519d469f7ca0d5a3..393cbbe968f18aac47c59f8c18cd84fa3af54e79 100644 --- a/src/theme/_cesium.scss +++ b/src/theme/_cesium.scss @@ -32,3 +32,11 @@ qr-code { ion-toolbar ion-menu-button { color: var(--ion-color-base-contrast); } + +ion-toolbar { + user-select: none; + + ion-title.balance { + font-weight: bold; + } +} diff --git a/tsconfig.json b/tsconfig.json index 268903a5b2f26aaa18569674b41291a658d6ad4b..d403b27907d58c898a13d29de8c7e9e02be4029c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "incremental": true, "target": "es2020", "module": "es2020", - "lib": ["es2018", "dom"], + "lib": ["es2020", "dom"], "allowSyntheticDefaultImports": true, "typeRoots": [ "node_modules/@types" @@ -28,7 +28,7 @@ "rxjs/*": ["node_modules/rxjs/*"], // Local deps // Package name - "@duniter/core-types/*": ["src/*"], + "@duniter/types": ["src/interfaces/types-lookup.ts"], // here we replace the @polkadot/api augmentation with our own, generated from chain "@polkadot/api/augment": ["src/interfaces/augment-api.ts"], @@ -50,6 +50,6 @@ "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, - "strictTemplates": true + "strictTemplates": false } } diff --git a/yarn.lock b/yarn.lock index 76d1d5abe78d16b0e5aea3b00bbabf5f6b4af943..78bbb45842f5b74325bc147af26a558c7c74b55a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1313,23 +1313,26 @@ resolved "https://registry.npmjs.org/@capacitor-community/barcode-scanner/-/barcode-scanner-3.0.0.tgz" integrity sha512-Jzr4phHyDS+C2ZE+eoVsuq15oFvFLVtAJmmegIjwVBcb3CxgRdkt/kdVurXgVKMQ6mJ6StYI7hALeMu2vkyC6A== -"@capacitor-community/sqlite@^4.0.1": - version "4.0.1" - resolved "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-4.0.1.tgz" - integrity sha512-bKdIUfDm+fXyZm2WZWWceFZjv8Zn0bRUI2lhaCRKJNqFk3jTI7z4qGTB2zqsKcijVhYF18j21pzXqJc3Ybb9xA== - dependencies: - jeep-sqlite "^1.5.5" - -"@capacitor/android@4.1.0": - version "4.1.0" - resolved "https://registry.npmjs.org/@capacitor/android/-/android-4.1.0.tgz" - integrity sha512-aYHvpYVlS6WC+bG9jJfwqgHMxTw3e8f3taNnl/y9qCjglmMmtFcZWFAVLlOleVK4Q7olSirqjx37f0ppvxRTLg== +"@capacitor/android@~4.2.0": + version "4.2.0" + resolved "https://registry.npmjs.org/@capacitor/android/-/android-4.2.0.tgz#f06539694adbaf189cf38cb039f079bde4f7422b" + integrity sha512-LWZhM31DoQuNSW8ZGslJ8gZfOAZS2A5TLq30HP1bn0OQTJGvOFIizQysraVRSOOq5FRptykf2nZWu6WEwoKMlA== -"@capacitor/app@4.0.1": +"@capacitor/app@~4.0.1": version "4.0.1" - resolved "https://registry.npmjs.org/@capacitor/app/-/app-4.0.1.tgz" + resolved "https://registry.npmjs.org/@capacitor/app/-/app-4.0.1.tgz#05c60541c427ef30f4762d8f786e70d0ed32fe01" integrity sha512-frPft9TMJL70jWq5jmxwGSBddZVvsWxX5Agj2i19WJerk37aTgljB05HRr/YLg6mF1G/NIXmmFJZDY8MEgirDg== +"@capacitor/browser@~4.0.1": + version "4.0.1" + resolved "https://registry.npmjs.org/@capacitor/browser/-/browser-4.0.1.tgz#0a725fea27d706940af8dbcc362b6d14bf20bfd2" + integrity sha512-tPOA/eYDJdDeMFd+A3I8teuGfPP0GEqhWtMzH8z5r2tW40iJ/mL8qH5NgVLMZ3uvs7gN2LfQTCMNJvl0F/WdBw== + +"@capacitor/camera@~4.1.1": + version "4.1.1" + resolved "https://registry.npmjs.org/@capacitor/camera/-/camera-4.1.1.tgz#a361b3595c378666614e3bd3384d158b85e4d7c3" + integrity sha512-L/1KLg4IRCAUmwhmu5jIic4U2OLAHFSW5GoExFU9yR8iCJo1SBGSiay2TpU3PpgmJHRtazO6pxKUCyAmDExkhw== + "@capacitor/cli@4.1.0": version "4.1.0" resolved "https://registry.npmjs.org/@capacitor/cli/-/cli-4.1.0.tgz" @@ -1353,36 +1356,36 @@ tslib "^2.4.0" xml2js "^0.4.23" -"@capacitor/clipboard@^4.0.1": +"@capacitor/clipboard@~4.0.1": version "4.0.1" - resolved "https://registry.npmjs.org/@capacitor/clipboard/-/clipboard-4.0.1.tgz" + resolved "https://registry.npmjs.org/@capacitor/clipboard/-/clipboard-4.0.1.tgz#b96ae5563583d12e510745fff03aa23050284948" integrity sha512-DO5fC6ax5Tm/4K77NjxRLu/bdyvO6FDCK38w05CE4LHvi3RF4LTM8EgnmIrEGKxwwbH5VloTeca9Cu6bsMXPiA== -"@capacitor/core@4.1.0": +"@capacitor/core@~4.1.0": version "4.1.0" resolved "https://registry.npmjs.org/@capacitor/core/-/core-4.1.0.tgz#4f5b80cd9cbf65bfc5a203082399fa794ffc5f45" integrity sha512-bMYFnC5l5n11D/kLTz9TOOJfUH+nQqYRwzG7h5h4AXIm8aFxH5ZY3iFlDZd7R4xcyrlnfxzfoooSH9trPjRBLA== dependencies: tslib "^2.1.0" -"@capacitor/haptics@4.0.1": +"@capacitor/haptics@~4.0.1": version "4.0.1" - resolved "https://registry.npmjs.org/@capacitor/haptics/-/haptics-4.0.1.tgz" + resolved "https://registry.npmjs.org/@capacitor/haptics/-/haptics-4.0.1.tgz#8113c757f9bce3cd6618f6aadda47e7aa7a92069" integrity sha512-ZLVoLdK1Md/xIRRrDOnrLCKGRg8UENY1ShpKcysPN1z1MgcpvB/9Nbqczm3RH24zyo3MP/7homkqMzUGxPBlwQ== -"@capacitor/keyboard@4.0.1": +"@capacitor/keyboard@~4.0.1": version "4.0.1" - resolved "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-4.0.1.tgz" + resolved "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-4.0.1.tgz#36cd5f8aa2ef87a722f318e525d1bda12e7811d1" integrity sha512-JZVci2v9jAKH0sIoNNZzmw/cWGXWf+KneLt0yDr/6YSs/2/tfuH10yOrUOhgrKFkR+fFj/rddTClQXUQ8Rqcrg== -"@capacitor/splash-screen@4.0.1": +"@capacitor/splash-screen@~4.0.1": version "4.0.1" - resolved "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-4.0.1.tgz" + resolved "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-4.0.1.tgz#5017a33bade5509e075f010c6381190e6ca6c5ba" integrity sha512-7hklUx69aZDonuLP1R5X4ZTGgZLwX8RTj9/3U1905Kz/XflcT7Rhighbad+uZBaOU+L/8Vm6Y3RlR3rFj4ELVA== -"@capacitor/status-bar@4.0.1": +"@capacitor/status-bar@~4.0.1": version "4.0.1" - resolved "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-4.0.1.tgz" + resolved "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-4.0.1.tgz#d1320add5f4ef383394b94beae45a0c5af7250e1" integrity sha512-BmEyOf3m/hAf8bO4hCX0m5gpQPSgd05mGYl+9E841WUZeJjcYlmiG/EBshAUb2uGCVtyNaG36yPXB0r0Ypg+rw== "@colors/colors@1.5.0": @@ -1653,7 +1656,7 @@ ionicons "^6.0.3" tslib "^2.1.0" -"@ionic/pwa-elements@~3.1.1": +"@ionic/pwa-elements@^3.1.1": version "3.1.1" resolved "https://registry.npmjs.org/@ionic/pwa-elements/-/pwa-elements-3.1.1.tgz#ae1a97260886545b3285364974674576f2b090a7" integrity sha512-BoEX1pSrKn5dP4VQwaf8DweAmT4ynmpW+9f6oj/W1xLe54bmuXcUY07ziuZX1DLu50xHSYcO5qJ/dbYC/w6wgA== @@ -2507,11 +2510,6 @@ resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz" integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== -"@types/emscripten@*": - version "1.39.6" - resolved "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.6.tgz" - integrity sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg== - "@types/eslint-scope@^3.7.3": version "3.7.3" resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz" @@ -2616,11 +2614,16 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>=10.0.0", "@types/node@^12.20.55": +"@types/node@*", "@types/node@>=10.0.0": version "12.20.55" resolved "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz" integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== +"@types/node@^14.18.28": + version "14.18.28" + resolved "https://registry.npmjs.org/@types/node/-/node-14.18.28.tgz#ddb82da2fff476a8e827e8773c84c19d9c235278" + integrity sha512-CK2fnrQlIgKlCV3N2kM+Gznb5USlwA1KFX3rJVHmgVk6NJxFPuQ86pAcvKnu37IA4BGlSRz7sEE1lHL1aLZ/eQ== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz" @@ -2686,14 +2689,6 @@ dependencies: "@types/node" "*" -"@types/sql.js@^1.4.3": - version "1.4.3" - resolved "https://registry.npmjs.org/@types/sql.js/-/sql.js-1.4.3.tgz" - integrity sha512-3bz1LJIiJtKMEL8tYf7c9Nrb1lYcFeWQkE8vhWvobE29ZzizW79DtoTjqx1bR82DS2Ch2K30nOwNhuLclZ1vYg== - dependencies: - "@types/emscripten" "*" - "@types/node" "*" - "@types/websocket@^1.0.5": version "1.0.5" resolved "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.5.tgz" @@ -7129,17 +7124,6 @@ jdenticon@^3.1.1: dependencies: canvas-renderer "~2.2.0" -jeep-sqlite@^1.5.5: - version "1.5.7" - resolved "https://registry.npmjs.org/jeep-sqlite/-/jeep-sqlite-1.5.7.tgz" - integrity sha512-/lxBCs1uw3cvsdZizhxw8Kn/7SX3Xcxel7ifRiokyDekDqH9fpcdBh+W1lCq+u895bYL+l9XoqceT4zqUI5zvA== - dependencies: - "@stencil/core" "^2.17.4" - "@types/sql.js" "^1.4.3" - jszip "^3.7.1" - localforage "^1.10.0" - sql.js "^1.7.0" - jest-worker@^27.4.5: version "27.5.1" resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" @@ -7282,16 +7266,6 @@ jszip@^3.1.3: readable-stream "~2.3.6" set-immediate-shim "~1.0.1" -jszip@^3.7.1: - version "3.10.1" - resolved "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz" - integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== - dependencies: - lie "~3.3.0" - pako "~1.0.2" - readable-stream "~2.3.6" - setimmediate "^1.0.5" - karma-chrome-launcher@~3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz" @@ -7497,7 +7471,14 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -localforage@^1.10.0, localforage@^1.9.0: +localforage-cordovasqlitedriver@~1.8.0: + version "1.8.0" + resolved "https://registry.npmjs.org/localforage-cordovasqlitedriver/-/localforage-cordovasqlitedriver-1.8.0.tgz#13a3613650b01e0899aaf19336ed4655cc0bf77f" + integrity sha512-AeYiVPURow8gPAGHNOiGMS9rlgv81wUuQLtnyCP6Eh1mq+IsqNl9fwAOP+RiTi6aO/Wfy3TTWiW2WtbTdJaUnQ== + dependencies: + localforage ">=1.5.0" + +localforage@>=1.5.0, localforage@^1.9.0, localforage@~1.10.0: version "1.10.0" resolved "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz" integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== @@ -9859,11 +9840,6 @@ set-immediate-shim@~1.0.1: resolved "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz" integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" - integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== - setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" @@ -10128,11 +10104,6 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sql.js@^1.7.0: - version "1.7.0" - resolved "https://registry.npmjs.org/sql.js/-/sql.js-1.7.0.tgz" - integrity sha512-qAfft3xkSgHqmmfNugWTp/59PsqIw8gbeao5TZmpmzQQsAJ49de3iDDKuxVixidYs6dkHNksY8m27v2dZNn2jw== - ssh-config@^1.1.1: version "1.1.6" resolved "https://registry.npmjs.org/ssh-config/-/ssh-config-1.1.6.tgz#c6ce2d7f85f395178c9e47c448d62b8adf9b2523" @@ -10526,9 +10497,9 @@ tree-kill@1.2.2, tree-kill@^1.2.2: resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -ts-node@^8.6.2: +ts-node@^8.10.2: version "8.10.2" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== dependencies: arg "^4.1.0" @@ -10783,7 +10754,7 @@ uuid@8.3.2, uuid@^8.3.2: uuid@^3.3.2: version "3.4.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== v8-compile-cache@2.3.0, v8-compile-cache@^2.0.3: @@ -11025,7 +10996,7 @@ which-module@^2.0.0: which@^1.2.1: version "1.3.1" - resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" + resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" @@ -11068,7 +11039,7 @@ wordwrap@^1.0.0: wrap-ansi@^6.2.0: version "6.2.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== dependencies: ansi-styles "^4.0.0"