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

[enh] transfer: fix when wot modal is opened

[enh] account pipes: auto-update value when account data changed (balance, name, etc.)
parent 07998442
No related branches found
No related tags found
No related merge requests found
Showing
with 362 additions and 87 deletions
...@@ -5,7 +5,7 @@ import {changeCaseToUnderscore, isNotNilOrBlank} from "@app/shared/functions"; ...@@ -5,7 +5,7 @@ import {changeCaseToUnderscore, isNotNilOrBlank} from "@app/shared/functions";
import {environment} from "@environments/environment"; import {environment} from "@environments/environment";
import {waitIdle} from "@app/shared/forms"; import {waitIdle} from "@app/shared/forms";
import {WaitForOptions} from "@app/shared/observables"; 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 {TranslateService} from "@ngx-translate/core";
import {Subscription} from "rxjs"; import {Subscription} from "rxjs";
...@@ -26,6 +26,7 @@ export abstract class BasePage< ...@@ -26,6 +26,7 @@ export abstract class BasePage<
protected translate: TranslateService; protected translate: TranslateService;
protected settings: SettingsService; protected settings: SettingsService;
protected readonly routerOutlet: IonRouterOutlet;
protected readonly activatedRoute: ActivatedRoute; protected readonly activatedRoute: ActivatedRoute;
protected toastController: ToastController; protected toastController: ToastController;
protected readonly _debug = !environment.production; protected readonly _debug = !environment.production;
...@@ -49,6 +50,7 @@ export abstract class BasePage< ...@@ -49,6 +50,7 @@ export abstract class BasePage<
this._cd = injector.get(ChangeDetectorRef); this._cd = injector.get(ChangeDetectorRef);
this.settings = injector.get(SettingsService); this.settings = injector.get(SettingsService);
this.translate = injector.get(TranslateService); this.translate = injector.get(TranslateService);
this.routerOutlet = injector.get(IonRouterOutlet);
this.activatedRoute = injector.get(ActivatedRoute); this.activatedRoute = injector.get(ActivatedRoute);
this.toastController = injector.get(ToastController); this.toastController = injector.get(ToastController);
this.mobile = this.settings.mobile; this.mobile = this.settings.mobile;
...@@ -167,4 +169,5 @@ export abstract class BasePage< ...@@ -167,4 +169,5 @@ export abstract class BasePage<
protected unregisterSubscription(sub: Subscription) { protected unregisterSubscription(sub: Subscription) {
this._subscription?.remove(sub); this._subscription?.remove(sub);
} }
} }
import {Pipe, PipeTransform} from '@angular/core'; import {ChangeDetectorRef, Directive, Pipe, PipeTransform} from '@angular/core';
import {NumberFormatPipe} from "@app/shared/pipes/number-format.pipe"; import {NumberFormatPipe} from "@app/shared/pipes/number-format.pipe";
import {NetworkService} from "@app/network/network.service"; import {NetworkService} from "@app/network/network.service";
import {Account, AccountUtils} from "@app/wallet/account.model"; import {Account, AccountData, AccountUtils} from "@app/wallet/account.model";
import {isNotNilOrBlank} from "@app/shared/functions"; import {equals, getPropertyByPath, isNotNilOrBlank} from "@app/shared/functions";
import {AddressFormatPipe} from "@app/shared/pipes/address.pipes"; 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({ @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> {
delegate = new NumberFormatPipe(); constructor(_ref: ChangeDetectorRef) {
super(_ref);
}
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 { protected _transform(account: Partial<Account>): number {
if (!account?.data) return undefined;
return AccountUtils.getBalance(account); return AccountUtils.getBalance(account);
} }
} }
@Pipe({ @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 { protected _transform(account: Partial<Account>): string {
return account?.meta?.name || this.addressFormatter.transform(account?.address, true); return account?.meta?.name || formatAddress(account?.address);
} }
} }
...@@ -6,7 +6,7 @@ import {formatAddress} from "@app/shared/currencies"; ...@@ -6,7 +6,7 @@ import {formatAddress} from "@app/shared/currencies";
}) })
export class AddressFormatPipe implements PipeTransform { export class AddressFormatPipe implements PipeTransform {
transform(value: string, withChecksum?: boolean ): string { transform(value: string): string {
return formatAddress(value, withChecksum); return formatAddress(value);
} }
} }
...@@ -34,7 +34,7 @@ import {FormGetArrayPipe, FormGetControlPipe, FormGetGroupPipe, FormGetPipe, For ...@@ -34,7 +34,7 @@ import {FormGetArrayPipe, FormGetControlPipe, FormGetGroupPipe, FormGetPipe, For
import {PropertyGetPipe} from './property.pipes'; import {PropertyGetPipe} from './property.pipes';
import {AmountFormatPipe} from "@app/shared/pipes/amount.pipe"; import {AmountFormatPipe} from "@app/shared/pipes/amount.pipe";
import {AddressFormatPipe} from "@app/shared/pipes/address.pipes"; 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({ @NgModule({
imports: [ imports: [
...@@ -81,6 +81,7 @@ import {AccountBalancePipe, AccountNamePipe} from "@app/shared/pipes/account.pip ...@@ -81,6 +81,7 @@ import {AccountBalancePipe, AccountNamePipe} from "@app/shared/pipes/account.pip
AddressFormatPipe, AddressFormatPipe,
AbbreviatePipe, AbbreviatePipe,
// Account pipes // Account pipes
AccountPropertyPipe,
AccountBalancePipe, AccountBalancePipe,
AccountNamePipe AccountNamePipe
], ],
...@@ -123,6 +124,7 @@ import {AccountBalancePipe, AccountNamePipe} from "@app/shared/pipes/account.pip ...@@ -123,6 +124,7 @@ import {AccountBalancePipe, AccountNamePipe} from "@app/shared/pipes/account.pip
AddressFormatPipe, AddressFormatPipe,
AbbreviatePipe, AbbreviatePipe,
// Account pipes // Account pipes
AccountPropertyPipe,
AccountBalancePipe, AccountBalancePipe,
AccountNamePipe AccountNamePipe
] ]
......
<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>
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 {}
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);
}
}
...@@ -7,8 +7,9 @@ import {StorageService} from "@app/shared/services/storage/storage.service"; ...@@ -7,8 +7,9 @@ import {StorageService} from "@app/shared/services/storage/storage.service";
import {environment} from "@environments/environment.prod"; import {environment} from "@environments/environment.prod";
import {TranslateService} from "@ngx-translate/core"; import {TranslateService} from "@ngx-translate/core";
import * as momentImported from 'moment'; import * as momentImported from 'moment';
import {Subject} from "rxjs"; import {StatusBar} from "@capacitor/status-bar";
import {Settings} from "@app/settings/settings.model"; import {Keyboard} from "@capacitor/keyboard";
const moment = momentImported; const moment = momentImported;
@Injectable({ @Injectable({
...@@ -18,7 +19,8 @@ export class PlatformService extends StartableService { ...@@ -18,7 +19,8 @@ export class PlatformService extends StartableService {
private _mobile: boolean = null; private _mobile: boolean = null;
private _touchUi: boolean = null; private _touchUi: boolean = null;
private _cordova: boolean = null;
private _capacitor: boolean = null;
get mobile(): boolean { get mobile(): boolean {
return this._mobile != null ? this._mobile : this.ionicPlatform.is('mobile'); return this._mobile != null ? this._mobile : this.ionicPlatform.is('mobile');
...@@ -29,6 +31,14 @@ export class PlatformService extends StartableService { ...@@ -29,6 +31,14 @@ export class PlatformService extends StartableService {
(this.mobile || this.ionicPlatform.is('tablet') || this.ionicPlatform.is('phablet')); (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( constructor(
protected ionicPlatform: Platform, protected ionicPlatform: Platform,
protected translate: TranslateService, protected translate: TranslateService,
...@@ -45,6 +55,11 @@ export class PlatformService extends StartableService { ...@@ -45,6 +55,11 @@ export class PlatformService extends StartableService {
this._mobile = this.mobile; this._mobile = this.mobile;
this._touchUi = this.touchUi; this._touchUi = this.touchUi;
this._cordova = this.cordova;
this._capacitor = this.capacitor;
// Configure Capacitor plugins
await this.configureCapacitorPlugins();
// Configure translation // Configure translation
await this.configureTranslate(); await this.configureTranslate();
...@@ -56,6 +71,24 @@ export class PlatformService extends StartableService { ...@@ -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() { protected configureTranslate() {
console.info('[platform] Configuring i18n ...'); console.info('[platform] Configuring i18n ...');
......
export const CapacitorPlugins = Object.freeze({
Camera: 'Camera',
BarcodeScanner: 'BarcodeScanner'
});
import {Injectable} from '@angular/core'; import {ENVIRONMENT_INITIALIZER, Injectable} from '@angular/core';
import {Storage} from '@ionic/storage-angular'; import {Storage} from '@ionic/storage-angular';
import {StartableService} from "@app/shared/services/startable-service.class"; import {StartableService} from "@app/shared/services/startable-service.class";
import {IStorage} from "@app/shared/services/storage/storage.utils"; import {IStorage} from "@app/shared/services/storage/storage.utils";
import {Platform} from '@ionic/angular'; import {Platform} from '@ionic/angular';
import {environment} from "@environments/environment";
import cordovaSQLiteDriver from 'localforage-cordovasqlitedriver';
@Injectable({ @Injectable({providedIn: 'root'})
providedIn: 'root'
})
export class StorageService extends StartableService<Storage> export class StorageService extends StartableService<Storage>
implements IStorage<Storage> { implements IStorage<Storage> {
...@@ -21,11 +21,22 @@ export class StorageService extends StartableService<Storage> ...@@ -21,11 +21,22 @@ export class StorageService extends StartableService<Storage>
} }
protected async ngOnStart(): Promise<Storage> { protected async ngOnStart(): Promise<Storage> {
await this.platform.ready(); 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(); const storage = await this.storage.create();
//console.info(`[storage-service] Started using driver=${storage?.driver}`);
console.info(`[storage-service] Started using driver: ${storage?.driver}`);
return storage; return storage;
} }
catch (err) {
console.error('[storage-service] Cannot create storage: ' + (err?.message || err), err);
}
}
async set(key: string, value: any) { async set(key: string, value: any) {
//if (this._debug) console.debug(`[storage-service] Set ${key} = `, value); //if (this._debug) console.debug(`[storage-service] Set ${key} = `, value);
......
...@@ -2,6 +2,7 @@ import {InjectionToken} from "@angular/core"; ...@@ -2,6 +2,7 @@ import {InjectionToken} from "@angular/core";
import {Drivers} from "@ionic/storage"; import {Drivers} from "@ionic/storage";
import * as LocalForage from "localforage"; import * as LocalForage from "localforage";
import * as CordovaSQLiteDriver from 'localforage-cordovasqlitedriver';
export interface IStorage<T = any> { export interface IStorage<T = any> {
readonly driver: string; readonly driver: string;
...@@ -15,7 +16,7 @@ export interface IStorage<T = any> { ...@@ -15,7 +16,7 @@ export interface IStorage<T = any> {
} }
export const StorageDrivers = { export const StorageDrivers = {
//SQLLite: CordovaSQLiteDriver._driver, SQLLite: CordovaSQLiteDriver._driver,
SecureStorage: Drivers.SecureStorage, SecureStorage: Drivers.SecureStorage,
WebSQL: LocalForage.WEBSQL, WebSQL: LocalForage.WEBSQL,
IndexedDB: Drivers.IndexedDB, IndexedDB: Drivers.IndexedDB,
......
...@@ -5,8 +5,8 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms'; ...@@ -5,8 +5,8 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {IonicModule} from '@ionic/angular'; import {IonicModule} from '@ionic/angular';
import {TranslateModule} from "@ngx-translate/core"; import {TranslateModule} from "@ngx-translate/core";
import {SharedPipesModule} from "@app/shared/pipes/pipes.module"; import {SharedPipesModule} from "@app/shared/pipes/pipes.module";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {QRCodeModule} from "angular2-qrcode"; import {QRCodeModule} from "angular2-qrcode";
import {ListPopoverModule} from "@app/shared/popover/list.popover.module";
@NgModule({ @NgModule({
imports: [ imports: [
...@@ -17,8 +17,9 @@ import {QRCodeModule} from "angular2-qrcode"; ...@@ -17,8 +17,9 @@ import {QRCodeModule} from "angular2-qrcode";
TranslateModule, TranslateModule,
QRCodeModule, QRCodeModule,
// App modules // Sub modules
SharedPipesModule SharedPipesModule,
ListPopoverModule
], ],
exports: [ exports: [
CommonModule, CommonModule,
...@@ -28,8 +29,9 @@ import {QRCodeModule} from "angular2-qrcode"; ...@@ -28,8 +29,9 @@ import {QRCodeModule} from "angular2-qrcode";
TranslateModule, TranslateModule,
QRCodeModule, QRCodeModule,
// App modules // Sub modules
SharedPipesModule SharedPipesModule,
ListPopoverModule
] ]
}) })
export class AppSharedModule {} export class AppSharedModule {}
...@@ -26,19 +26,22 @@ ...@@ -26,19 +26,22 @@
<!-- TO --> <!-- TO -->
<ion-item> <ion-item>
<ion-label color="medium" translate>TRANSFER.TO</ion-label> <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" [rows]="mobile ? 2 : 1"
class="ion-text-wrap ion-text-end" class="ion-text-wrap ion-padding-start"
[(ngModel)]="recipient.address" (ionFocus)="wotModal.present()"> [(ngModel)]="recipient.address"
(ionFocus)="focusFrom($event, fromInput)">
</ion-textarea> </ion-textarea>
<ion-button slot="end" fill="clear" id="open-modal-trigger" <ion-button slot="end" fill="clear"
[class.cdk-visually-hidden]="loading || mobile" (click)="showWotModal($event, 0.75)"
[disabled]="loading" [disabled]="loading"
[title]="'COMMON.BTN_SEARCH'|translate"> [title]="'COMMON.BTN_SEARCH'|translate">
<ion-icon slot="icon-only" name="search"></ion-icon> <ion-icon slot="icon-only" name="search"></ion-icon>
</ion-button> </ion-button>
<!-- Scan QR code-->
<ion-button slot="end" fill="clear" color="dark" <ion-button slot="end" fill="clear" color="dark"
*ngIf="_capacitor" *ngIf="_enableScan"
(click)="scanRecipient($event)" (click)="scanRecipient($event)"
[disabled]="loading"> [disabled]="loading">
<ion-icon slot="icon-only" name="qr-code"></ion-icon> <ion-icon slot="icon-only" name="qr-code"></ion-icon>
...@@ -107,20 +110,22 @@ ...@@ -107,20 +110,22 @@
<ion-modal <ion-modal
#wotModal #wotModal
trigger="open-modal-trigger" [initialBreakpoint]="_initialWotModalBreakpoint"
[initialBreakpoint]="0.25"
[breakpoints]="[0.25, 0.5, 0.75]" [breakpoints]="[0.25, 0.5, 0.75]"
[backdropDismiss]="false" [backdropDismiss]="true"
[backdropBreakpoint]="0.5" [backdropBreakpoint]="0.5"
[keepContentsMounted]="true"
> >
<ng-template> <ng-template>
<app-wot-lookup <app-wot-lookup
[showToolbar]="false" showToolbar="true"
toolbarColor="secondary"
[showSearchBar]="true" [showSearchBar]="true"
[showItemActions]="false" [showItemActions]="false"
(itemClick)="setRecipient($event) || wotModal.dismiss()" (itemClick)="setRecipient($event) && hideWotModal()"
(searchClick)="wotModal.setCurrentBreakpoint(0.75)"> (searchClick)="wotModal.setCurrentBreakpoint(0.75)">
<ion-button toolbar-end fill="clear" (click)="hideWotModal($event)" translate>COMMON.BTN_CLOSE</ion-button>
</app-wot-lookup> </app-wot-lookup>
</ng-template> </ng-template>
</ion-modal> </ion-modal>
......
...@@ -10,7 +10,7 @@ import { ...@@ -10,7 +10,7 @@ import {
import {AccountService} from "../wallet/account.service"; import {AccountService} from "../wallet/account.service";
import {BasePage} from "@app/shared/pages/base.page"; import {BasePage} from "@app/shared/pages/base.page";
import {Account} from "@app/wallet/account.model"; 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 {BehaviorSubject, firstValueFrom, Observable} from "rxjs";
import {isNotEmptyArray, isNotNilOrBlank} from "@app/shared/functions"; import {isNotEmptyArray, isNotNilOrBlank} from "@app/shared/functions";
import {filter} from "rxjs/operators"; import {filter} from "rxjs/operators";
...@@ -20,6 +20,8 @@ import {Currency} from "@app/network/currency.model"; ...@@ -20,6 +20,8 @@ import {Currency} from "@app/network/currency.model";
import {Router} from "@angular/router"; import {Router} from "@angular/router";
import {BarcodeScanner} from "@capacitor-community/barcode-scanner"; import {BarcodeScanner} from "@capacitor-community/barcode-scanner";
import {BarcodeScannerWeb} from "@capacitor-community/barcode-scanner/dist/esm/web"; import {BarcodeScannerWeb} from "@capacitor-community/barcode-scanner/dist/esm/web";
import {Capacitor} from "@capacitor/core";
import {CapacitorPlugins} from "@app/shared/services/plugins";
@Component({ @Component({
selector: 'app-transfer', selector: 'app-transfer',
...@@ -34,7 +36,7 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI ...@@ -34,7 +36,7 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI
recipient: Account = {address: null, meta: null}; recipient: Account = {address: null, meta: null};
amount: number; amount: number;
fee: number; fee: number;
protected _capacitor: boolean; protected _enableScan: boolean = false;
protected actionSheetOptions: Partial<ActionSheetOptions> = { protected actionSheetOptions: Partial<ActionSheetOptions> = {
cssClass: 'select-account-action-sheet' cssClass: 'select-account-action-sheet'
...@@ -79,22 +81,16 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI ...@@ -79,22 +81,16 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI
protected cd: ChangeDetectorRef protected cd: ChangeDetectorRef
) { ) {
super(injector, {name: 'transfer', loadDueTime: 250}); super(injector, {name: 'transfer', loadDueTime: 250});
} }
ngOnInit() { ngOnInit() {
super.ngOnInit(); super.ngOnInit();
}
ionViewWillLeave() {
// Hide modal when leave page // Hide modal when leave page
this.registerSubscription( this.hideWotModal();
this.router.events
.pipe(filter(
(value, index) => {
console.log(value);
return true;
}
)).subscribe()
)
} }
async ngOnDestroy() { async ngOnDestroy() {
...@@ -103,14 +99,62 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI ...@@ -103,14 +99,62 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI
await this.qrCodeModal?.dismiss(); 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[]>> { protected async ngOnLoad(): Promise<Observable<Account[]>> {
this._enableScan = this.ionicPlatform.is('capacitor') && Capacitor.isPluginAvailable(CapacitorPlugins.BarcodeScanner);
await this.accountService.ready(); await this.accountService.ready();
const subject = new BehaviorSubject<Account[]>(null); const subject = new BehaviorSubject<Account[]>(null);
this.registerSubscription( this.registerSubscription(
this.accountService.watchAll({positiveBalanceFirst: true}) this.accountService.watchAll({positiveBalanceFirst: true})
.pipe(filter(isNotEmptyArray)) .pipe(filter(isNotEmptyArray))
.subscribe((value) => subject.next(value)) .subscribe((value) => {
subject.next(value);
if (this.loaded) this.cd.markForCheck();
})
); );
const accounts = await firstValueFrom(subject); const accounts = await firstValueFrom(subject);
...@@ -133,12 +177,11 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI ...@@ -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.fee = (this.networkService.currency?.fees.tx || 0) / Math.pow(10, this.networkService.currency?.decimals || 0);
this._capacitor = this.ionicPlatform.is('capacitor');
return subject; return subject;
} }
setRecipient(recipient: string|Account) { setRecipient(recipient: string|Account): boolean {
if (typeof recipient === 'object') { if (typeof recipient === 'object') {
this.recipient = recipient; this.recipient = recipient;
} }
...@@ -146,6 +189,7 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI ...@@ -146,6 +189,7 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI
this.recipient = {address: recipient, meta: null}; this.recipient = {address: recipient, meta: null};
} }
this.markForCheck(); this.markForCheck();
return true;
} }
cancel(event?: UIEvent) { cancel(event?: UIEvent) {
...@@ -165,6 +209,10 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI ...@@ -165,6 +209,10 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI
await this.showToast({message: 'INFO.TRANSFER_SENT'}); await this.showToast({message: 'INFO.TRANSFER_SENT'});
this.reset(); this.reset();
if (this.routerOutlet.canGoBack()) {
await this.routerOutlet.pop();
}
} }
catch (err) { catch (err) {
this.setError(err); this.setError(err);
...@@ -173,7 +221,7 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI ...@@ -173,7 +221,7 @@ export class TransferPage extends BasePage<Observable<Account[]>> implements OnI
} }
async scanRecipient(event: UIEvent) { async scanRecipient(event: UIEvent) {
if (!this._capacitor) return; // SKip if (!this._enableScan) return; // SKip
await BarcodeScanner.hideBackground(); // make background of WebView transparent await BarcodeScanner.hideBackground(); // make background of WebView transparent
......
import {KeypairType} from "@polkadot/util-crypto/types"; import {KeypairType} from "@polkadot/util-crypto/types";
import {Subject} from "rxjs";
export interface Account { export interface Account {
address: string; address: string;
...@@ -8,6 +9,7 @@ export interface Account { ...@@ -8,6 +9,7 @@ export interface Account {
meta: AccountMeta; meta: AccountMeta;
data?: AccountData; data?: AccountData;
dataSubject?: Subject<AccountData>;
} }
export interface AccountMeta { export interface AccountMeta {
name: string; name: string;
......
import {Inject, Injectable} from "@angular/core"; import {Inject, Injectable} from "@angular/core";
import {NetworkService} from "../network/network.service"; import {NetworkService} from "../network/network.service";
import {ApiPromise} from "@polkadot/api"; 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 {StartableService} from "@app/shared/services/startable-service.class";
import {AuthData} from "@app/auth/auth.model"; import {AuthData} from "@app/auth/auth.model";
import {keyring} from "@polkadot/ui-keyring"; import {keyring} from "@polkadot/ui-keyring";
...@@ -26,7 +26,7 @@ import { ...@@ -26,7 +26,7 @@ import {
firstValueFrom, firstValueFrom,
from, from,
map, map,
Observable, Observable, Subject,
Subscription, Subscription,
switchMap, switchMap,
timer timer
...@@ -132,7 +132,8 @@ export class AccountService extends StartableService { ...@@ -132,7 +132,8 @@ export class AccountService extends StartableService {
meta: { meta: {
name: ka.meta.name, name: ka.meta.name,
genesisHash: ka.meta.genesisHash genesisHash: ka.meta.genesisHash
} },
dataSubject: new Subject<AccountData>()
} }
}); });
...@@ -492,7 +493,7 @@ export class AccountService extends StartableService { ...@@ -492,7 +493,7 @@ export class AccountService extends StartableService {
try { try {
const now = Date.now(); const now = Date.now();
let loaded = false; let changed = false;
// Load balance (free + reserved) // Load balance (free + reserved)
if (opts.withBalance === true && (isNil(account.data?.free) || opts.reload === true)) { if (opts.withBalance === true && (isNil(account.data?.free) || opts.reload === true)) {
...@@ -502,7 +503,7 @@ export class AccountService extends StartableService { ...@@ -502,7 +503,7 @@ export class AccountService extends StartableService {
...account.data, ...account.data,
...JSON.parse(data.toString()) ...JSON.parse(data.toString())
}; };
loaded = true; changed = true;
} }
// Load TX // Load TX
...@@ -513,7 +514,10 @@ export class AccountService extends StartableService { ...@@ -513,7 +514,10 @@ export class AccountService extends StartableService {
//somethingLoaded = true; //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); console.debug(`${this._logPrefix} Loading ${formatAddress(account.address)} data [OK] in ${Date.now()-now}ms`, account.data);
} }
......
...@@ -4,10 +4,9 @@ ...@@ -4,10 +4,9 @@
<ion-menu-button></ion-menu-button> <ion-menu-button></ion-menu-button>
</ion-buttons> </ion-buttons>
<ion-title translate>ACCOUNT.TITLE</ion-title> <ion-title translate>ACCOUNT.TITLE</ion-title>
<ion-buttons slot="end">
<ng-container *ngIf="$account|async; let accounts"> <ion-select *ngIf="$account|async; let accounts"
<ion-select slot="end" [class.cdk-visually-hidden]="accounts|isEmptyArray"
*ngIf="accounts|isNotEmptyArray"
[(ngModel)]="data" [(ngModel)]="data"
[interface]="mobile ? 'action-sheet' : 'popover'" [interface]="mobile ? 'action-sheet' : 'popover'"
[okText]="'COMMON.BTN_OK'|translate" [okText]="'COMMON.BTN_OK'|translate"
...@@ -16,8 +15,13 @@ ...@@ -16,8 +15,13 @@
[value]="account"> [value]="account">
{{account|accountName}} {{account|accountName}}
</ion-select-option> </ion-select-option>
<ion-select-option (click)="addNewWallet($event)"
[value]="null"
translate>ACCOUNT.WALLET_LIST.BTN_NEW</ion-select-option>
</ion-select> </ion-select>
</ng-container> </ion-buttons>
</ion-toolbar> </ion-toolbar>
<ion-progress-bar type="indeterminate" *ngIf="loading"></ion-progress-bar> <ion-progress-bar type="indeterminate" *ngIf="loading"></ion-progress-bar>
</ion-header> </ion-header>
...@@ -31,9 +35,9 @@ ...@@ -31,9 +35,9 @@
<ion-icon slot="icon-only" name="qr-code"></ion-icon> <ion-icon slot="icon-only" name="qr-code"></ion-icon>
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
<ion-label slot="end" *ngIf="loaded" class="ion-text-end ion-margin-end"> <ion-title slot="end" *ngIf="loaded" class="balance">
<b>{{ balance | amountFormat }}</b> {{ data|balance|amountFormat }}
</ion-label> </ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
......
ion-toolbar {
ion-buttons[slot="end"] {
padding-inline-end: var(--ion-padding);
}
}
...@@ -22,21 +22,11 @@ export class WalletPage extends BasePage<Account> implements OnInit, AfterViewCh ...@@ -22,21 +22,11 @@ export class WalletPage extends BasePage<Account> implements OnInit, AfterViewCh
$account = new BehaviorSubject<Account[]>(null); $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 { get account(): Account {
return this.data; return this.data;
} }
@ViewChild('authModal') authModal: IonModal; @ViewChild('authModal') authModal: IonModal;
@ViewChild('qrCodeModal') qrCodeModal: IonModal; @ViewChild('qrCodeModal') qrCodeModal: IonModal;
constructor( constructor(
...@@ -117,6 +107,10 @@ export class WalletPage extends BasePage<Account> implements OnInit, AfterViewCh ...@@ -117,6 +107,10 @@ export class WalletPage extends BasePage<Account> implements OnInit, AfterViewCh
this.qrCodeModal.present(); this.qrCodeModal.present();
} }
addNewWallet(event: UIEvent) {
}
async openAuthModal(): Promise<Account|null> { async openAuthModal(): Promise<Account|null> {
if (!this.authModal.isOpen) { if (!this.authModal.isOpen) {
await this.authModal.present(); await this.authModal.present();
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment