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
Branches
Tags
No related merge requests found
Showing
with 362 additions and 87 deletions
......@@ -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);
}
}
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> {
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 {
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);
}
}
......@@ -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);
}
}
......@@ -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
]
......
<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";
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 ...');
......
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 {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,11 +21,22 @@ export class StorageService extends StartableService<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();
//console.info(`[storage-service] Started using driver=${storage?.driver}`);
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) {
//if (this._debug) console.debug(`[storage-service] Set ${key} = `, value);
......
......@@ -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,
......
......@@ -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 {}
......@@ -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>
......
......@@ -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
......
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;
......
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);
}
......
......@@ -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>
......
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
$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();
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment