From ab5c8143c9709fc116e481ce2089a0ed3b52fcbe Mon Sep 17 00:00:00 2001 From: Sean Wu Date: Fri, 4 Dec 2020 22:15:08 +0800 Subject: [PATCH 01/11] Rewrite the tests of NotificationService with MockLocalNotificationsPlugin. --- .../services/collector/collector.service.ts | 2 +- .../notification/notification-item.spec.ts | 43 ++++++++++ .../notification/notification-item.ts | 19 +++-- .../notification-testing.service.ts | 52 ------------ .../notification/notification.service.spec.ts | 20 ++++- .../notification/notification.service.ts | 16 ++-- .../capacitor-plugins-testing.module.ts | 16 +++- .../capacitor-plugins.module.ts | 14 +++- .../mock-local-notifications-plugin.ts | 82 +++++++++++++++++++ src/app/shared/shared-testing.module.ts | 7 +- tsconfig.app.json | 5 +- tslint.json | 2 +- 12 files changed, 193 insertions(+), 85 deletions(-) create mode 100644 src/app/services/notification/notification-item.spec.ts delete mode 100644 src/app/services/notification/notification-testing.service.ts create mode 100644 src/app/shared/capacitor-plugins/mock-local-notifications-plugin.ts diff --git a/src/app/services/collector/collector.service.ts b/src/app/services/collector/collector.service.ts index 276275a6f..2fdd815fd 100644 --- a/src/app/services/collector/collector.service.ts +++ b/src/app/services/collector/collector.service.ts @@ -37,7 +37,7 @@ export class CollectorService { const signatures = await this.signTargets({ assets, truth }); const proof = await Proof.from(this.imageStore, assets, truth, signatures); await this.proofRepository.add(proof); - notification.cancel(); + await notification.cancel(); return proof; } diff --git a/src/app/services/notification/notification-item.spec.ts b/src/app/services/notification/notification-item.spec.ts new file mode 100644 index 000000000..890ccc64f --- /dev/null +++ b/src/app/services/notification/notification-item.spec.ts @@ -0,0 +1,43 @@ +import { TestBed } from '@angular/core/testing'; +import { LocalNotificationsPlugin, Plugins } from '@capacitor/core'; +import { TranslocoService } from '@ngneat/transloco'; +import { LOCAL_NOTIFICATIONS_PLUGIN } from '../../shared/capacitor-plugins/capacitor-plugins.module'; +import { SharedTestingModule } from '../../shared/shared-testing.module'; +import { NotificationItem } from './notification-item'; + +const { LocalNotifications } = Plugins; + +describe('NotificationItem', () => { + let item: NotificationItem; + const id = 2; + let localNotificationsPlugin: LocalNotificationsPlugin; + let translocoService: TranslocoService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SharedTestingModule], + providers: [ + { provide: LOCAL_NOTIFICATIONS_PLUGIN, useValue: LocalNotifications }, + ], + }); + localNotificationsPlugin = TestBed.inject(LOCAL_NOTIFICATIONS_PLUGIN); + translocoService = TestBed.inject(TranslocoService); + item = new NotificationItem(2, localNotificationsPlugin, translocoService); + }); + + it('should be created', () => expect(item).toBeTruthy()); + + it('should be able to notify', async () => { + spyOn(console, 'log'); + expect(await item.notify('', '')).toBeInstanceOf(NotificationItem); + }); + + it('should be able to notify error', async () => { + spyOn(console, 'error'); + expect(await item.error(new Error())).toBeInstanceOf(NotificationItem); + }); + + it('should be able to cancel itself', async () => { + expect(await item.cancel()).toBeInstanceOf(NotificationItem); + }); +}); diff --git a/src/app/services/notification/notification-item.ts b/src/app/services/notification/notification-item.ts index 3639df1f0..e5b23d66d 100644 --- a/src/app/services/notification/notification-item.ts +++ b/src/app/services/notification/notification-item.ts @@ -1,19 +1,20 @@ -import { Plugins } from '@capacitor/core'; +import { Inject } from '@angular/core'; +import { LocalNotificationsPlugin } from '@capacitor/core'; import { TranslocoService } from '@ngneat/transloco'; - -const { LocalNotifications } = Plugins; +import { LOCAL_NOTIFICATIONS_PLUGIN } from '../../shared/capacitor-plugins/capacitor-plugins.module'; export class NotificationItem { constructor( private readonly id: number, + @Inject(LOCAL_NOTIFICATIONS_PLUGIN) + private readonly localNotificationsPlugin: LocalNotificationsPlugin, private readonly translocoService: TranslocoService ) {} async notify(title: string, body: string): Promise { - // tslint:disable-next-line: no-console - console.log(`${title}: ${body}`); + console.info(`${title}: ${body}`); - await LocalNotifications.schedule({ + await this.localNotificationsPlugin.schedule({ notifications: [{ title, body, id: this.id }], }); return this; @@ -22,7 +23,7 @@ export class NotificationItem { async error(error: Error): Promise { console.error(error); - await LocalNotifications.schedule({ + await this.localNotificationsPlugin.schedule({ notifications: [ { title: this.translocoService.translate('unknownError'), @@ -35,7 +36,9 @@ export class NotificationItem { } async cancel(): Promise { - LocalNotifications.cancel({ notifications: [{ id: String(this.id) }] }); + this.localNotificationsPlugin.cancel({ + notifications: [{ id: String(this.id) }], + }); return this; } } diff --git a/src/app/services/notification/notification-testing.service.ts b/src/app/services/notification/notification-testing.service.ts deleted file mode 100644 index f699e9340..000000000 --- a/src/app/services/notification/notification-testing.service.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Injectable } from '@angular/core'; -import { NotificationPermissionResponse } from '@capacitor/core'; -import { TranslocoService } from '@ngneat/transloco'; -import { NotificationItem } from './notification-item'; -import { NotificationService } from './notification.service'; - -@Injectable({ - providedIn: 'root', -}) -export class NotificationTestingService extends NotificationService { - constructor(translocoService: TranslocoService) { - super(translocoService); - } - - // tslint:disable-next-line: prefer-function-over-method - async requestPermission(): Promise { - return { granted: true }; - } - - createNotification() { - return new MockNotificationItem( - this.getNewNotificationId(), - this.translocoService - ); - } - - async notify(title: string, body: string) { - return this.createNotification().notify(title, body); - } - - async error(error: Error) { - return this.createNotification().error(error); - } -} - -export class MockNotificationItem extends NotificationItem { - constructor(id: number, translocoService: TranslocoService) { - super(id, translocoService); - } - - async notify(_title: string, _body: string): Promise { - return this; - } - - async error(_error: Error): Promise { - return this; - } - - async cancel(): Promise { - return this; - } -} diff --git a/src/app/services/notification/notification.service.spec.ts b/src/app/services/notification/notification.service.spec.ts index c9c853ad4..e603d584d 100644 --- a/src/app/services/notification/notification.service.spec.ts +++ b/src/app/services/notification/notification.service.spec.ts @@ -1,13 +1,21 @@ import { TestBed } from '@angular/core/testing'; +import { Plugins } from '@capacitor/core'; +import { LOCAL_NOTIFICATIONS_PLUGIN } from '../../shared/capacitor-plugins/capacitor-plugins.module'; import { SharedTestingModule } from '../../shared/shared-testing.module'; +import { NotificationItem } from './notification-item'; import { NotificationService } from './notification.service'; +const { LocalNotifications } = Plugins; + describe('NotificationService', () => { let service: NotificationService; beforeEach(() => { TestBed.configureTestingModule({ imports: [SharedTestingModule], + providers: [ + { provide: LOCAL_NOTIFICATIONS_PLUGIN, useValue: LocalNotifications }, + ], }); service = TestBed.inject(NotificationService); }); @@ -15,9 +23,15 @@ describe('NotificationService', () => { it('should be created', () => expect(service).toBeTruthy()); it('should create notification', () => - expect(service.createNotification()).toBeTruthy()); + expect(service.createNotification()).toBeInstanceOf(NotificationItem)); - it('should notify', () => expect(service.notify('', '')).toBeTruthy()); + it('should be able to notify', async () => { + spyOn(console, 'log'); + expect(await service.notify('', '')).toBeInstanceOf(NotificationItem); + }); - it('should error', () => expect(service.error(new Error())).toBeTruthy()); + it('should be able to notify error', async () => { + spyOn(console, 'error'); + expect(await service.error(new Error())).toBeInstanceOf(NotificationItem); + }); }); diff --git a/src/app/services/notification/notification.service.ts b/src/app/services/notification/notification.service.ts index dd9011a8e..9a43d4483 100644 --- a/src/app/services/notification/notification.service.ts +++ b/src/app/services/notification/notification.service.ts @@ -1,26 +1,30 @@ -import { Injectable } from '@angular/core'; -import { Plugins } from '@capacitor/core'; +import { Inject, Injectable } from '@angular/core'; +import { LocalNotificationsPlugin } from '@capacitor/core'; import { TranslocoService } from '@ngneat/transloco'; +import { LOCAL_NOTIFICATIONS_PLUGIN } from '../../shared/capacitor-plugins/capacitor-plugins.module'; import { NotificationItem } from './notification-item'; -const { LocalNotifications } = Plugins; - @Injectable({ providedIn: 'root', }) export class NotificationService { protected currentId = 1; - constructor(protected readonly translocoService: TranslocoService) {} + constructor( + @Inject(LOCAL_NOTIFICATIONS_PLUGIN) + private readonly localNotificationsPlugin: LocalNotificationsPlugin, + private readonly translocoService: TranslocoService + ) {} // tslint:disable-next-line: prefer-function-over-method async requestPermission() { - return LocalNotifications.requestPermission(); + return this.localNotificationsPlugin.requestPermission(); } createNotification() { return new NotificationItem( this.getNewNotificationId(), + this.localNotificationsPlugin, this.translocoService ); } diff --git a/src/app/shared/capacitor-plugins/capacitor-plugins-testing.module.ts b/src/app/shared/capacitor-plugins/capacitor-plugins-testing.module.ts index fab881f9c..45c6aa4f7 100644 --- a/src/app/shared/capacitor-plugins/capacitor-plugins-testing.module.ts +++ b/src/app/shared/capacitor-plugins/capacitor-plugins-testing.module.ts @@ -2,14 +2,26 @@ import { NgModule } from '@angular/core'; import { FILESYSTEM_PLUGIN, GEOLOCATION_PLUGIN, + LOCAL_NOTIFICATIONS_PLUGIN, } from './capacitor-plugins.module'; import { MockFilesystemPlugin } from './mock-filesystem-plugin'; import { MockGeolocationPlugin } from './mock-geolocation-plugin'; +import { MockLocalNotificationsPlugin } from './mock-local-notifications-plugin'; @NgModule({ providers: [ - { provide: GEOLOCATION_PLUGIN, useClass: MockGeolocationPlugin }, - { provide: FILESYSTEM_PLUGIN, useClass: MockFilesystemPlugin }, + { + provide: GEOLOCATION_PLUGIN, + useClass: MockGeolocationPlugin, + }, + { + provide: FILESYSTEM_PLUGIN, + useClass: MockFilesystemPlugin, + }, + { + provide: LOCAL_NOTIFICATIONS_PLUGIN, + useClass: MockLocalNotificationsPlugin, + }, ], }) export class CapacitorPluginsTestingModule {} diff --git a/src/app/shared/capacitor-plugins/capacitor-plugins.module.ts b/src/app/shared/capacitor-plugins/capacitor-plugins.module.ts index 0782b1a72..9d1d56e14 100644 --- a/src/app/shared/capacitor-plugins/capacitor-plugins.module.ts +++ b/src/app/shared/capacitor-plugins/capacitor-plugins.module.ts @@ -1,20 +1,28 @@ import { InjectionToken, NgModule } from '@angular/core'; -import { FilesystemPlugin, GeolocationPlugin, Plugins } from '@capacitor/core'; +import { + FilesystemPlugin, + GeolocationPlugin, + LocalNotificationsPlugin, + Plugins, +} from '@capacitor/core'; -const { Filesystem, Geolocation } = Plugins; +const { Filesystem, Geolocation, LocalNotifications } = Plugins; export const GEOLOCATION_PLUGIN = new InjectionToken( 'GEOLOCATION_PLUGIN' ); - export const FILESYSTEM_PLUGIN = new InjectionToken( 'FILESYSTEM_PLUGIN' ); +export const LOCAL_NOTIFICATIONS_PLUGIN = new InjectionToken( + 'LOCAL_NOTIFICATIONS_PLUGIN' +); @NgModule({ providers: [ { provide: GEOLOCATION_PLUGIN, useValue: Geolocation }, { provide: FILESYSTEM_PLUGIN, useValue: Filesystem }, + { provide: LOCAL_NOTIFICATIONS_PLUGIN, useValue: LocalNotifications }, ], }) export class CapacitorPluginsModule {} diff --git a/src/app/shared/capacitor-plugins/mock-local-notifications-plugin.ts b/src/app/shared/capacitor-plugins/mock-local-notifications-plugin.ts new file mode 100644 index 000000000..842a78477 --- /dev/null +++ b/src/app/shared/capacitor-plugins/mock-local-notifications-plugin.ts @@ -0,0 +1,82 @@ +// tslint:disable: prefer-function-over-method no-async-without-await + +import { + LocalNotification, + LocalNotificationActionPerformed, + LocalNotificationActionType, + LocalNotificationEnabledResult, + LocalNotificationPendingList, + LocalNotificationScheduleResult, + LocalNotificationsPlugin, + NotificationChannel, + NotificationChannelList, + NotificationPermissionResponse, + PluginListenerHandle, +} from '@capacitor/core'; + +export class MockLocalNotificationsPlugin implements LocalNotificationsPlugin { + async schedule(options: { + notifications: LocalNotification[]; + }): Promise { + return { + notifications: options.notifications.map(notification => ({ + id: `${notification.id}`, + })), + }; + } + + async getPending(): Promise { + throw new Error('Method not implemented.'); + } + + async registerActionTypes(options: { + types: LocalNotificationActionType[]; + }): Promise { + throw new Error('Method not implemented.'); + } + + async cancel(pending: LocalNotificationPendingList): Promise { + return; + } + + async areEnabled(): Promise { + throw new Error('Method not implemented.'); + } + + async createChannel(channel: NotificationChannel): Promise { + throw new Error('Method not implemented.'); + } + + async deleteChannel(channel: NotificationChannel): Promise { + throw new Error('Method not implemented.'); + } + + async listChannels(): Promise { + throw new Error('Method not implemented.'); + } + + async requestPermission(): Promise { + return { granted: true }; + } + + addListener( + eventName: 'localNotificationReceived', + listenerFunc: (notification: LocalNotification) => void + ): PluginListenerHandle; + addListener( + eventName: 'localNotificationActionPerformed', + listenerFunc: (notificationAction: LocalNotificationActionPerformed) => void + ): PluginListenerHandle; + addListener( + eventName: 'localNotificationReceived' | 'localNotificationActionPerformed', + listenerFunc: + | ((notification: LocalNotification) => void) + | ((notificationAction: LocalNotificationActionPerformed) => void) + ): PluginListenerHandle { + throw new Error('Method not implemented.'); + } + + removeAllListeners(): void { + throw new Error('Method not implemented.'); + } +} diff --git a/src/app/shared/shared-testing.module.ts b/src/app/shared/shared-testing.module.ts index 1c6820e7c..ccf44f859 100644 --- a/src/app/shared/shared-testing.module.ts +++ b/src/app/shared/shared-testing.module.ts @@ -3,8 +3,6 @@ import { NgModule } from '@angular/core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; import { IonicModule } from '@ionic/angular'; -import { NotificationTestingService } from '../services/notification/notification-testing.service'; -import { NotificationService } from '../services/notification/notification.service'; import { MemoryPreferences } from '../services/preference-manager/preferences/memory-preferences/memory-preferences'; import { PREFERENCES_IMPL } from '../services/preference-manager/preferences/preferences'; import { getTranslocoTestingModule } from '../services/transloco/transloco-testing.module'; @@ -23,10 +21,7 @@ import { SharedModule } from './shared.module'; MaterialTestingModule, CapacitorPluginsTestingModule, ], - providers: [ - { provide: PREFERENCES_IMPL, useValue: MemoryPreferences }, - { provide: NotificationService, useClass: NotificationTestingService }, - ], + providers: [{ provide: PREFERENCES_IMPL, useValue: MemoryPreferences }], exports: [MaterialTestingModule], }) export class SharedTestingModule {} diff --git a/tsconfig.app.json b/tsconfig.app.json index 6020eb870..08a710cc4 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -9,9 +9,8 @@ "exclude": [ "src/**/*.spec.ts", "src/**/*-testing.*.ts", + "src/**/mock-*.ts", "src/test.ts", - "src/environments/environment.prod.ts", - "src/**/memory-table.ts", - "src/**/memory-preferences.ts" + "src/environments/environment.prod.ts" ] } diff --git a/tslint.json b/tslint.json index ffe65cdee..016ce6ab6 100644 --- a/tslint.json +++ b/tslint.json @@ -27,7 +27,7 @@ "curly": true, "import-blacklist": [true, "rxjs/Rx"], "no-async-without-await": true, - "no-console": [true, "log", "info", "debug"], + "no-console": [true, "log", "debug"], "no-duplicate-switch-case": true, "no-for-in-array": true, "no-inferred-empty-object-type": true, From bd43cae13d0cbde7bde5f1ceba057dbd1cf3f53c Mon Sep 17 00:00:00 2001 From: Sean Wu Date: Fri, 4 Dec 2020 22:38:01 +0800 Subject: [PATCH 02/11] Rewrite tests for PreferenceManager service by mocking Capacitor Storage plugin. --- .../notification/notification-item.spec.ts | 10 +- .../preference-manager.service.ts | 11 +- .../capacitor-storage-preferences.spec.ts | 15 +- .../capacitor-storage-preferences.ts | 20 +- .../memory-preferences.spec.ts | 203 ------------------ .../memory-preferences/memory-preferences.ts | 81 ------- .../preferences/preferences.ts | 5 - .../capacitor-plugins-testing.module.ts | 6 + .../capacitor-plugins.module.ts | 7 +- .../capacitor-plugins/mock-storage-plugin.ts | 33 +++ src/app/shared/shared-testing.module.ts | 3 - src/app/shared/shared.module.ts | 5 - 12 files changed, 83 insertions(+), 316 deletions(-) delete mode 100644 src/app/services/preference-manager/preferences/memory-preferences/memory-preferences.spec.ts delete mode 100644 src/app/services/preference-manager/preferences/memory-preferences/memory-preferences.ts create mode 100644 src/app/shared/capacitor-plugins/mock-storage-plugin.ts diff --git a/src/app/services/notification/notification-item.spec.ts b/src/app/services/notification/notification-item.spec.ts index 890ccc64f..1420a6420 100644 --- a/src/app/services/notification/notification-item.spec.ts +++ b/src/app/services/notification/notification-item.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@angular/core/testing'; -import { LocalNotificationsPlugin, Plugins } from '@capacitor/core'; +import { Plugins } from '@capacitor/core'; import { TranslocoService } from '@ngneat/transloco'; import { LOCAL_NOTIFICATIONS_PLUGIN } from '../../shared/capacitor-plugins/capacitor-plugins.module'; import { SharedTestingModule } from '../../shared/shared-testing.module'; @@ -10,8 +10,6 @@ const { LocalNotifications } = Plugins; describe('NotificationItem', () => { let item: NotificationItem; const id = 2; - let localNotificationsPlugin: LocalNotificationsPlugin; - let translocoService: TranslocoService; beforeEach(() => { TestBed.configureTestingModule({ @@ -20,9 +18,9 @@ describe('NotificationItem', () => { { provide: LOCAL_NOTIFICATIONS_PLUGIN, useValue: LocalNotifications }, ], }); - localNotificationsPlugin = TestBed.inject(LOCAL_NOTIFICATIONS_PLUGIN); - translocoService = TestBed.inject(TranslocoService); - item = new NotificationItem(2, localNotificationsPlugin, translocoService); + const localNotificationsPlugin = TestBed.inject(LOCAL_NOTIFICATIONS_PLUGIN); + const translocoService = TestBed.inject(TranslocoService); + item = new NotificationItem(id, localNotificationsPlugin, translocoService); }); it('should be created', () => expect(item).toBeTruthy()); diff --git a/src/app/services/preference-manager/preference-manager.service.ts b/src/app/services/preference-manager/preference-manager.service.ts index c5732b04c..2b3e3dcf6 100644 --- a/src/app/services/preference-manager/preference-manager.service.ts +++ b/src/app/services/preference-manager/preference-manager.service.ts @@ -1,5 +1,8 @@ -import { Inject, Injectable, Type } from '@angular/core'; -import { Preferences, PREFERENCES_IMPL } from './preferences/preferences'; +import { Inject, Injectable } from '@angular/core'; +import { StoragePlugin } from '@capacitor/core'; +import { STORAGE_PLUGIN } from '../../shared/capacitor-plugins/capacitor-plugins.module'; +import { CapacitorStoragePreferences } from './preferences/capacitor-storage-preferences/capacitor-storage-preferences'; +import { Preferences } from './preferences/preferences'; @Injectable({ providedIn: 'root', @@ -8,7 +11,7 @@ export class PreferenceManager { private readonly preferencesMap = new Map(); constructor( - @Inject(PREFERENCES_IMPL) private readonly PreferenceImpl: Type + @Inject(STORAGE_PLUGIN) private readonly storagePlugin: StoragePlugin ) {} getPreferences(id: string) { @@ -20,7 +23,7 @@ export class PreferenceManager { } private createPreferences(id: string) { - const created = new this.PreferenceImpl(id); + const created = new CapacitorStoragePreferences(id, this.storagePlugin); this.preferencesMap.set(id, created); return created; } diff --git a/src/app/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.spec.ts b/src/app/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.spec.ts index 3ae1ce06f..a37733568 100644 --- a/src/app/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.spec.ts +++ b/src/app/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.spec.ts @@ -1,13 +1,26 @@ +import { TestBed } from '@angular/core/testing'; +import { Plugins } from '@capacitor/core'; import { zip } from 'rxjs'; import { first } from 'rxjs/operators'; +import { STORAGE_PLUGIN } from '../../../../shared/capacitor-plugins/capacitor-plugins.module'; +import { SharedTestingModule } from '../../../../shared/shared-testing.module'; import { Preferences } from '../preferences'; import { CapacitorStoragePreferences } from './capacitor-storage-preferences'; +const { Storage } = Plugins; + describe('CapacitorStoragePreferences', () => { let preferences: Preferences; const id = 'id'; - beforeEach(() => (preferences = new CapacitorStoragePreferences(id))); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SharedTestingModule], + providers: [{ provide: STORAGE_PLUGIN, useValue: Storage }], + }); + const storagePlugin = TestBed.inject(STORAGE_PLUGIN); + preferences = new CapacitorStoragePreferences(id, storagePlugin); + }); it('should be created', () => expect(preferences).toBeTruthy()); diff --git a/src/app/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.ts b/src/app/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.ts index 755de4fd8..e785c817a 100644 --- a/src/app/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.ts +++ b/src/app/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.ts @@ -1,16 +1,17 @@ -import { Plugins } from '@capacitor/core'; +import { StoragePlugin } from '@capacitor/core'; import { Mutex } from 'async-mutex'; import { BehaviorSubject, defer, Observable } from 'rxjs'; import { concatMap } from 'rxjs/operators'; import { Preferences } from '../preferences'; -const { Storage } = Plugins; - export class CapacitorStoragePreferences implements Preferences { private readonly subjects = new Map>(); private readonly mutex = new Mutex(); - constructor(readonly id: string) {} + constructor( + readonly id: string, + private readonly storagePlugin: StoragePlugin + ) {} getBoolean$(key: string, defaultValue = false) { return this.get$(key, defaultValue); @@ -65,7 +66,9 @@ export class CapacitorStoragePreferences implements Preferences { key: string, defaultValue: boolean | number | string ) { - const rawValue = (await Storage.get({ key: this.toStorageKey(key) })).value; + const rawValue = ( + await this.storagePlugin.get({ key: this.toStorageKey(key) }) + ).value; if (!rawValue) { return defaultValue; } @@ -104,12 +107,15 @@ export class CapacitorStoragePreferences implements Preferences { } private async storeValue(key: string, value: boolean | number | string) { - return Storage.set({ key: this.toStorageKey(key), value: `${value}` }); + return this.storagePlugin.set({ + key: this.toStorageKey(key), + value: `${value}`, + }); } async clear() { for (const key of this.subjects.keys()) { - await Storage.remove({ key: this.toStorageKey(key) }); + await this.storagePlugin.remove({ key: this.toStorageKey(key) }); } this.subjects.clear(); return this; diff --git a/src/app/services/preference-manager/preferences/memory-preferences/memory-preferences.spec.ts b/src/app/services/preference-manager/preferences/memory-preferences/memory-preferences.spec.ts deleted file mode 100644 index c4aeb7c51..000000000 --- a/src/app/services/preference-manager/preferences/memory-preferences/memory-preferences.spec.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { zip } from 'rxjs'; -import { first } from 'rxjs/operators'; -import { Preferences } from '../preferences'; -import { MemoryPreferences } from './memory-preferences'; - -describe('MemoryPreferences', () => { - let preferences: Preferences; - const id = 'id'; - - beforeEach(() => (preferences = new MemoryPreferences(id))); - - it('should be created', () => expect(preferences).toBeTruthy()); - - it('should get the same ID set in constructor', () => - expect(preferences.id).toEqual(id)); - - it('should get the default boolean Observable if not set previously', done => { - const notExistedKey = 'unknown'; - const defaultValue = true; - preferences.getBoolean$(notExistedKey, defaultValue).subscribe(result => { - expect(result).toEqual(defaultValue); - done(); - }); - }); - - it('should get the default number Observable if not set previously', done => { - const notExistedKey = 'unknown'; - const defaultValue = 999; - preferences.getNumber$(notExistedKey, defaultValue).subscribe(result => { - expect(result).toEqual(defaultValue); - done(); - }); - }); - - it('should get the default string Observable if not set previously', done => { - const notExistedKey = 'unknown'; - const defaultValue = 'default'; - preferences.getString$(notExistedKey, defaultValue).subscribe(result => { - expect(result).toEqual(defaultValue); - done(); - }); - }); - - it('should get the default boolean value if not set previously', async () => { - const notExistedKey = 'unknown'; - const defaultValue = true; - const bool = await preferences.getBoolean(notExistedKey, defaultValue); - expect(bool).toEqual(defaultValue); - }); - - it('should get the default number value if not set previously', async () => { - const notExistedKey = 'unknown'; - const defaultValue = 999; - const num = await preferences.getNumber(notExistedKey, defaultValue); - expect(num).toEqual(defaultValue); - }); - - it('should get the default string value if not set previously', async () => { - const notExistedKey = 'unknown'; - const defaultValue = 'default'; - const str = await preferences.getString(notExistedKey, defaultValue); - expect(str).toEqual(defaultValue); - }); - - it('should get the same boolean Observable set previously', async done => { - const key = 'key'; - const value = true; - await preferences.setBoolean(key, value); - - preferences.getBoolean$(key).subscribe(result => { - expect(result).toEqual(value); - done(); - }); - }); - - it('should get the same number Observable set previously', async done => { - const key = 'key'; - const value = 99; - await preferences.setNumber(key, value); - - preferences.getNumber$(key).subscribe(result => { - expect(result).toEqual(value); - done(); - }); - }); - - it('should get the same string Observable set previously', async done => { - const key = 'key'; - const value = 'value'; - await preferences.setString(key, value); - - preferences.getString$(key).subscribe(result => { - expect(result).toEqual(value); - done(); - }); - }); - - it('should get the same boolean value set previously', async () => { - const key = 'key'; - const value = true; - await preferences.setBoolean(key, value); - - const result = await preferences.getBoolean(key); - expect(result).toEqual(value); - }); - - it('should get the same number value set previously', async () => { - const key = 'key'; - const value = 99; - await preferences.setNumber(key, value); - - const result = await preferences.getNumber(key); - expect(result).toEqual(value); - }); - - it('should get the same string value set previously', async () => { - const key = 'key'; - const value = 'value'; - await preferences.setString(key, value); - - const result = await preferences.getString(key); - expect(result).toEqual(value); - }); - - it('should set boolean atomically', async done => { - const key = 'key'; - const operationCount = 100; - const lastBoolean = true; - const booleans: boolean[] = [ - ...Array(operationCount - 1).fill(false), - lastBoolean, - ]; - - await Promise.all(booleans.map(bool => preferences.setBoolean(key, bool))); - - preferences.getBoolean$(key).subscribe(result => { - expect(result).toEqual(lastBoolean); - done(); - }); - }); - - it('should set number atomically', async done => { - const key = 'key'; - const operationCount = 100; - const lastNumber = -20; - const numbers: number[] = [...Array(operationCount - 1).keys(), lastNumber]; - - await Promise.all(numbers.map(n => preferences.setNumber(key, n))); - - preferences.getNumber$(key).subscribe(result => { - expect(result).toEqual(lastNumber); - done(); - }); - }); - - it('should set string atomically', async done => { - const key = 'key'; - const operationCount = 1000; - const lastString = 'last'; - const strings: string[] = [ - ...`${Array(operationCount - 1).keys()}`, - lastString, - ]; - - await Promise.all(strings.map(str => preferences.setString(key, str))); - - preferences.getString$(key).subscribe(result => { - expect(result).toEqual(lastString); - done(); - }); - }); - - it('should remove all values after clear', async done => { - const booleanKey = 'booleanKey'; - const booleanValue = true; - const defaultBooleanValue = false; - const numberKey = 'numberKey'; - const numberValue = 77; - const defaultNumberValue = 55; - const stringKey = 'stringKey'; - const stringValue = 'stringValue'; - const defaultStringValue = 'defaultStringValue'; - - await preferences.setBoolean(booleanKey, booleanValue); - await preferences.setNumber(numberKey, numberValue); - await preferences.setString(stringKey, stringValue); - - await preferences.clear(); - - zip( - preferences.getBoolean$(booleanKey, defaultBooleanValue), - preferences.getNumber$(numberKey, defaultNumberValue), - preferences.getString$(stringKey, defaultStringValue) - ) - .pipe(first()) - .subscribe(([booleanResult, numberResult, stringResult]) => { - expect(booleanResult).toEqual(defaultBooleanValue); - expect(numberResult).toEqual(defaultNumberValue); - expect(stringResult).toEqual(defaultStringValue); - done(); - }); - }); -}); diff --git a/src/app/services/preference-manager/preferences/memory-preferences/memory-preferences.ts b/src/app/services/preference-manager/preferences/memory-preferences/memory-preferences.ts deleted file mode 100644 index 6767a3deb..000000000 --- a/src/app/services/preference-manager/preferences/memory-preferences/memory-preferences.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { BehaviorSubject, Observable } from 'rxjs'; -import { Preferences } from '../preferences'; - -export class MemoryPreferences implements Preferences { - private readonly subjects = new Map>(); - - constructor(readonly id: string) {} - - getBoolean$(key: string, defaultValue = false) { - return this.get$(key, defaultValue); - } - - getNumber$(key: string, defaultValue = 0) { - return this.get$(key, defaultValue); - } - - getString$(key: string, defaultValue = '') { - return this.get$(key, defaultValue); - } - - get$(key: string, defaultValue: T) { - this.initializeValue(key, defaultValue); - // tslint:disable-next-line: no-non-null-assertion - return this.subjects.get(key)!.asObservable() as Observable; - } - - async getBoolean(key: string, defaultValue = true) { - return this.get(key, defaultValue); - } - async getNumber(key: string, defaultValue = 0) { - return this.get(key, defaultValue); - } - async getString(key: string, defaultValue = '') { - return this.get(key, defaultValue); - } - - private get( - key: string, - defaultValue: T - ) { - this.initializeValue(key, defaultValue); - // tslint:disable-next-line: no-non-null-assertion - return this.subjects.get(key)!.value as T; - } - - private initializeValue( - key: string, - defaultValue: boolean | number | string - ) { - if (!this.subjects.has(key)) { - this.subjects.set(key, new BehaviorSubject(defaultValue)); - } - } - - async setBoolean(key: string, value: boolean) { - return this.set(key, value); - } - - async setNumber(key: string, value: number) { - return this.set(key, value); - } - - async setString(key: string, value: string) { - return this.set(key, value); - } - - async set(key: string, value: T) { - if (!this.subjects.has(key)) { - this.subjects.set(key, new BehaviorSubject(value)); - } - // tslint:disable-next-line: no-non-null-assertion - const subject$ = this.subjects.get(key)! as BehaviorSubject; - subject$.next(value); - return value; - } - - async clear() { - this.subjects.clear(); - return this; - } -} diff --git a/src/app/services/preference-manager/preferences/preferences.ts b/src/app/services/preference-manager/preferences/preferences.ts index 4355f5d0a..b9de07d96 100644 --- a/src/app/services/preference-manager/preferences/preferences.ts +++ b/src/app/services/preference-manager/preferences/preferences.ts @@ -1,4 +1,3 @@ -import { InjectionToken, Type } from '@angular/core'; import { Observable } from 'rxjs'; export interface Preferences { @@ -14,7 +13,3 @@ export interface Preferences { setString(key: string, value: string): Promise; clear(): Promise; } - -export const PREFERENCES_IMPL = new InjectionToken>( - 'PREFERENCES_IMPL' -); diff --git a/src/app/shared/capacitor-plugins/capacitor-plugins-testing.module.ts b/src/app/shared/capacitor-plugins/capacitor-plugins-testing.module.ts index 45c6aa4f7..40e37e915 100644 --- a/src/app/shared/capacitor-plugins/capacitor-plugins-testing.module.ts +++ b/src/app/shared/capacitor-plugins/capacitor-plugins-testing.module.ts @@ -3,10 +3,12 @@ import { FILESYSTEM_PLUGIN, GEOLOCATION_PLUGIN, LOCAL_NOTIFICATIONS_PLUGIN, + STORAGE_PLUGIN, } from './capacitor-plugins.module'; import { MockFilesystemPlugin } from './mock-filesystem-plugin'; import { MockGeolocationPlugin } from './mock-geolocation-plugin'; import { MockLocalNotificationsPlugin } from './mock-local-notifications-plugin'; +import { MockStoragePlugin } from './mock-storage-plugin'; @NgModule({ providers: [ @@ -22,6 +24,10 @@ import { MockLocalNotificationsPlugin } from './mock-local-notifications-plugin' provide: LOCAL_NOTIFICATIONS_PLUGIN, useClass: MockLocalNotificationsPlugin, }, + { + provide: STORAGE_PLUGIN, + useClass: MockStoragePlugin, + }, ], }) export class CapacitorPluginsTestingModule {} diff --git a/src/app/shared/capacitor-plugins/capacitor-plugins.module.ts b/src/app/shared/capacitor-plugins/capacitor-plugins.module.ts index 9d1d56e14..ca6f479a1 100644 --- a/src/app/shared/capacitor-plugins/capacitor-plugins.module.ts +++ b/src/app/shared/capacitor-plugins/capacitor-plugins.module.ts @@ -4,9 +4,10 @@ import { GeolocationPlugin, LocalNotificationsPlugin, Plugins, + StoragePlugin, } from '@capacitor/core'; -const { Filesystem, Geolocation, LocalNotifications } = Plugins; +const { Filesystem, Geolocation, LocalNotifications, Storage } = Plugins; export const GEOLOCATION_PLUGIN = new InjectionToken( 'GEOLOCATION_PLUGIN' @@ -17,12 +18,16 @@ export const FILESYSTEM_PLUGIN = new InjectionToken( export const LOCAL_NOTIFICATIONS_PLUGIN = new InjectionToken( 'LOCAL_NOTIFICATIONS_PLUGIN' ); +export const STORAGE_PLUGIN = new InjectionToken( + 'STORAGE_PLUGIN' +); @NgModule({ providers: [ { provide: GEOLOCATION_PLUGIN, useValue: Geolocation }, { provide: FILESYSTEM_PLUGIN, useValue: Filesystem }, { provide: LOCAL_NOTIFICATIONS_PLUGIN, useValue: LocalNotifications }, + { provide: STORAGE_PLUGIN, useValue: Storage }, ], }) export class CapacitorPluginsModule {} diff --git a/src/app/shared/capacitor-plugins/mock-storage-plugin.ts b/src/app/shared/capacitor-plugins/mock-storage-plugin.ts new file mode 100644 index 000000000..d05dac4a1 --- /dev/null +++ b/src/app/shared/capacitor-plugins/mock-storage-plugin.ts @@ -0,0 +1,33 @@ +// tslint:disable: prefer-function-over-method no-async-without-await no-non-null-assertion +import { PluginListenerHandle, StoragePlugin } from '@capacitor/core'; + +export class MockStoragePlugin implements StoragePlugin { + private readonly map = new Map(); + + async get(options: { key: string }): Promise<{ value: string }> { + return { value: this.map.get(options.key)! }; + } + + async set(options: { key: string; value: string }): Promise { + this.map.set(options.key, options.value); + } + + async remove(options: { key: string }): Promise { + this.map.delete(options.key); + } + + async clear(): Promise { + throw new Error('Method not implemented.'); + } + + async keys(): Promise<{ keys: string[] }> { + throw new Error('Method not implemented.'); + } + + addListener( + eventName: string, + listenerFunc: () => any + ): PluginListenerHandle { + throw new Error('Method not implemented.'); + } +} diff --git a/src/app/shared/shared-testing.module.ts b/src/app/shared/shared-testing.module.ts index ccf44f859..c5f7dc31e 100644 --- a/src/app/shared/shared-testing.module.ts +++ b/src/app/shared/shared-testing.module.ts @@ -3,8 +3,6 @@ import { NgModule } from '@angular/core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; import { IonicModule } from '@ionic/angular'; -import { MemoryPreferences } from '../services/preference-manager/preferences/memory-preferences/memory-preferences'; -import { PREFERENCES_IMPL } from '../services/preference-manager/preferences/preferences'; import { getTranslocoTestingModule } from '../services/transloco/transloco-testing.module'; import { CapacitorPluginsTestingModule } from './capacitor-plugins/capacitor-plugins-testing.module'; import { MaterialTestingModule } from './material/material-testing.module'; @@ -21,7 +19,6 @@ import { SharedModule } from './shared.module'; MaterialTestingModule, CapacitorPluginsTestingModule, ], - providers: [{ provide: PREFERENCES_IMPL, useValue: MemoryPreferences }], exports: [MaterialTestingModule], }) export class SharedTestingModule {} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 1fb767d4b..8766f961c 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -4,8 +4,6 @@ import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { IonicModule } from '@ionic/angular'; import { TranslocoModule } from '@ngneat/transloco'; -import { CapacitorStoragePreferences } from '../services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences'; -import { PREFERENCES_IMPL } from '../services/preference-manager/preferences/preferences'; import { CapacitorPluginsModule } from './capacitor-plugins/capacitor-plugins.module'; import { MaterialModule } from './material/material.module'; @@ -20,9 +18,6 @@ import { MaterialModule } from './material/material.module'; MaterialModule, CapacitorPluginsModule, ], - providers: [ - { provide: PREFERENCES_IMPL, useValue: CapacitorStoragePreferences }, - ], exports: [ CommonModule, IonicModule, From e926c016488c7f436d8e88159cdb8b33a20e3a6a Mon Sep 17 00:00:00 2001 From: Sean Wu Date: Fri, 4 Dec 2020 23:07:51 +0800 Subject: [PATCH 03/11] Rewrite the interface of ConfirmAlert with tests. --- src/app/pages/home/asset/asset.page.ts | 51 ++++++++++--------- .../sending-post-capture.page.ts | 18 +++---- .../confirm-alert.service.spec.ts | 7 +-- .../confirm-alert/confirm-alert.service.ts | 42 +++++++-------- src/assets/i18n/en-us.json | 1 - src/assets/i18n/zh-tw.json | 1 - 6 files changed, 58 insertions(+), 62 deletions(-) diff --git a/src/app/pages/home/asset/asset.page.ts b/src/app/pages/home/asset/asset.page.ts index 2dd305327..9e1b1d9ac 100644 --- a/src/app/pages/home/asset/asset.page.ts +++ b/src/app/pages/home/asset/asset.page.ts @@ -1,13 +1,19 @@ import { Component } from '@angular/core'; import { MatBottomSheet } from '@angular/material/bottom-sheet'; import { MatDialog } from '@angular/material/dialog'; -import { MatSnackBar } from '@angular/material/snack-bar'; import { ActivatedRoute, Router } from '@angular/router'; import { Plugins } from '@capacitor/core'; import { TranslocoService } from '@ngneat/transloco'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { combineLatest, defer, forkJoin, zip } from 'rxjs'; -import { concatMap, map, switchMap, switchMapTo, tap } from 'rxjs/operators'; +import { + concatMap, + first, + map, + switchMap, + switchMapTo, + tap, +} from 'rxjs/operators'; import { BlockingActionService } from '../../../services/blocking-action/blocking-action.service'; import { ConfirmAlert } from '../../../services/confirm-alert/confirm-alert.service'; import { AssetRepository } from '../../../services/publisher/numbers-storage/repositories/asset/asset-repository.service'; @@ -79,7 +85,6 @@ export class AssetPage { private readonly assetRepository: AssetRepository, private readonly proofRepository: ProofRepository, private readonly blockingActionService: BlockingActionService, - private readonly snackBar: MatSnackBar, private readonly dialog: MatDialog, private readonly bottomSheet: MatBottomSheet ) {} @@ -115,31 +120,27 @@ export class AssetPage { .subscribe(); } - private remove() { - const onConfirm = () => - this.blockingActionService - .run$( - zip(this.asset$, this.capture$).pipe( - concatMap(([asset, capture]) => - forkJoin([ - this.assetRepository.remove$(asset), - this.proofRepository.remove( - // tslint:disable-next-line: no-non-null-assertion - capture.proofWithOld!.proof - ), - ]) - ), - switchMapTo(defer(() => this.router.navigate(['..']))) + private async remove() { + const action$ = zip(this.asset$, this.capture$).pipe( + first(), + concatMap(([asset, capture]) => + forkJoin([ + this.assetRepository.remove$(asset), + this.proofRepository.remove( + // tslint:disable-next-line: no-non-null-assertion + capture.proofWithOld!.proof ), - { message: this.translocoService.translate('processing') } - ) + ]) + ), + switchMapTo(defer(() => this.router.navigate(['..']))) + ); + const result = await this.confirmAlert.present(); + if (result) { + this.blockingActionService + .run$(action$) .pipe(untilDestroyed(this)) .subscribe(); - - return this.confirmAlert - .present$(onConfirm) - .pipe(untilDestroyed(this)) - .subscribe(); + } } openDashboardLink() { diff --git a/src/app/pages/home/asset/sending-post-capture/sending-post-capture.page.ts b/src/app/pages/home/asset/sending-post-capture/sending-post-capture.page.ts index 8fb77c208..004b855e6 100644 --- a/src/app/pages/home/asset/sending-post-capture/sending-post-capture.page.ts +++ b/src/app/pages/home/asset/sending-post-capture/sending-post-capture.page.ts @@ -76,7 +76,7 @@ export class SendingPostCapturePage { this.isPreview = true; } - send(captionText: string) { + async send(captionText: string) { const action$ = zip(this.asset$, this.contact$).pipe( first(), concatMap(([asset, contact]) => @@ -92,20 +92,16 @@ export class SendingPostCapturePage { ) ); - const onConfirm = () => { + const result = await this.confirmAlert.present( + this.translocoService.translate('message.sendPostCaptureAlert') + ); + + if (result) { this.blockingActionService .run$(action$) .pipe(untilDestroyed(this)) .subscribe(); - }; - - this.confirmAlert - .present$( - onConfirm, - this.translocoService.translate('message.sendPostCaptureAlert') - ) - .pipe(untilDestroyed(this)) - .subscribe(); + } } private removeAsset$() { diff --git a/src/app/services/confirm-alert/confirm-alert.service.spec.ts b/src/app/services/confirm-alert/confirm-alert.service.spec.ts index a532dcf6c..29a8de13a 100644 --- a/src/app/services/confirm-alert/confirm-alert.service.spec.ts +++ b/src/app/services/confirm-alert/confirm-alert.service.spec.ts @@ -12,7 +12,8 @@ describe('ConfirmAlert', () => { service = TestBed.inject(ConfirmAlert); }); - it('should be created', () => { - expect(service).toBeTruthy(); - }); + it('should be created', () => expect(service).toBeTruthy()); + + it('should be able to present alert', () => + expect(service.present()).toBeTruthy()); }); diff --git a/src/app/services/confirm-alert/confirm-alert.service.ts b/src/app/services/confirm-alert/confirm-alert.service.ts index 9bcbc1db3..f5fe7e835 100644 --- a/src/app/services/confirm-alert/confirm-alert.service.ts +++ b/src/app/services/confirm-alert/confirm-alert.service.ts @@ -1,8 +1,6 @@ import { Injectable } from '@angular/core'; import { AlertController } from '@ionic/angular'; import { TranslocoService } from '@ngneat/transloco'; -import { defer } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; @Injectable({ providedIn: 'root', @@ -13,26 +11,28 @@ export class ConfirmAlert { private readonly translocoService: TranslocoService ) {} - present$( - onConfirm: () => void, + async present( message: string = this.translocoService.translate('message.areYouSure') ) { - return defer(() => - this.alertController.create({ - header: this.translocoService.translate('areYouSure'), - message, - buttons: [ - { - text: this.translocoService.translate('cancel'), - role: 'cancel', - }, - { - text: this.translocoService.translate('ok'), - handler: onConfirm, - }, - ], - mode: 'md', - }) - ).pipe(switchMap(alertElement => alertElement.present())); + return new Promise(resolve => { + this.alertController + .create({ + header: this.translocoService.translate('areYouSure'), + message, + buttons: [ + { + text: this.translocoService.translate('cancel'), + role: 'cancel', + handler: () => resolve(false), + }, + { + text: this.translocoService.translate('ok'), + handler: () => resolve(true), + }, + ], + mode: 'md', + }) + .then(alertElement => alertElement.present()); + }); } } diff --git a/src/assets/i18n/en-us.json b/src/assets/i18n/en-us.json index d6f06f046..4f497d19f 100644 --- a/src/assets/i18n/en-us.json +++ b/src/assets/i18n/en-us.json @@ -40,7 +40,6 @@ "low": "Low", "high": "High", "device": "Device", - "processing": "Processing", "email": "Email", "password": "Password", "confirmPassword": "Confirm Password", diff --git a/src/assets/i18n/zh-tw.json b/src/assets/i18n/zh-tw.json index 7c0b4183e..c243989d5 100644 --- a/src/assets/i18n/zh-tw.json +++ b/src/assets/i18n/zh-tw.json @@ -40,7 +40,6 @@ "low": "低", "high": "高", "device": "設備", - "processing": "處理中", "email": "電子郵件", "password": "密碼", "confirmPassword": "再次輸入密碼", From fc631e6682195c2091002258504617b505fe81d3 Mon Sep 17 00:00:00 2001 From: Sean Wu Date: Fri, 4 Dec 2020 23:22:52 +0800 Subject: [PATCH 04/11] Add tests for LanguageService. --- src/app/app.module.ts | 2 +- src/app/services/language/language.service.spec.ts | 9 +++++++-- src/app/services/language/language.service.ts | 5 +---- .../{ => language}/transloco/transloco-root.module.ts | 2 +- .../{ => language}/transloco/transloco-testing.module.ts | 4 ++-- src/app/services/notification/notification-item.spec.ts | 7 +++++-- .../services/notification/notification.service.spec.ts | 6 ++++-- src/app/shared/shared-testing.module.ts | 2 +- 8 files changed, 22 insertions(+), 15 deletions(-) rename src/app/services/{ => language}/transloco/transloco-root.module.ts (95%) rename src/app/services/{ => language}/transloco/transloco-testing.module.ts (82%) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 048cbec13..77ee61b4e 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -8,7 +8,7 @@ import { FormlyModule } from '@ngx-formly/core'; import { FormlyMaterialModule } from '@ngx-formly/material'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { TranslocoRootModule } from './services/transloco/transloco-root.module'; +import { TranslocoRootModule } from './services/language/transloco/transloco-root.module'; import { SharedModule } from './shared/shared.module'; @NgModule({ diff --git a/src/app/services/language/language.service.spec.ts b/src/app/services/language/language.service.spec.ts index 12887c45b..cc13829e8 100644 --- a/src/app/services/language/language.service.spec.ts +++ b/src/app/services/language/language.service.spec.ts @@ -12,7 +12,12 @@ describe('LanguageService', () => { service = TestBed.inject(LanguageService); }); - it('should be created', () => { - expect(service).toBeTruthy(); + it('should be created', () => expect(service).toBeTruthy()); + + it('should get current language key', async () => { + const expected = 'es-mx'; + await service.initialize(); + await service.setCurrentLanguage(expected); + expect(await service.getCurrentLanguageKey()).toEqual(expected); }); }); diff --git a/src/app/services/language/language.service.ts b/src/app/services/language/language.service.ts index bb5b8e978..3e761bb9d 100644 --- a/src/app/services/language/language.service.ts +++ b/src/app/services/language/language.service.ts @@ -1,10 +1,7 @@ import { Injectable } from '@angular/core'; import { TranslocoService } from '@ngneat/transloco'; -import { - defaultLanguage, - languages, -} from '../../services/transloco/transloco-root.module'; import { PreferenceManager } from '../preference-manager/preference-manager.service'; +import { defaultLanguage, languages } from './transloco/transloco-root.module'; @Injectable({ providedIn: 'root', diff --git a/src/app/services/transloco/transloco-root.module.ts b/src/app/services/language/transloco/transloco-root.module.ts similarity index 95% rename from src/app/services/transloco/transloco-root.module.ts rename to src/app/services/language/transloco/transloco-root.module.ts index 2dc646019..747ea9fe1 100644 --- a/src/app/services/transloco/transloco-root.module.ts +++ b/src/app/services/language/transloco/transloco-root.module.ts @@ -9,7 +9,7 @@ import { TRANSLOCO_CONFIG, TRANSLOCO_LOADER, } from '@ngneat/transloco'; -import { environment } from '../../../environments/environment'; +import { environment } from '../../../../environments/environment'; export const languages: { [key: string]: string } = { 'en-us': 'English (United State)', diff --git a/src/app/services/transloco/transloco-testing.module.ts b/src/app/services/language/transloco/transloco-testing.module.ts similarity index 82% rename from src/app/services/transloco/transloco-testing.module.ts rename to src/app/services/language/transloco/transloco-testing.module.ts index 2a607c72d..2cd90fd0d 100644 --- a/src/app/services/transloco/transloco-testing.module.ts +++ b/src/app/services/language/transloco/transloco-testing.module.ts @@ -1,6 +1,6 @@ import { TranslocoConfig, TranslocoTestingModule } from '@ngneat/transloco'; -import enUs from '../../../assets/i18n/en-us.json'; -import zhTw from '../../../assets/i18n/zh-tw.json'; +import enUs from '../../../../assets/i18n/en-us.json'; +import zhTw from '../../../../assets/i18n/zh-tw.json'; import { defaultLanguage, languages } from './transloco-root.module'; export function getTranslocoTestingModule( diff --git a/src/app/services/notification/notification-item.spec.ts b/src/app/services/notification/notification-item.spec.ts index 1420a6420..80ee4ecd0 100644 --- a/src/app/services/notification/notification-item.spec.ts +++ b/src/app/services/notification/notification-item.spec.ts @@ -26,8 +26,10 @@ describe('NotificationItem', () => { it('should be created', () => expect(item).toBeTruthy()); it('should be able to notify', async () => { - spyOn(console, 'log'); - expect(await item.notify('', '')).toBeInstanceOf(NotificationItem); + spyOn(console, 'info'); + expect(await item.notify('SAMPLE_TITLE', 'SAMPLE_MESSAGE')).toBeInstanceOf( + NotificationItem + ); }); it('should be able to notify error', async () => { @@ -36,6 +38,7 @@ describe('NotificationItem', () => { }); it('should be able to cancel itself', async () => { + spyOn(console, 'log'); expect(await item.cancel()).toBeInstanceOf(NotificationItem); }); }); diff --git a/src/app/services/notification/notification.service.spec.ts b/src/app/services/notification/notification.service.spec.ts index e603d584d..6d9c04230 100644 --- a/src/app/services/notification/notification.service.spec.ts +++ b/src/app/services/notification/notification.service.spec.ts @@ -26,8 +26,10 @@ describe('NotificationService', () => { expect(service.createNotification()).toBeInstanceOf(NotificationItem)); it('should be able to notify', async () => { - spyOn(console, 'log'); - expect(await service.notify('', '')).toBeInstanceOf(NotificationItem); + spyOn(console, 'info'); + expect( + await service.notify('SAMPLE_TITLE', 'SAMPLE_MESSAGE') + ).toBeInstanceOf(NotificationItem); }); it('should be able to notify error', async () => { diff --git a/src/app/shared/shared-testing.module.ts b/src/app/shared/shared-testing.module.ts index c5f7dc31e..251296723 100644 --- a/src/app/shared/shared-testing.module.ts +++ b/src/app/shared/shared-testing.module.ts @@ -3,7 +3,7 @@ import { NgModule } from '@angular/core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; import { IonicModule } from '@ionic/angular'; -import { getTranslocoTestingModule } from '../services/transloco/transloco-testing.module'; +import { getTranslocoTestingModule } from '../services/language/transloco/transloco-testing.module'; import { CapacitorPluginsTestingModule } from './capacitor-plugins/capacitor-plugins-testing.module'; import { MaterialTestingModule } from './material/material-testing.module'; import { SharedModule } from './shared.module'; From fd13bccf360ff1ec97d8bc781279e8d8adcbc0a2 Mon Sep 17 00:00:00 2001 From: Sean Wu Date: Sat, 5 Dec 2020 00:17:18 +0800 Subject: [PATCH 05/11] Fix incorrect info in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b22bf24f..3a9aa71dd 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Run `npm i` to update `package-lock.json`. Write the changelog in `CHANGELOG.md`. -When push to the `develop` branch with new version in the `package.json` file, GitHub Action would automatically do the following jobs: +When push to the `master` branch with new version in the `package.json` file, GitHub Action would automatically do the following jobs: 1. Create release GitHub page with debug APK. 1. Publish the app to Play Console on alpha track. From abf085ec6deb206385f9365ac7e6c34e24aaab5e Mon Sep 17 00:00:00 2001 From: Sean Wu Date: Sat, 5 Dec 2020 20:31:29 +0800 Subject: [PATCH 06/11] Mock AlertController to avoid pop up alert during tests. --- .../confirm-alert/confirm-alert.service.spec.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/services/confirm-alert/confirm-alert.service.spec.ts b/src/app/services/confirm-alert/confirm-alert.service.spec.ts index 29a8de13a..4b5f7404f 100644 --- a/src/app/services/confirm-alert/confirm-alert.service.spec.ts +++ b/src/app/services/confirm-alert/confirm-alert.service.spec.ts @@ -1,18 +1,27 @@ import { TestBed } from '@angular/core/testing'; +import { AlertController } from '@ionic/angular'; import { SharedTestingModule } from '../../shared/shared-testing.module'; import { ConfirmAlert } from './confirm-alert.service'; describe('ConfirmAlert', () => { let service: ConfirmAlert; + let alertController: AlertController; beforeEach(() => { TestBed.configureTestingModule({ imports: [SharedTestingModule], }); service = TestBed.inject(ConfirmAlert); + alertController = TestBed.inject(AlertController); + const htmlIonAlertElementSpy = jasmine.createSpyObj('HTMLIonAlertElement', { + present: new Promise(resolve => resolve()), + }); + spyOn(alertController, 'create').and.returnValue(htmlIonAlertElementSpy); }); - it('should be created', () => expect(service).toBeTruthy()); + it('should be created', () => { + expect(service).toBeTruthy(); + }); it('should be able to present alert', () => expect(service.present()).toBeTruthy()); From f1a8f347b4af75c6f03107ec76041230b3df7c32 Mon Sep 17 00:00:00 2001 From: Sean Wu Date: Sun, 6 Dec 2020 00:32:19 +0800 Subject: [PATCH 07/11] Fix mocking AlertController might throw non-thenable error. --- src/app/services/confirm-alert/confirm-alert.service.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/services/confirm-alert/confirm-alert.service.spec.ts b/src/app/services/confirm-alert/confirm-alert.service.spec.ts index 4b5f7404f..42fd5c49d 100644 --- a/src/app/services/confirm-alert/confirm-alert.service.spec.ts +++ b/src/app/services/confirm-alert/confirm-alert.service.spec.ts @@ -5,18 +5,17 @@ import { ConfirmAlert } from './confirm-alert.service'; describe('ConfirmAlert', () => { let service: ConfirmAlert; - let alertController: AlertController; beforeEach(() => { TestBed.configureTestingModule({ imports: [SharedTestingModule], }); service = TestBed.inject(ConfirmAlert); - alertController = TestBed.inject(AlertController); + const alertController = TestBed.inject(AlertController); const htmlIonAlertElementSpy = jasmine.createSpyObj('HTMLIonAlertElement', { present: new Promise(resolve => resolve()), }); - spyOn(alertController, 'create').and.returnValue(htmlIonAlertElementSpy); + spyOn(alertController, 'create').and.resolveTo(htmlIonAlertElementSpy); }); it('should be created', () => { From 28a8a0152ae9be4a9953f5696c50c14676757a4a Mon Sep 17 00:00:00 2001 From: Sean Wu Date: Mon, 7 Dec 2020 00:59:37 +0800 Subject: [PATCH 08/11] Refactor API for backend. Close #271, #254. - Rename NumbersStorageBackend with DiaBackend. - Implement NotificationService.notifyOnGoing method. Close #254. - Apply on-going notification to CollectorService.runAndStore method. - Apply on-going notification to DiaBackendAssetRepository.add method. - Reimplement simplified IgnoredTransactionRepository. - Extract /api/**/transactions endpoints to standalone service. - Extract /api/**/assets endpoints to standalone service. - Extract /auth endpoints to standalone service with tests. - Add readonly modifier to most dictionary interface. - Improve the import location for secret.ts from set-secret preconfig. Squashed commit of the following: commit af576406552595882f5290baf6e084091240cee2 Author: Sean Wu Date: Sun Dec 6 23:41:22 2020 +0800 Remove NumbersStorageApi and its old repositories. commit abfd9eeb78e160019db1fd971459e2a875baed92 Author: Sean Wu Date: Sun Dec 6 17:03:01 2020 +0800 Remove Publisher abstraction. commit 44a68a6afed72075aa3923ed208c352ddf73717a Author: Sean Wu Date: Sun Dec 6 16:56:21 2020 +0800 Apply notifyOnGoing to CollectorService. commit 276402fce136796d850409f25010c3b501f135f6 Author: Sean Wu Date: Sun Dec 6 16:20:28 2020 +0800 Fix tests for IgnoredTransactionRepository. commit d93007290ab4c44bb4fcdf2c432ef3a6c75a8b9e Author: Sean Wu Date: Sun Dec 6 16:20:11 2020 +0800 Add notifyOnGoing method. commit 94fe139b33a3e7b544f018b6b6758c0aba5bbf72 Author: Sean Wu Date: Sun Dec 6 16:19:50 2020 +0800 Supress TSLint warnings for whole test files. commit 3101a4de5d774c12e0f1b94cac4991d17d998eb3 Author: Sean Wu Date: Sun Dec 6 14:18:22 2020 +0800 Reimplement IgnoredTransactionRepository. commit 397e6f9b42586444b81960cb0975d452247a6e26 Author: Sean Wu Date: Sun Dec 6 01:29:27 2020 +0800 Add TODO for extracting CacheStore from ImageStore. commit eb40474181a57f0f2c8572f2c1d92c4343b2567e Author: Sean Wu Date: Sun Dec 6 01:29:04 2020 +0800 Extract /api/**/transactions endpoints. commit 7daf71aad13b9ad668948c5f3eafc6e5f6ffc0a5 Author: Sean Wu Date: Sun Dec 6 01:00:46 2020 +0800 Improve logging message of set-secret. commit ad3203159a1eb85d90681142fa74c45dd263b763 Author: Sean Wu Date: Sun Dec 6 00:58:58 2020 +0800 Add readonly modifiers. commit 60fc9b07d8e03076072ece2dddb3633152088113 Merge: 3f0691c f1a8f34 Author: Sean Wu Date: Sun Dec 6 00:32:58 2020 +0800 Merge branch 'develop' into refactor-dia-backend commit 3f0691c4b9607447916681dcff4032c67a0fd52b Author: Sean Wu Date: Sun Dec 6 00:25:43 2020 +0800 Extract /api/**/assets endpoints. commit 9f78b2afada1387dd8bf2a627a3b676ca133c46c Merge: a70f741 abf085e Author: Sean Wu Date: Sat Dec 5 20:31:54 2020 +0800 Merge branch 'develop' into refactor-dia-backend commit a70f7416843708621b76baa735403026ddf9c68f Author: Sean Wu Date: Sat Dec 5 20:20:41 2020 +0800 Extract /auth endpoints with tests. --- e2e/src/app.po.ts | 4 +- set-secret.js | 10 +- src/app/app.component.ts | 48 +-- src/app/guards/auth/auth.guard.ts | 27 +- .../pages/home/activity/activity.page.html | 8 +- src/app/pages/home/activity/activity.page.ts | 62 ++-- src/app/pages/home/asset/asset.page.ts | 18 +- .../asset/information/information.page.ts | 6 +- .../sending-post-capture.page.html | 2 +- .../sending-post-capture.page.ts | 22 +- src/app/pages/home/home.page.ts | 73 ++-- src/app/pages/home/inbox/inbox.page.ts | 47 +-- src/app/pages/login/login.page.ts | 6 +- src/app/pages/profile/profile.page.ts | 16 +- src/app/pages/signup/signup.page.ts | 6 +- .../collector/collector.service.spec.ts | 18 +- .../services/collector/collector.service.ts | 11 +- .../capacitor-filesystem-table.spec.ts | 10 +- .../capacitor-filesystem-table.ts | 10 +- src/app/services/database/table/table.ts | 8 +- ...a-backend-asset-repository.service.spec.ts | 18 + .../dia-backend-asset-repository.service.ts | 159 +++++++++ .../auth/dia-backend-auth.service.spec.ts | 139 ++++++++ .../auth/dia-backend-auth.service.ts | 166 +++++++++ ...end-transaction-repository.service.spec.ts | 16 + ...-backend-transaction-repository.service.ts | 75 ++++ ...red-transaction-repository.service.spec.ts | 8 +- .../ignored-transaction-repository.service.ts | 29 ++ .../image-store/image-store.service.ts | 8 +- .../notification/notification-item.spec.ts | 49 ++- .../notification/notification-item.ts | 59 +++- .../notification/notification.service.spec.ts | 49 ++- .../notification/notification.service.ts | 6 +- .../numbers-storage-api.service.spec.ts | 18 - .../numbers-storage-api.service.ts | 328 ------------------ .../numbers-storage-publisher.ts | 45 --- .../asset/asset-repository.service.spec.ts | 18 - .../asset/asset-repository.service.ts | 44 --- .../repositories/asset/asset.ts | 17 - .../ignored-transaction-repository.service.ts | 33 -- .../ignored-transaction.ts | 5 - src/app/services/publisher/publisher.ts | 37 -- .../publishers-alert.service.spec.ts | 18 - .../publishers-alert.service.ts | 69 ---- .../push-notification.service.ts | 36 +- src/app/services/repositories/proof/proof.ts | 22 +- .../post-capture-card.component.spec.ts | 9 +- .../post-capture-card.component.ts | 8 +- src/app/utils/camera.ts | 4 +- src/app/utils/crypto/crypto.ts | 4 +- src/app/utils/immutable/immutable.ts | 2 +- src/assets/i18n/en-us.json | 2 +- src/assets/i18n/zh-tw.json | 2 +- 53 files changed, 993 insertions(+), 921 deletions(-) create mode 100644 src/app/services/dia-backend/asset/dia-backend-asset-repository.service.spec.ts create mode 100644 src/app/services/dia-backend/asset/dia-backend-asset-repository.service.ts create mode 100644 src/app/services/dia-backend/auth/dia-backend-auth.service.spec.ts create mode 100644 src/app/services/dia-backend/auth/dia-backend-auth.service.ts create mode 100644 src/app/services/dia-backend/transaction/dia-backend-transaction-repository.service.spec.ts create mode 100644 src/app/services/dia-backend/transaction/dia-backend-transaction-repository.service.ts rename src/app/services/{publisher/numbers-storage/repositories/ignored-transaction => dia-backend/transaction}/ignored-transaction-repository.service.spec.ts (60%) create mode 100644 src/app/services/dia-backend/transaction/ignored-transaction-repository.service.ts delete mode 100644 src/app/services/publisher/numbers-storage/numbers-storage-api.service.spec.ts delete mode 100644 src/app/services/publisher/numbers-storage/numbers-storage-api.service.ts delete mode 100644 src/app/services/publisher/numbers-storage/numbers-storage-publisher.ts delete mode 100644 src/app/services/publisher/numbers-storage/repositories/asset/asset-repository.service.spec.ts delete mode 100644 src/app/services/publisher/numbers-storage/repositories/asset/asset-repository.service.ts delete mode 100644 src/app/services/publisher/numbers-storage/repositories/asset/asset.ts delete mode 100644 src/app/services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction-repository.service.ts delete mode 100644 src/app/services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction.ts delete mode 100644 src/app/services/publisher/publisher.ts delete mode 100644 src/app/services/publisher/publishers-alert/publishers-alert.service.spec.ts delete mode 100644 src/app/services/publisher/publishers-alert/publishers-alert.service.ts diff --git a/e2e/src/app.po.ts b/e2e/src/app.po.ts index 8fd36d5ca..bea1c4a97 100644 --- a/e2e/src/app.po.ts +++ b/e2e/src/app.po.ts @@ -1,13 +1,11 @@ -// tslint:disable-next-line: no-implicit-dependencies +// tslint:disable: no-implicit-dependencies prefer-function-over-method import { browser, by, element } from 'protractor'; export class AppPage { - // tslint:disable-next-line: prefer-function-over-method async navigateTo() { return browser.get('/'); } - // tslint:disable-next-line: prefer-function-over-method async getParagraphText() { return element(by.deepCss('app-root ion-content')).getText(); } diff --git a/set-secret.js b/set-secret.js index 219dc1263..dc3a5517a 100644 --- a/set-secret.js +++ b/set-secret.js @@ -1,19 +1,15 @@ const fs = require('fs'); // Configure Angular `secret.ts` file path -const targetPath = './src/environments/secret.ts'; +const targetPath = './src/app/services/dia-backend/secret.ts'; // `secret.ts` file structure -const envConfigFile = `export const secret = { - numbersStorageBaseUrl: '${process.env.NUMBERS_STORAGE_BASE_URL}' -}; +const envConfigFile = `export const BASE_URL = '${process.env.NUMBERS_STORAGE_BASE_URL}'; `; fs.writeFile(targetPath, envConfigFile, err => { if (err) { throw console.error(err); } else { - console.log( - `Angular secrets.ts file generated correctly at ${targetPath} \n` - ); + console.log(`A secret file has generated successfully at ${targetPath} \n`); } }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 1ddeb035f..5a5e01ee7 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,17 +3,15 @@ import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { Plugins } from '@capacitor/core'; import { Platform } from '@ionic/angular'; -import { TranslocoService } from '@ngneat/transloco'; -import { UntilDestroy } from '@ngneat/until-destroy'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { defer } from 'rxjs'; +import { concatMap, first } from 'rxjs/operators'; import { CollectorService } from './services/collector/collector.service'; import { CapacitorFactsProvider } from './services/collector/facts/capacitor-facts-provider/capacitor-facts-provider.service'; import { WebCryptoApiSignatureProvider } from './services/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service'; +import { DiaBackendAssetRepository } from './services/dia-backend/asset/dia-backend-asset-repository.service'; import { LanguageService } from './services/language/language.service'; import { NotificationService } from './services/notification/notification.service'; -import { NumbersStorageApi } from './services/publisher/numbers-storage/numbers-storage-api.service'; -import { NumbersStoragePublisher } from './services/publisher/numbers-storage/numbers-storage-publisher'; -import { AssetRepository } from './services/publisher/numbers-storage/repositories/asset/asset-repository.service'; -import { PublishersAlert } from './services/publisher/publishers-alert/publishers-alert.service'; import { restoreKilledCapture } from './utils/camera'; const { SplashScreen } = Plugins; @@ -28,15 +26,12 @@ export class AppComponent { constructor( private readonly platform: Platform, private readonly collectorService: CollectorService, - private readonly publishersAlert: PublishersAlert, - private readonly translocoService: TranslocoService, - private readonly notificationService: NotificationService, - private readonly numbersStorageApi: NumbersStorageApi, - private readonly assetRepository: AssetRepository, private readonly iconRegistry: MatIconRegistry, private readonly sanitizer: DomSanitizer, private readonly capacitorFactsProvider: CapacitorFactsProvider, private readonly webCryptoApiSignatureProvider: WebCryptoApiSignatureProvider, + private readonly diaBackendAssetRepository: DiaBackendAssetRepository, + notificationService: NotificationService, langaugeService: LanguageService ) { notificationService.requestPermission(); @@ -44,16 +39,22 @@ export class AppComponent { this.restoreAppStatus(); this.initializeApp(); this.initializeCollector(); - this.initializePublisher(); this.registerIcon(); } - async restoreAppStatus() { - const photo = await restoreKilledCapture(); - const proof = await this.collectorService.runAndStore({ - [photo.base64]: { mimeType: photo.mimeType }, - }); - return this.publishersAlert.presentOrPublish(proof); + restoreAppStatus() { + return defer(restoreKilledCapture) + .pipe( + concatMap(photo => + this.collectorService.runAndStore({ + [photo.base64]: { mimeType: photo.mimeType }, + }) + ), + concatMap(proof => this.diaBackendAssetRepository.add(proof)), + first(), + untilDestroyed(this) + ) + .subscribe(); } async initializeApp() { @@ -69,17 +70,6 @@ export class AppComponent { ); } - initializePublisher() { - this.publishersAlert.addPublisher( - new NumbersStoragePublisher( - this.translocoService, - this.notificationService, - this.numbersStorageApi, - this.assetRepository - ) - ); - } - registerIcon() { this.iconRegistry.addSvgIcon( 'media-id', diff --git a/src/app/guards/auth/auth.guard.ts b/src/app/guards/auth/auth.guard.ts index da39c797e..cbc151ac6 100644 --- a/src/app/guards/auth/auth.guard.ts +++ b/src/app/guards/auth/auth.guard.ts @@ -6,9 +6,7 @@ import { RouterStateSnapshot, UrlTree, } from '@angular/router'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { NumbersStorageApi } from '../../services/publisher/numbers-storage/numbers-storage-api.service'; +import { DiaBackendAuthService } from '../../services/dia-backend/auth/dia-backend-auth.service'; @Injectable({ providedIn: 'root', @@ -16,24 +14,17 @@ import { NumbersStorageApi } from '../../services/publisher/numbers-storage/numb export class AuthGuard implements CanActivate { constructor( private readonly router: Router, - private readonly numbersStorageApi: NumbersStorageApi + private readonly diaBackendAuthService: DiaBackendAuthService ) {} - canActivate( + async canActivate( _route: ActivatedRouteSnapshot, _state: RouterStateSnapshot - ): - | Observable - | Promise - | boolean - | UrlTree { - return this.numbersStorageApi.isEnabled$().pipe( - map(isEnabled => { - if (isEnabled) { - return true; - } - return this.router.parseUrl('/login'); - }) - ); + ): Promise { + const hasLoggedIn = await this.diaBackendAuthService.hasLoggedIn(); + if (!hasLoggedIn) { + return this.router.parseUrl('/login'); + } + return hasLoggedIn; } } diff --git a/src/app/pages/home/activity/activity.page.html b/src/app/pages/home/activity/activity.page.html index d429a7a7a..89bf5460d 100644 --- a/src/app/pages/home/activity/activity.page.html +++ b/src/app/pages/home/activity/activity.page.html @@ -14,8 +14,12 @@
{{ activity.asset.id }}
{{ activity.created_at | date: 'short' }}
- diff --git a/src/app/pages/home/activity/activity.page.ts b/src/app/pages/home/activity/activity.page.ts index 8f174fce5..1d2f54707 100644 --- a/src/app/pages/home/activity/activity.page.ts +++ b/src/app/pages/home/activity/activity.page.ts @@ -1,12 +1,11 @@ import { Component } from '@angular/core'; import { UntilDestroy } from '@ngneat/until-destroy'; -import { of, zip } from 'rxjs'; -import { concatMap, first, map, pluck } from 'rxjs/operators'; +import { map, pluck } from 'rxjs/operators'; +import { DiaBackendAuthService } from '../../../services/dia-backend/auth/dia-backend-auth.service'; import { - NumbersStorageApi, - Transaction, -} from '../../../services/publisher/numbers-storage/numbers-storage-api.service'; -import { forkJoinWithDefault } from '../../../utils/rx-operators'; + DiaBackendTransaction, + DiaBackendTransactionRepository, +} from '../../../services/dia-backend/transaction/dia-backend-transaction-repository.service'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -16,44 +15,35 @@ import { forkJoinWithDefault } from '../../../utils/rx-operators'; }) export class ActivityPage { readonly status = Status; - readonly activitiesWithStatus$ = this.numbersStorageApi - .listTransactions$() + readonly activitiesWithStatus$ = this.diaBackendTransactionRepository + .getAll$() .pipe( pluck('results'), - concatMap(activities => - zip( - of(activities), - forkJoinWithDefault( - activities.map(activity => this.getStatus$(activity)) - ) - ) - ), - map(([activities, statusList]) => - activities.map((activity, index) => ({ + map(activities => + activities.map(activity => ({ ...activity, - status: statusList[index], + status: this.getStatus(activity), })) ) ); - constructor(private readonly numbersStorageApi: NumbersStorageApi) {} + constructor( + private readonly diaBackendAuthService: DiaBackendAuthService, + private readonly diaBackendTransactionRepository: DiaBackendTransactionRepository + ) {} - private getStatus$(activity: Transaction) { - return this.numbersStorageApi.getEmail$().pipe( - map(email => { - if (activity.expired) { - return Status.Returned; - } - if (!activity.fulfilled_at) { - return Status.InProgress; - } - if (activity.sender === email) { - return Status.Delivered; - } - return Status.Accepted; - }), - first() - ); + private async getStatus(activity: DiaBackendTransaction) { + const email = await this.diaBackendAuthService.getEmail(); + if (activity.expired) { + return Status.Returned; + } + if (!activity.fulfilled_at) { + return Status.InProgress; + } + if (activity.sender === email) { + return Status.Delivered; + } + return Status.Accepted; } } diff --git a/src/app/pages/home/asset/asset.page.ts b/src/app/pages/home/asset/asset.page.ts index 9e1b1d9ac..9205b7e6f 100644 --- a/src/app/pages/home/asset/asset.page.ts +++ b/src/app/pages/home/asset/asset.page.ts @@ -3,7 +3,6 @@ import { MatBottomSheet } from '@angular/material/bottom-sheet'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { Plugins } from '@capacitor/core'; -import { TranslocoService } from '@ngneat/transloco'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { combineLatest, defer, forkJoin, zip } from 'rxjs'; import { @@ -16,7 +15,7 @@ import { } from 'rxjs/operators'; import { BlockingActionService } from '../../../services/blocking-action/blocking-action.service'; import { ConfirmAlert } from '../../../services/confirm-alert/confirm-alert.service'; -import { AssetRepository } from '../../../services/publisher/numbers-storage/repositories/asset/asset-repository.service'; +import { DiaBackendAssetRepository } from '../../../services/dia-backend/asset/dia-backend-asset-repository.service'; import { getOldProof } from '../../../services/repositories/proof/old-proof-adapter'; import { ProofRepository } from '../../../services/repositories/proof/proof-repository.service'; import { isNonNullable } from '../../../utils/rx-operators'; @@ -38,7 +37,7 @@ export class AssetPage { readonly asset$ = this.route.paramMap.pipe( map(params => params.get('id')), isNonNullable(), - switchMap(id => this.assetRepository.getById$(id)), + switchMap(id => this.diaBackendAssetRepository.getById$(id)), isNonNullable() ); private readonly proofsWithOld$ = this.proofRepository @@ -51,9 +50,10 @@ export class AssetPage { readonly capture$ = combineLatest([this.asset$, this.proofsWithOld$]).pipe( map(([asset, proofsWithOld]) => ({ asset, + // tslint:disable-next-line: no-non-null-assertion proofWithOld: proofsWithOld.find( p => p.oldProof.hash === asset.proof_hash - ), + )!, })), isNonNullable() ); @@ -80,9 +80,8 @@ export class AssetPage { constructor( private readonly router: Router, private readonly route: ActivatedRoute, - private readonly translocoService: TranslocoService, private readonly confirmAlert: ConfirmAlert, - private readonly assetRepository: AssetRepository, + private readonly diaBackendAssetRepository: DiaBackendAssetRepository, private readonly proofRepository: ProofRepository, private readonly blockingActionService: BlockingActionService, private readonly dialog: MatDialog, @@ -125,11 +124,8 @@ export class AssetPage { first(), concatMap(([asset, capture]) => forkJoin([ - this.assetRepository.remove$(asset), - this.proofRepository.remove( - // tslint:disable-next-line: no-non-null-assertion - capture.proofWithOld!.proof - ), + this.diaBackendAssetRepository.remove(asset), + this.proofRepository.remove(capture.proofWithOld.proof), ]) ), switchMapTo(defer(() => this.router.navigate(['..']))) diff --git a/src/app/pages/home/asset/information/information.page.ts b/src/app/pages/home/asset/information/information.page.ts index 533efa537..dce9e8d6a 100644 --- a/src/app/pages/home/asset/information/information.page.ts +++ b/src/app/pages/home/asset/information/information.page.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router'; import { UntilDestroy } from '@ngneat/until-destroy'; import { combineLatest } from 'rxjs'; import { concatMap, map, switchMap } from 'rxjs/operators'; -import { AssetRepository } from '../../../../services/publisher/numbers-storage/repositories/asset/asset-repository.service'; +import { DiaBackendAssetRepository } from '../../../../services/dia-backend/asset/dia-backend-asset-repository.service'; import { getOldProof, getOldSignatures, @@ -21,7 +21,7 @@ export class InformationPage { readonly asset$ = this.route.paramMap.pipe( map(params => params.get('id')), isNonNullable(), - switchMap(id => this.assetRepository.getById$(id)), + switchMap(id => this.diaBackendAssetRepository.getById$(id)), isNonNullable() ); private readonly proofsWithOld$ = this.proofRepository @@ -70,7 +70,7 @@ export class InformationPage { constructor( private readonly route: ActivatedRoute, - private readonly assetRepository: AssetRepository, + private readonly diaBackendAssetRepository: DiaBackendAssetRepository, private readonly proofRepository: ProofRepository ) {} } diff --git a/src/app/pages/home/asset/sending-post-capture/sending-post-capture.page.html b/src/app/pages/home/asset/sending-post-capture/sending-post-capture.page.html index 3f3f0f103..ca6d6e574 100644 --- a/src/app/pages/home/asset/sending-post-capture/sending-post-capture.page.html +++ b/src/app/pages/home/asset/sending-post-capture/sending-post-capture.page.html @@ -12,7 +12,7 @@
-
+ diff --git a/src/app/pages/home/asset/sending-post-capture/sending-post-capture.page.ts b/src/app/pages/home/asset/sending-post-capture/sending-post-capture.page.ts index 004b855e6..d43a8a4c5 100644 --- a/src/app/pages/home/asset/sending-post-capture/sending-post-capture.page.ts +++ b/src/app/pages/home/asset/sending-post-capture/sending-post-capture.page.ts @@ -6,8 +6,8 @@ import { combineLatest, defer, forkJoin, zip } from 'rxjs'; import { concatMap, concatMapTo, first, map, switchMap } from 'rxjs/operators'; import { BlockingActionService } from '../../../../services/blocking-action/blocking-action.service'; import { ConfirmAlert } from '../../../../services/confirm-alert/confirm-alert.service'; -import { NumbersStorageApi } from '../../../../services/publisher/numbers-storage/numbers-storage-api.service'; -import { AssetRepository } from '../../../../services/publisher/numbers-storage/repositories/asset/asset-repository.service'; +import { DiaBackendAssetRepository } from '../../../../services/dia-backend/asset/dia-backend-asset-repository.service'; +import { DiaBackendTransactionRepository } from '../../../../services/dia-backend/transaction/dia-backend-transaction-repository.service'; import { getOldProof } from '../../../../services/repositories/proof/old-proof-adapter'; import { ProofRepository } from '../../../../services/repositories/proof/proof-repository.service'; import { isNonNullable } from '../../../../utils/rx-operators'; @@ -22,7 +22,7 @@ export class SendingPostCapturePage { readonly asset$ = this.route.paramMap.pipe( map(params => params.get('id')), isNonNullable(), - switchMap(id => this.assetRepository.getById$(id)), + switchMap(id => this.diaBackendAssetRepository.getById$(id)), isNonNullable() ); private readonly proofsWithOld$ = this.proofRepository @@ -35,9 +35,10 @@ export class SendingPostCapturePage { readonly capture$ = combineLatest([this.asset$, this.proofsWithOld$]).pipe( map(([asset, proofsWithThumbnailAndOld]) => ({ asset, + // tslint:disable-next-line: no-non-null-assertion proofWithThumbnailAndOld: proofsWithThumbnailAndOld.find( p => p.oldProof.hash === asset.proof_hash - ), + )!, })), isNonNullable() ); @@ -64,15 +65,15 @@ export class SendingPostCapturePage { constructor( private readonly router: Router, private readonly route: ActivatedRoute, - private readonly assetRepository: AssetRepository, + private readonly diaBackendAssetRepository: DiaBackendAssetRepository, private readonly proofRepository: ProofRepository, private readonly confirmAlert: ConfirmAlert, private readonly translocoService: TranslocoService, - private readonly numbersStorageApi: NumbersStorageApi, + private readonly diaBackendTransactionRepository: DiaBackendTransactionRepository, private readonly blockingActionService: BlockingActionService ) {} - preview(captionText: string) { + preview() { this.isPreview = true; } @@ -80,7 +81,7 @@ export class SendingPostCapturePage { const action$ = zip(this.asset$, this.contact$).pipe( first(), concatMap(([asset, contact]) => - this.numbersStorageApi.createTransaction$( + this.diaBackendTransactionRepository.add$( asset.id, contact, captionText @@ -109,9 +110,8 @@ export class SendingPostCapturePage { first(), concatMap(([asset, capture]) => forkJoin([ - this.assetRepository.remove$(asset), - // tslint:disable-next-line: no-non-null-assertion - this.proofRepository.remove(capture.proofWithThumbnailAndOld!.proof), + this.diaBackendAssetRepository.remove(asset), + this.proofRepository.remove(capture.proofWithThumbnailAndOld.proof), ]) ) ); diff --git a/src/app/pages/home/home.page.ts b/src/app/pages/home/home.page.ts index 42c1b3c61..90c7de980 100644 --- a/src/app/pages/home/home.page.ts +++ b/src/app/pages/home/home.page.ts @@ -1,21 +1,22 @@ import { formatDate } from '@angular/common'; import { Component, OnInit } from '@angular/core'; import { MatTabChangeEvent } from '@angular/material/tabs'; -import { UntilDestroy } from '@ngneat/until-destroy'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { groupBy } from 'lodash'; import { combineLatest, defer, interval, of, zip } from 'rxjs'; import { concatMap, concatMapTo, distinctUntilChanged, + first, map, pluck, } from 'rxjs/operators'; import { CollectorService } from '../../services/collector/collector.service'; -import { NumbersStorageApi } from '../../services/publisher/numbers-storage/numbers-storage-api.service'; -import { AssetRepository } from '../../services/publisher/numbers-storage/repositories/asset/asset-repository.service'; -import { IgnoredTransactionRepository } from '../../services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction-repository.service'; -import { PublishersAlert } from '../../services/publisher/publishers-alert/publishers-alert.service'; +import { DiaBackendAssetRepository } from '../../services/dia-backend/asset/dia-backend-asset-repository.service'; +import { DiaBackendAuthService } from '../../services/dia-backend/auth/dia-backend-auth.service'; +import { DiaBackendTransactionRepository } from '../../services/dia-backend/transaction/dia-backend-transaction-repository.service'; +import { IgnoredTransactionRepository } from '../../services/dia-backend/transaction/ignored-transaction-repository.service'; import { PushNotificationService } from '../../services/push-notification/push-notification.service'; import { getOldProof } from '../../services/repositories/proof/old-proof-adapter'; import { ProofRepository } from '../../services/repositories/proof/proof-repository.service'; @@ -32,23 +33,27 @@ export class HomePage implements OnInit { readonly capturesByDate$ = this.getCaptures$().pipe( map(captures => groupBy(captures, c => - formatDate(c.asset.uploaded_at, 'mediumDate', 'en-US') + formatDate( + c.proofWithThumbnail?.proof.truth.timestamp, + 'mediumDate', + 'en-US' + ) ) ) ); postCaptures$ = this.getPostCaptures$(); - readonly username$ = this.numbersStorageApi.getUsername$(); + readonly username$ = this.diaBackendAuthService.getUsername$(); captureButtonShow = true; inboxCount$ = this.pollingInbox$().pipe( map(transactions => transactions.length) ); constructor( - private readonly assetRepository: AssetRepository, private readonly proofRepository: ProofRepository, private readonly collectorService: CollectorService, - private readonly publishersAlert: PublishersAlert, - private readonly numbersStorageApi: NumbersStorageApi, + private readonly diaBackendAuthService: DiaBackendAuthService, + private readonly diaBackendAssetRepository: DiaBackendAssetRepository, + private readonly diaBackendTransactionRepository: DiaBackendTransactionRepository, private readonly pushNotificationService: PushNotificationService, private readonly ignoredTransactionRepository: IgnoredTransactionRepository ) {} @@ -58,7 +63,7 @@ export class HomePage implements OnInit { } private getCaptures$() { - const originallyOwnedAssets$ = this.assetRepository + const originallyOwnedAssets$ = this.diaBackendAssetRepository .getAll$() .pipe(map(assets => assets.filter(asset => asset.is_original_owner))); @@ -75,9 +80,10 @@ export class HomePage implements OnInit { map(([assets, proofsWithThumbnail]) => assets.map(asset => ({ asset, + // tslint:disable-next-line: no-non-null-assertion proofWithThumbnail: proofsWithThumbnail.find( p => getOldProof(p.proof).hash === asset.proof_hash - ), + )!, })) ), // WORKAROUND: Use the lax comparison for now. We will redefine the flow @@ -92,8 +98,8 @@ export class HomePage implements OnInit { private getPostCaptures$() { return zip( - this.numbersStorageApi.listTransactions$(), - this.numbersStorageApi.getEmail$() + this.diaBackendTransactionRepository.getAll$(), + this.diaBackendAuthService.getEmail() ).pipe( map(([transactionListResponse, email]) => transactionListResponse.results.filter( @@ -108,7 +114,7 @@ export class HomePage implements OnInit { of(transactions), forkJoinWithDefault( transactions.map(transaction => - this.numbersStorageApi.readAsset$(transaction.asset.id) + this.diaBackendAssetRepository.getById$(transaction.asset.id) ) ) ) @@ -122,12 +128,19 @@ export class HomePage implements OnInit { ); } - async capture() { - const photo = await capture(); - const proof = await this.collectorService.runAndStore({ - [photo.base64]: { mimeType: photo.mimeType }, - }); - return this.publishersAlert.presentOrPublish(proof); + capture() { + return defer(capture) + .pipe( + concatMap(photo => + this.collectorService.runAndStore({ + [photo.base64]: { mimeType: photo.mimeType }, + }) + ), + concatMap(proof => this.diaBackendAssetRepository.add(proof)), + first(), + untilDestroyed(this) + ) + .subscribe(); } onTapChanged(event: MatTabChangeEvent) { @@ -143,17 +156,25 @@ export class HomePage implements OnInit { private pollingInbox$() { // tslint:disable-next-line: no-magic-numbers return interval(10000).pipe( - concatMapTo(this.numbersStorageApi.listInbox$()), + concatMapTo(this.diaBackendTransactionRepository.getAll$()), pluck('results'), + concatMap(postCaptures => + zip(of(postCaptures), this.diaBackendAuthService.getEmail()) + ), + map(([postCaptures, email]) => + postCaptures.filter( + postCapture => + postCapture.receiver_email === email && + !postCapture.fulfilled_at && + !postCapture.expired + ) + ), concatMap(postCaptures => zip(of(postCaptures), this.ignoredTransactionRepository.getAll$()) ), map(([postCaptures, ignoredTransactions]) => postCaptures.filter( - postcapture => - !ignoredTransactions - .map(transaction => transaction.id) - .includes(postcapture.id) + postcapture => !ignoredTransactions.includes(postcapture.id) ) ) ); diff --git a/src/app/pages/home/inbox/inbox.page.ts b/src/app/pages/home/inbox/inbox.page.ts index c8cc1a6fd..553fb2f8d 100644 --- a/src/app/pages/home/inbox/inbox.page.ts +++ b/src/app/pages/home/inbox/inbox.page.ts @@ -3,8 +3,9 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { of, zip } from 'rxjs'; import { concatMap, map, pluck, tap } from 'rxjs/operators'; import { BlockingActionService } from '../../../services/blocking-action/blocking-action.service'; -import { NumbersStorageApi } from '../../../services/publisher/numbers-storage/numbers-storage-api.service'; -import { IgnoredTransactionRepository } from '../../../services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction-repository.service'; +import { DiaBackendAuthService } from '../../../services/dia-backend/auth/dia-backend-auth.service'; +import { DiaBackendTransactionRepository } from '../../../services/dia-backend/transaction/dia-backend-transaction-repository.service'; +import { IgnoredTransactionRepository } from '../../../services/dia-backend/transaction/ignored-transaction-repository.service'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -13,34 +14,43 @@ import { IgnoredTransactionRepository } from '../../../services/publisher/number styleUrls: ['./inbox.page.scss'], }) export class InboxPage { - postCaptures$ = this.listInbox(); + postCaptures$ = this.listInbox$(); constructor( - private readonly numbersStorageApi: NumbersStorageApi, + private readonly diaBackendAuthService: DiaBackendAuthService, + private readonly diaBackendTransactionRepository: DiaBackendTransactionRepository, private readonly ignoredTransactionRepository: IgnoredTransactionRepository, private readonly blockingActionService: BlockingActionService ) {} - private listInbox() { - return this.numbersStorageApi.listInbox$().pipe( + private listInbox$() { + return this.diaBackendTransactionRepository.getAll$().pipe( pluck('results'), + concatMap(postCaptures => + zip(of(postCaptures), this.diaBackendAuthService.getEmail()) + ), + map(([postCaptures, email]) => + postCaptures.filter( + postCapture => + postCapture.receiver_email === email && + !postCapture.fulfilled_at && + !postCapture.expired + ) + ), concatMap(postCaptures => zip(of(postCaptures), this.ignoredTransactionRepository.getAll$()) ), map(([postCaptures, ignoredTransactions]) => postCaptures.filter( - postcapture => - !ignoredTransactions - .map(transaction => transaction.id) - .includes(postcapture.id) + postcapture => !ignoredTransactions.includes(postcapture.id) ) ) ); } accept(id: string) { - const action$ = this.numbersStorageApi - .acceptTransaction$(id) + const action$ = this.diaBackendTransactionRepository + .accept$(id) .pipe(tap(_ => this.refresh())); this.blockingActionService @@ -49,17 +59,12 @@ export class InboxPage { .subscribe(); } - ignore(id: string) { - this.ignoredTransactionRepository - .add$({ id }) - .pipe( - tap(_ => this.refresh()), - untilDestroyed(this) - ) - .subscribe(); + async ignore(id: string) { + await this.ignoredTransactionRepository.add(id); + this.refresh(); } private refresh() { - this.postCaptures$ = this.listInbox(); + this.postCaptures$ = this.listInbox$(); } } diff --git a/src/app/pages/login/login.page.ts b/src/app/pages/login/login.page.ts index 79b871b05..77fc5d74e 100644 --- a/src/app/pages/login/login.page.ts +++ b/src/app/pages/login/login.page.ts @@ -8,7 +8,7 @@ import { FormlyFieldConfig } from '@ngx-formly/core'; import { combineLatest } from 'rxjs'; import { tap } from 'rxjs/operators'; import { BlockingActionService } from '../../services/blocking-action/blocking-action.service'; -import { NumbersStorageApi } from '../../services/publisher/numbers-storage/numbers-storage-api.service'; +import { DiaBackendAuthService } from '../../services/dia-backend/auth/dia-backend-auth.service'; import { EMAIL_REGEXP } from '../../utils/validation'; @UntilDestroy({ checkProperties: true }) @@ -24,7 +24,7 @@ export class LoginPage { constructor( private readonly blockingActionService: BlockingActionService, - private readonly numbersStorageApi: NumbersStorageApi, + private readonly diaBackendAuthService: DiaBackendAuthService, private readonly toastController: ToastController, private readonly translocoService: TranslocoService, private readonly router: Router @@ -78,7 +78,7 @@ export class LoginPage { } onSubmit() { - const action$ = this.numbersStorageApi.login$( + const action$ = this.diaBackendAuthService.login$( this.model.email, this.model.password ); diff --git a/src/app/pages/profile/profile.page.ts b/src/app/pages/profile/profile.page.ts index 4fc03266e..169251679 100644 --- a/src/app/pages/profile/profile.page.ts +++ b/src/app/pages/profile/profile.page.ts @@ -9,9 +9,7 @@ import { defer } from 'rxjs'; import { catchError, concatMapTo } from 'rxjs/operators'; import { BlockingActionService } from '../../services/blocking-action/blocking-action.service'; import { WebCryptoApiSignatureProvider } from '../../services/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service'; -import { NumbersStorageApi } from '../../services/publisher/numbers-storage/numbers-storage-api.service'; -import { AssetRepository } from '../../services/publisher/numbers-storage/repositories/asset/asset-repository.service'; -import { IgnoredTransactionRepository } from '../../services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction-repository.service'; +import { DiaBackendAuthService } from '../../services/dia-backend/auth/dia-backend-auth.service'; const { Clipboard } = Plugins; @@ -22,8 +20,8 @@ const { Clipboard } = Plugins; styleUrls: ['./profile.page.scss'], }) export class ProfilePage { - readonly username$ = this.numbersStorageApi.getUsername$(); - readonly email$ = this.numbersStorageApi.getEmail$(); + readonly username$ = this.diaBackendAuthService.getUsername$(); + readonly email$ = this.diaBackendAuthService.getEmail$(); readonly publicKey$ = this.webCryptoApiSignatureProvider.getPublicKey$(); readonly privateKey$ = this.webCryptoApiSignatureProvider.getPrivateKey$(); @@ -33,9 +31,7 @@ export class ProfilePage { private readonly toastController: ToastController, private readonly translocoService: TranslocoService, private readonly snackBar: MatSnackBar, - private readonly numbersStorageApi: NumbersStorageApi, - private readonly assetRepository: AssetRepository, - private readonly ignoredTransactionRepository: IgnoredTransactionRepository, + private readonly diaBackendAuthService: DiaBackendAuthService, private readonly webCryptoApiSignatureProvider: WebCryptoApiSignatureProvider ) {} @@ -47,9 +43,7 @@ export class ProfilePage { } logout() { - const action$ = this.assetRepository.removeAll$().pipe( - concatMapTo(this.ignoredTransactionRepository.removeAll$()), - concatMapTo(this.numbersStorageApi.logout$()), + const action$ = this.diaBackendAuthService.logout$().pipe( concatMapTo(defer(() => this.router.navigate(['/login']))), catchError(err => this.toastController diff --git a/src/app/pages/signup/signup.page.ts b/src/app/pages/signup/signup.page.ts index 78da88971..637d3a26c 100644 --- a/src/app/pages/signup/signup.page.ts +++ b/src/app/pages/signup/signup.page.ts @@ -8,7 +8,7 @@ import { FormlyFieldConfig } from '@ngx-formly/core'; import { combineLatest } from 'rxjs'; import { tap } from 'rxjs/operators'; import { BlockingActionService } from '../../services/blocking-action/blocking-action.service'; -import { NumbersStorageApi } from '../../services/publisher/numbers-storage/numbers-storage-api.service'; +import { DiaBackendAuthService } from '../../services/dia-backend/auth/dia-backend-auth.service'; import { EMAIL_REGEXP } from '../../utils/validation'; @UntilDestroy({ checkProperties: true }) @@ -29,7 +29,7 @@ export class SignupPage { constructor( private readonly blockingActionService: BlockingActionService, - private readonly numbersStorageApi: NumbersStorageApi, + private readonly diaBackendAuthService: DiaBackendAuthService, private readonly toastController: ToastController, private readonly translocoService: TranslocoService, private readonly router: Router @@ -155,7 +155,7 @@ export class SignupPage { } onSubmit() { - const action$ = this.numbersStorageApi.createUser$( + const action$ = this.diaBackendAuthService.createUser$( this.model.username, this.model.email, this.model.password diff --git a/src/app/services/collector/collector.service.spec.ts b/src/app/services/collector/collector.service.spec.ts index d54a8021d..83c54586e 100644 --- a/src/app/services/collector/collector.service.spec.ts +++ b/src/app/services/collector/collector.service.spec.ts @@ -1,3 +1,5 @@ +// tslint:disable: prefer-function-over-method no-unbound-method + import { TestBed } from '@angular/core/testing'; import { SharedTestingModule } from '../../shared/shared-testing.module'; import { MimeType } from '../../utils/mime-type'; @@ -15,18 +17,15 @@ import { SignatureProvider } from './signature/signature-provider'; describe('CollectorService', () => { let service: CollectorService; - let proofRepositorySpy: jasmine.SpyObj; + let proofRepository: ProofRepository; beforeEach(() => { - const spy = jasmine.createSpyObj('ProofRepository', ['add']); TestBed.configureTestingModule({ imports: [SharedTestingModule], - providers: [{ provide: ProofRepository, useValue: spy }], }); service = TestBed.inject(CollectorService); - proofRepositorySpy = TestBed.inject( - ProofRepository - ) as jasmine.SpyObj; + proofRepository = TestBed.inject(ProofRepository); + spyOn(console, 'info'); }); it('should be created', () => expect(service).toBeTruthy()); @@ -67,10 +66,11 @@ describe('CollectorService', () => { }); it('should store proof with ProofRepository', async () => { + spyOn(proofRepository, 'add').and.callThrough(); + const proof = await service.runAndStore(ASSETS); - // tslint:disable-next-line: no-unbound-method - expect(proofRepositorySpy.add).toHaveBeenCalledWith(proof); + expect(proofRepository.add).toHaveBeenCalledWith(proof); }); }); @@ -98,7 +98,6 @@ const FACTS: Facts = { class MockFactsProvider implements FactsProvider { readonly id = MockFactsProvider.name; - // tslint:disable-next-line: prefer-function-over-method async provide(_: Assets) { return FACTS; } @@ -116,7 +115,6 @@ const SIGNATURE: Signature = { }; class MockSignatureProvider implements SignatureProvider { readonly id = MockSignatureProvider.name; - // tslint:disable-next-line: prefer-function-over-method async provide(_: string) { return SIGNATURE; } diff --git a/src/app/services/collector/collector.service.ts b/src/app/services/collector/collector.service.ts index 2fdd815fd..93ee16a10 100644 --- a/src/app/services/collector/collector.service.ts +++ b/src/app/services/collector/collector.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { TranslocoService } from '@ngneat/transloco'; +import { defer } from 'rxjs'; import { ImageStore } from '../image-store/image-store.service'; import { NotificationService } from '../notification/notification.service'; import { @@ -29,16 +30,18 @@ export class CollectorService { ) {} async runAndStore(assets: Assets) { - const notification = await this.notificationService.notify( + return this.notificationService.notifyOnGoing( + defer(() => this._runAndStore(assets)), this.translocoService.translate('storingAssets'), this.translocoService.translate('message.storingAssets') ); + } + + private async _runAndStore(assets: Assets) { const truth = await this.collectTruth(assets); const signatures = await this.signTargets({ assets, truth }); const proof = await Proof.from(this.imageStore, assets, truth, signatures); - await this.proofRepository.add(proof); - await notification.cancel(); - return proof; + return this.proofRepository.add(proof); } private async collectTruth(assets: Assets): Promise { diff --git a/src/app/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.spec.ts b/src/app/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.spec.ts index 5d6f280d9..bb1908a99 100644 --- a/src/app/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.spec.ts +++ b/src/app/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.spec.ts @@ -159,14 +159,14 @@ describe('CapacitorFilesystemTable', () => { }); interface TestTuple extends Tuple { - id: number; - name: string; - happy: boolean; - skills: { + readonly id: number; + readonly name: string; + readonly happy: boolean; + readonly skills: { name: string; level: number; }[]; - address: { + readonly address: { country: string; city: string; }; diff --git a/src/app/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.ts b/src/app/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.ts index 954224425..4a15fb838 100644 --- a/src/app/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.ts +++ b/src/app/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.ts @@ -99,7 +99,7 @@ export class CapacitorFilesystemTable implements Table { private assertNoConflictWithExistedTuples(tuples: T[]) { const conflicted = intersaction(tuples, this.tuples$.value); if (conflicted.length !== 0) { - throw new Error(`Tuples existed: ${conflicted}`); + throw new Error(`Tuples existed: ${JSON.stringify(conflicted)}`); } } @@ -121,7 +121,11 @@ export class CapacitorFilesystemTable implements Table { tuple => !this.tuples$.value.find(t => equals(tuple)(t)) ); if (nonexistent.length !== 0) { - throw new Error(`Cannot delete nonexistent tuples: ${nonexistent}`); + console.error(JSON.stringify(this.tuples$.value)); + + throw new Error( + `Cannot delete nonexistent tuples: ${JSON.stringify(nonexistent)}` + ); } } @@ -159,7 +163,7 @@ function assertNoDuplicatedTuples(tuples: T[]) { } }); if (conflicted.length !== 0) { - throw new Error(`Tuples duplicated: ${conflicted}`); + throw new Error(`Tuples duplicated: ${JSON.stringify(conflicted)}`); } } diff --git a/src/app/services/database/table/table.ts b/src/app/services/database/table/table.ts index 787757206..a2a004e5f 100644 --- a/src/app/services/database/table/table.ts +++ b/src/app/services/database/table/table.ts @@ -10,5 +10,11 @@ export interface Table { } export interface Tuple { - [key: string]: boolean | number | string | undefined | Tuple | Tuple[]; + readonly [key: string]: + | boolean + | number + | string + | undefined + | Tuple + | Tuple[]; } diff --git a/src/app/services/dia-backend/asset/dia-backend-asset-repository.service.spec.ts b/src/app/services/dia-backend/asset/dia-backend-asset-repository.service.spec.ts new file mode 100644 index 000000000..76b19e429 --- /dev/null +++ b/src/app/services/dia-backend/asset/dia-backend-asset-repository.service.spec.ts @@ -0,0 +1,18 @@ +import { TestBed } from '@angular/core/testing'; +import { SharedTestingModule } from '../../../shared/shared-testing.module'; +import { DiaBackendAssetRepository } from './dia-backend-asset-repository.service'; + +describe('DiaBackendAssetRepository', () => { + let service: DiaBackendAssetRepository; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SharedTestingModule], + }); + service = TestBed.inject(DiaBackendAssetRepository); + }); + + it('should be created', () => expect(service).toBeTruthy()); + + // TODO: add tests when implementing repository pattern +}); diff --git a/src/app/services/dia-backend/asset/dia-backend-asset-repository.service.ts b/src/app/services/dia-backend/asset/dia-backend-asset-repository.service.ts new file mode 100644 index 000000000..aaf89970f --- /dev/null +++ b/src/app/services/dia-backend/asset/dia-backend-asset-repository.service.ts @@ -0,0 +1,159 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { TranslocoService } from '@ngneat/transloco'; +import { defer, forkJoin } from 'rxjs'; +import { concatMap, single } from 'rxjs/operators'; +import { base64ToBlob } from '../../../utils/encoding/encoding'; +import { Database } from '../../database/database.service'; +import { Tuple } from '../../database/table/table'; +import { NotificationService } from '../../notification/notification.service'; +import { + getOldSignatures, + getSortedProofInformation, + OldDefaultInformationName, + OldSignature, + SortedProofInformation, +} from '../../repositories/proof/old-proof-adapter'; +import { DefaultFactId, Proof } from '../../repositories/proof/proof'; +import { DiaBackendAuthService } from '../auth/dia-backend-auth.service'; +import { BASE_URL } from '../secret'; + +@Injectable({ + providedIn: 'root', +}) +export class DiaBackendAssetRepository { + private readonly table = this.database.getTable( + DiaBackendAssetRepository.name + ); + + constructor( + private readonly httpClient: HttpClient, + private readonly authService: DiaBackendAuthService, + private readonly database: Database, + private readonly notificationService: NotificationService, + private readonly translocoService: TranslocoService + ) {} + + getAll$() { + return this.table.queryAll$(); + } + + // TODO: use repository pattern to read locally. + // NOTE: The DiaBackendAsset object is DIFFERENT between the one received from + // posting /api/v2/assets/ and getting /api/v2/assets/${id}/. This is a + // pitfall when you want to delete the existent one with another asset. + getById$(id: string) { + return defer(() => this.authService.getAuthHeaders()).pipe( + concatMap(headers => + this.httpClient.get( + `${BASE_URL}/api/v2/assets/${id}/`, + { headers } + ) + ) + ); + } + + async add(proof: Proof) { + return this.notificationService.notifyOnGoing( + this._add$(proof), + this.translocoService.translate('registeringProof'), + this.translocoService.translate('message.registeringProof') + ); + } + + private _add$(proof: Proof) { + return this.createAsset$(proof).pipe( + single(), + concatMap(asset => this.table.insert([asset])) + ); + } + + private createAsset$(proof: Proof) { + return forkJoin([ + defer(() => this.authService.getAuthHeaders()), + defer(() => buildFormDataToCreateAsset(proof)), + ]).pipe( + concatMap(([headers, formData]) => + this.httpClient.post( + `${BASE_URL}/api/v2/assets/`, + formData, + { headers } + ) + ) + ); + } + + // TODO: use repository to remove this method. + async addAssetDirectly(asset: DiaBackendAsset) { + return this.table.insert([asset]); + } + + // TODO: use repository to remove this method. + // NOTE: The DiaBackendAsset object is DIFFERENT between the one received from + // posting /api/v2/assets/ and getting /api/v2/assets/${id}/. This is a + // pitfall when you want to delete the existent one with another asset. + // You have to use ID as the primary key. + async remove(asset: DiaBackendAsset) { + const all = await this.table.queryAll(); + return this.table.delete(all.filter(a => a.id === asset.id)); + } +} + +export interface DiaBackendAsset extends Tuple { + readonly id: string; + readonly proof_hash: string; + readonly is_original_owner: boolean; + readonly asset_file: string; + readonly information: SortedProofInformation; + readonly signature: OldSignature[]; +} + +async function buildFormDataToCreateAsset(proof: Proof) { + const formData = new FormData(); + + const info = await getSortedProofInformation(proof); + const oldInfo = replaceDefaultFactIdWithOldDefaultInformationName(info); + formData.set('meta', JSON.stringify(oldInfo)); + + formData.set('signature', JSON.stringify(getOldSignatures(proof))); + + const fileBase64 = Object.keys(await proof.getAssets())[0]; + const mimeType = Object.values(proof.indexedAssets)[0].mimeType; + formData.set('asset_file', await base64ToBlob(fileBase64, mimeType)); + + formData.set('asset_file_mime_type', mimeType); + + return formData; +} + +function replaceDefaultFactIdWithOldDefaultInformationName( + sortedProofInformation: SortedProofInformation +): SortedProofInformation { + return { + proof: sortedProofInformation.proof, + information: sortedProofInformation.information.map(info => { + if (info.name === DefaultFactId.DEVICE_NAME) { + return { + provider: info.provider, + value: info.value, + name: OldDefaultInformationName.DEVICE_NAME, + }; + } + if (info.name === DefaultFactId.GEOLOCATION_LATITUDE) { + return { + provider: info.provider, + value: info.value, + name: OldDefaultInformationName.GEOLOCATION_LATITUDE, + }; + } + if (info.name === DefaultFactId.GEOLOCATION_LONGITUDE) { + return { + provider: info.provider, + value: info.value, + name: OldDefaultInformationName.GEOLOCATION_LONGITUDE, + }; + } + return info; + }), + }; +} diff --git a/src/app/services/dia-backend/auth/dia-backend-auth.service.spec.ts b/src/app/services/dia-backend/auth/dia-backend-auth.service.spec.ts new file mode 100644 index 000000000..47c7ef5a0 --- /dev/null +++ b/src/app/services/dia-backend/auth/dia-backend-auth.service.spec.ts @@ -0,0 +1,139 @@ +import { HttpClient } from '@angular/common/http'; +import {} from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { defer, EMPTY, of } from 'rxjs'; +import { concatMapTo, tap } from 'rxjs/operators'; +import { SharedTestingModule } from '../../../shared/shared-testing.module'; +import { BASE_URL } from '../secret'; +import { DiaBackendAuthService } from './dia-backend-auth.service'; + +const sampleUsername = 'test'; +const sampleEmail = 'test@test.com'; +const samplePassword = 'testpassword'; +const sampleToken = '0123-4567-89ab-cdef'; + +describe('DiaBackendAuthService', () => { + let service: DiaBackendAuthService; + let httpClient: HttpClient; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SharedTestingModule], + }); + service = TestBed.inject(DiaBackendAuthService); + httpClient = TestBed.inject(HttpClient); + mockHttpClient(httpClient); + }); + + it('should be created', () => expect(service).toBeTruthy()); + + it('should not login after created', done => { + service.hasLoggedIn$().subscribe(result => { + expect(result).toBeFalse(); + done(); + }); + }); + + it('should not have username after created', done => { + service.getUsername$().subscribe(result => { + expect(result).toBeFalsy(); + done(); + }); + }); + + it('should not have email after created', done => { + service.getEmail$().subscribe(result => { + expect(result).toBeFalsy(); + done(); + }); + }); + + it('should get username and email from result after logged in', done => { + service + .login$(sampleEmail, samplePassword) + .pipe( + tap(result => { + expect(result.username).toBeTruthy(); + expect(result.email).toBeTruthy(); + }) + ) + .subscribe(done); + }); + + it('should get username after logged in', done => { + service + .login$(sampleEmail, samplePassword) + .pipe( + concatMapTo(defer(() => service.getUsername())), + tap(username => expect(username).toBeTruthy()) + ) + .subscribe(done); + }); + + it('should get email after logged in', done => { + service + .login$(sampleEmail, samplePassword) + .pipe( + concatMapTo(defer(() => service.getEmail())), + tap(email => expect(email).toBeTruthy()) + ) + .subscribe(done); + }); + + it('should indicate has-logged-in after logged in', done => { + service + .login$(sampleEmail, samplePassword) + .pipe( + concatMapTo(defer(() => service.hasLoggedIn())), + tap(hasLoggedIn => expect(hasLoggedIn).toBeTrue()) + ) + .subscribe(done); + }); + + it('should clear email after logged out', done => { + service + .login$(sampleEmail, samplePassword) + .pipe( + concatMapTo(service.logout$()), + concatMapTo(defer(() => service.getEmail())), + tap(email => expect(email).toBeFalsy()) + ) + .subscribe(done); + }); + + it('should create user', done => { + service + .createUser$(sampleUsername, sampleEmail, samplePassword) + .subscribe(result => { + expect(result).toBeTruthy(); + done(); + }); + }); +}); + +function mockHttpClient(httpClient: HttpClient) { + spyOn(httpClient, 'post') + .withArgs(`${BASE_URL}/auth/token/login/`, { + email: sampleEmail, + password: samplePassword, + }) + .and.returnValue(of({ auth_token: sampleToken })) + .withArgs( + `${BASE_URL}/auth/token/logout/`, + {}, + { headers: { authorization: `token ${sampleToken}` } } + ) + .and.returnValue(of(EMPTY)) + .withArgs(`${BASE_URL}/auth/users/`, { + username: sampleUsername, + email: sampleEmail, + password: samplePassword, + }) + .and.returnValue(of(EMPTY)); + + spyOn(httpClient, 'get') + .withArgs(`${BASE_URL}/auth/users/me/`, { + headers: { authorization: `token ${sampleToken}` }, + }) + .and.returnValue(of({ username: sampleUsername, email: sampleEmail })); +} diff --git a/src/app/services/dia-backend/auth/dia-backend-auth.service.ts b/src/app/services/dia-backend/auth/dia-backend-auth.service.ts new file mode 100644 index 000000000..1cf3b2bad --- /dev/null +++ b/src/app/services/dia-backend/auth/dia-backend-auth.service.ts @@ -0,0 +1,166 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Plugins } from '@capacitor/core'; +import { defer, forkJoin, Observable } from 'rxjs'; +import { concatMap, concatMapTo, map } from 'rxjs/operators'; +import { PreferenceManager } from '../../preference-manager/preference-manager.service'; +import { BASE_URL } from '../secret'; + +const { Device } = Plugins; + +@Injectable({ + providedIn: 'root', +}) +export class DiaBackendAuthService { + private readonly preferences = this.preferenceManager.getPreferences( + DiaBackendAuthService.name + ); + + constructor( + private readonly httpClient: HttpClient, + private readonly preferenceManager: PreferenceManager + ) {} + + login$(email: string, password: string): Observable { + return this.httpClient + .post(`${BASE_URL}/auth/token/login/`, { + email, + password, + }) + .pipe( + concatMap(response => this.setToken(response.auth_token)), + concatMapTo(this.readUser$()), + concatMap(response => + forkJoin([ + this.setUsername(response.username), + this.setEmail(response.email), + ]) + ), + map(([username, _email]) => ({ username, email: _email })) + ); + } + + private readUser$() { + return defer(() => this.getAuthHeaders()).pipe( + concatMap(headers => + this.httpClient.get(`${BASE_URL}/auth/users/me/`, { + headers, + }) + ) + ); + } + + logout$() { + return defer(() => this.getAuthHeaders()).pipe( + concatMap(headers => + this.httpClient.post(`${BASE_URL}/auth/token/logout/`, {}, { headers }) + ), + concatMapTo( + defer(() => + Promise.all([ + this.setToken(''), + this.setEmail(''), + this.setUsername(''), + ]) + ) + ) + ); + } + + createUser$(username: string, email: string, password: string) { + return this.httpClient.post(`${BASE_URL}/auth/users/`, { + username, + email, + password, + }); + } + + // TODO: Internally depend on PushNotificationService and remove parameters. + createDevice$(fcmToken: string) { + return defer(() => + forkJoin([this.getAuthHeaders(), Device.getInfo()]) + ).pipe( + concatMap(([headers, deviceInfo]) => + this.httpClient.post( + `${BASE_URL}/auth/devices/`, + { + fcm_token: fcmToken, + platform: deviceInfo.platform, + device_identifier: deviceInfo.uuid, + }, + { headers } + ) + ) + ); + } + + hasLoggedIn$() { + return this.preferences + .getString$(PrefKeys.TOKEN) + .pipe(map(token => token !== '')); + } + + async hasLoggedIn() { + const token = await this.preferences.getString(PrefKeys.TOKEN); + return token !== ''; + } + + getUsername$() { + return this.preferences.getString$(PrefKeys.USERNAME); + } + + async getUsername() { + return this.preferences.getString(PrefKeys.USERNAME); + } + + private async setUsername(value: string) { + return this.preferences.setString(PrefKeys.USERNAME, value); + } + + getEmail$() { + return this.preferences.getString$(PrefKeys.EMAIL); + } + + async getEmail() { + return this.preferences.getString(PrefKeys.EMAIL); + } + + private async setEmail(value: string) { + return this.preferences.setString(PrefKeys.EMAIL, value); + } + + async getAuthHeaders() { + return { authorization: `token ${await this.getToken()}` }; + } + + private async getToken() { + return this.preferences.getString(PrefKeys.TOKEN); + } + + private async setToken(value: string) { + return this.preferences.setString(PrefKeys.TOKEN, value); + } +} + +const enum PrefKeys { + TOKEN = 'TOKEN', + USERNAME = 'USERNAME', + EMAIL = 'EMAIL', +} + +interface LoginResult { + readonly username: string; + readonly email: string; +} + +export interface LoginResponse { + readonly auth_token: string; +} + +export interface ReadUserResponse { + readonly username: string; + readonly email: string; +} + +// tslint:disable-next-line: no-empty-interface +interface CreateUserResponse {} diff --git a/src/app/services/dia-backend/transaction/dia-backend-transaction-repository.service.spec.ts b/src/app/services/dia-backend/transaction/dia-backend-transaction-repository.service.spec.ts new file mode 100644 index 000000000..fa2d2d930 --- /dev/null +++ b/src/app/services/dia-backend/transaction/dia-backend-transaction-repository.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; +import { SharedTestingModule } from '../../../shared/shared-testing.module'; +import { DiaBackendTransactionRepository } from './dia-backend-transaction-repository.service'; + +describe('DiaBackendTransactionRepository', () => { + let service: DiaBackendTransactionRepository; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SharedTestingModule], + }); + service = TestBed.inject(DiaBackendTransactionRepository); + }); + + it('should be created', () => expect(service).toBeTruthy()); +}); diff --git a/src/app/services/dia-backend/transaction/dia-backend-transaction-repository.service.ts b/src/app/services/dia-backend/transaction/dia-backend-transaction-repository.service.ts new file mode 100644 index 000000000..5fa876367 --- /dev/null +++ b/src/app/services/dia-backend/transaction/dia-backend-transaction-repository.service.ts @@ -0,0 +1,75 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { defer } from 'rxjs'; +import { concatMap } from 'rxjs/operators'; +import { DiaBackendAuthService } from '../auth/dia-backend-auth.service'; +import { BASE_URL } from '../secret'; + +@Injectable({ + providedIn: 'root', +}) +export class DiaBackendTransactionRepository { + constructor( + private readonly httpClient: HttpClient, + private readonly authService: DiaBackendAuthService + ) {} + + getAll$() { + return defer(() => this.authService.getAuthHeaders()).pipe( + concatMap(headers => + this.httpClient.get( + `${BASE_URL}/api/v2/transactions/`, + { headers } + ) + ) + ); + } + + add$(assetId: string, targetEmail: string, caption: string) { + return defer(() => this.authService.getAuthHeaders()).pipe( + concatMap(headers => + this.httpClient.post( + `${BASE_URL}/api/v2/transactions/`, + { asset_id: assetId, email: targetEmail, caption }, + { headers } + ) + ) + ); + } + + accept$(id: string) { + return defer(() => this.authService.getAuthHeaders()).pipe( + concatMap(headers => + this.httpClient.post( + `${BASE_URL}/api/v2/transactions/${id}/accept/`, + {}, + { headers } + ) + ) + ); + } +} + +export interface DiaBackendTransaction { + readonly id: string; + readonly asset: { + readonly id: string; + readonly asset_file_thumbnail: string; + readonly caption: string; + }; + readonly sender: string; + readonly receiver_email: string; + readonly created_at: string; + readonly fulfilled_at: string; + readonly expired: boolean; +} + +interface ListTransactionResponse { + readonly results: DiaBackendTransaction[]; +} + +// tslint:disable-next-line: no-empty-interface +interface CreateTransactionResponse {} + +// tslint:disable-next-line: no-empty-interface +interface AcceptTransactionResponse {} diff --git a/src/app/services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction-repository.service.spec.ts b/src/app/services/dia-backend/transaction/ignored-transaction-repository.service.spec.ts similarity index 60% rename from src/app/services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction-repository.service.spec.ts rename to src/app/services/dia-backend/transaction/ignored-transaction-repository.service.spec.ts index 7e9cc640e..0d9ab81be 100644 --- a/src/app/services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction-repository.service.spec.ts +++ b/src/app/services/dia-backend/transaction/ignored-transaction-repository.service.spec.ts @@ -1,18 +1,18 @@ import { TestBed } from '@angular/core/testing'; -import { SharedTestingModule } from '../../../../../shared/shared-testing.module'; +import { SharedTestingModule } from '../../../shared/shared-testing.module'; import { IgnoredTransactionRepository } from './ignored-transaction-repository.service'; describe('IgnoredTransactionRepository', () => { - let service: IgnoredTransactionRepository; + let repository: IgnoredTransactionRepository; beforeEach(() => { TestBed.configureTestingModule({ imports: [SharedTestingModule], }); - service = TestBed.inject(IgnoredTransactionRepository); + repository = TestBed.inject(IgnoredTransactionRepository); }); it('should be created', () => { - expect(service).toBeTruthy(); + expect(repository).toBeTruthy(); }); }); diff --git a/src/app/services/dia-backend/transaction/ignored-transaction-repository.service.ts b/src/app/services/dia-backend/transaction/ignored-transaction-repository.service.ts new file mode 100644 index 000000000..0c0a81112 --- /dev/null +++ b/src/app/services/dia-backend/transaction/ignored-transaction-repository.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { Database } from '../../database/database.service'; +import { Tuple } from '../../database/table/table'; + +@Injectable({ + providedIn: 'root', +}) +export class IgnoredTransactionRepository { + private readonly table = this.database.getTable( + IgnoredTransactionRepository.name + ); + + constructor(private readonly database: Database) {} + + getAll$() { + return this.table + .queryAll$() + .pipe(map(tuples => tuples.map(tuple => tuple.id))); + } + + async add(id: string) { + return this.table.insert([{ id }]); + } +} + +interface IgnoredTransaction extends Tuple { + readonly id: string; +} diff --git a/src/app/services/image-store/image-store.service.ts b/src/app/services/image-store/image-store.service.ts index 6477d7c70..955e9fbcf 100644 --- a/src/app/services/image-store/image-store.service.ts +++ b/src/app/services/image-store/image-store.service.ts @@ -9,6 +9,10 @@ import { MimeType } from '../../utils/mime-type'; import { Database } from '../database/database.service'; import { Tuple } from '../database/table/table'; +// TODO: Implement a CacheStore service to cache the thumb and other files, such +// as the image thumb from backend. User should be able to clear the cache +// freely. Finally, extract ImageStore.thumbnailTable to CacheStore. + const imageBlobReduce = new ImageBlobReduce(); @Injectable({ @@ -147,6 +151,6 @@ export class ImageStore { } interface Thumbnail extends Tuple { - imageIndex: string; - thumbnailIndex: string; + readonly imageIndex: string; + readonly thumbnailIndex: string; } diff --git a/src/app/services/notification/notification-item.spec.ts b/src/app/services/notification/notification-item.spec.ts index 80ee4ecd0..a03197a85 100644 --- a/src/app/services/notification/notification-item.spec.ts +++ b/src/app/services/notification/notification-item.spec.ts @@ -1,6 +1,9 @@ +// tslint:disable: no-unbound-method + import { TestBed } from '@angular/core/testing'; -import { Plugins } from '@capacitor/core'; +import { LocalNotificationsPlugin, Plugins } from '@capacitor/core'; import { TranslocoService } from '@ngneat/transloco'; +import { of, throwError } from 'rxjs'; import { LOCAL_NOTIFICATIONS_PLUGIN } from '../../shared/capacitor-plugins/capacitor-plugins.module'; import { SharedTestingModule } from '../../shared/shared-testing.module'; import { NotificationItem } from './notification-item'; @@ -9,7 +12,7 @@ const { LocalNotifications } = Plugins; describe('NotificationItem', () => { let item: NotificationItem; - const id = 2; + let localNotificationsPlugin: LocalNotificationsPlugin; beforeEach(() => { TestBed.configureTestingModule({ @@ -18,8 +21,9 @@ describe('NotificationItem', () => { { provide: LOCAL_NOTIFICATIONS_PLUGIN, useValue: LocalNotifications }, ], }); - const localNotificationsPlugin = TestBed.inject(LOCAL_NOTIFICATIONS_PLUGIN); + localNotificationsPlugin = TestBed.inject(LOCAL_NOTIFICATIONS_PLUGIN); const translocoService = TestBed.inject(TranslocoService); + const id = 2; item = new NotificationItem(id, localNotificationsPlugin, translocoService); }); @@ -27,18 +31,53 @@ describe('NotificationItem', () => { it('should be able to notify', async () => { spyOn(console, 'info'); - expect(await item.notify('SAMPLE_TITLE', 'SAMPLE_MESSAGE')).toBeInstanceOf( + expect(await item.notify(SAMPLE_TITLE, SAMPLE_MESSAGE)).toBeInstanceOf( NotificationItem ); }); it('should be able to notify error', async () => { spyOn(console, 'error'); - expect(await item.error(new Error())).toBeInstanceOf(NotificationItem); + expect(await item.error(SAMPLE_ERROR)).toBeInstanceOf(Error); }); it('should be able to cancel itself', async () => { spyOn(console, 'log'); expect(await item.cancel()).toBeInstanceOf(NotificationItem); }); + + it('should be able to notify with on going action and cancel automatically', async () => { + spyOn(console, 'info'); + spyOn(localNotificationsPlugin, 'cancel').and.resolveTo(); + const expected = 2; + const result = await item.notifyOnGoing( + of(1, expected), + SAMPLE_TITLE, + SAMPLE_MESSAGE + ); + expect(localNotificationsPlugin.cancel).toHaveBeenCalled(); + expect(result).toEqual(expected); + }); + + it('should return an Error and do not cancel automatically when throw during on going action', async () => { + const expected = SAMPLE_ERROR; + spyOn(console, 'info'); + spyOn(localNotificationsPlugin, 'cancel').and.resolveTo(); + spyOn(item, 'error').and.resolveTo(expected); + try { + await item.notifyOnGoing( + throwError(expected), + SAMPLE_TITLE, + SAMPLE_MESSAGE + ); + } catch (err) { + expect(err).toEqual(expected); + } + expect(localNotificationsPlugin.cancel).not.toHaveBeenCalled(); + expect(item.error).toHaveBeenCalled(); + }); }); + +const SAMPLE_TITLE = 'SAMPLE_TITLE'; +const SAMPLE_MESSAGE = 'SAMPLE_MESSAGE'; +const SAMPLE_ERROR = new Error('SAMPLE_ERROR'); diff --git a/src/app/services/notification/notification-item.ts b/src/app/services/notification/notification-item.ts index e5b23d66d..22dbb3aba 100644 --- a/src/app/services/notification/notification-item.ts +++ b/src/app/services/notification/notification-item.ts @@ -1,6 +1,8 @@ import { Inject } from '@angular/core'; -import { LocalNotificationsPlugin } from '@capacitor/core'; +import { LocalNotification, LocalNotificationsPlugin } from '@capacitor/core'; import { TranslocoService } from '@ngneat/transloco'; +import { Observable } from 'rxjs'; +import { last } from 'rxjs/operators'; import { LOCAL_NOTIFICATIONS_PLUGIN } from '../../shared/capacitor-plugins/capacitor-plugins.module'; export class NotificationItem { @@ -11,33 +13,54 @@ export class NotificationItem { private readonly translocoService: TranslocoService ) {} - async notify(title: string, body: string): Promise { + async notify(title: string, body: string) { console.info(`${title}: ${body}`); + return this.schedule({ id: this.id, title, body }); + } - await this.localNotificationsPlugin.schedule({ - notifications: [{ title, body, id: this.id }], + async error(error: Error) { + console.error(error); + await this.schedule({ + id: this.id, + title: this.translocoService.translate('unknownError'), + body: JSON.stringify(error), + ongoing: false, }); - return this; + return error; } - async error(error: Error): Promise { - console.error(error); + async cancel() { + this.localNotificationsPlugin.cancel({ + notifications: [{ id: String(this.id) }], + }); + return this; + } - await this.localNotificationsPlugin.schedule({ - notifications: [ - { - title: this.translocoService.translate('unknownError'), - body: JSON.stringify(error), - id: this.id, + async notifyOnGoing(action$: Observable, title: string, body: string) { + console.info(`${title}: ${body}`); + const notification = await this.schedule({ + id: this.id, + title, + body, + ongoing: true, + }); + return new Promise((resolve, reject) => { + action$.pipe(last()).subscribe({ + next(value) { + notification.cancel(); + resolve(value); + }, + error(err) { + notification.error(err); + reject(err); }, - ], + }); }); - return this; } - async cancel(): Promise { - this.localNotificationsPlugin.cancel({ - notifications: [{ id: String(this.id) }], + private async schedule(options: LocalNotification) { + await this.localNotificationsPlugin.schedule({ + notifications: [options], }); return this; } diff --git a/src/app/services/notification/notification.service.spec.ts b/src/app/services/notification/notification.service.spec.ts index 6d9c04230..e6617f37c 100644 --- a/src/app/services/notification/notification.service.spec.ts +++ b/src/app/services/notification/notification.service.spec.ts @@ -1,5 +1,8 @@ +// tslint:disable: no-unbound-method + import { TestBed } from '@angular/core/testing'; -import { Plugins } from '@capacitor/core'; +import { LocalNotificationsPlugin, Plugins } from '@capacitor/core'; +import { of, throwError } from 'rxjs'; import { LOCAL_NOTIFICATIONS_PLUGIN } from '../../shared/capacitor-plugins/capacitor-plugins.module'; import { SharedTestingModule } from '../../shared/shared-testing.module'; import { NotificationItem } from './notification-item'; @@ -9,6 +12,7 @@ const { LocalNotifications } = Plugins; describe('NotificationService', () => { let service: NotificationService; + let localNotificationsPlugin: LocalNotificationsPlugin; beforeEach(() => { TestBed.configureTestingModule({ @@ -18,6 +22,7 @@ describe('NotificationService', () => { ], }); service = TestBed.inject(NotificationService); + localNotificationsPlugin = TestBed.inject(LOCAL_NOTIFICATIONS_PLUGIN); }); it('should be created', () => expect(service).toBeTruthy()); @@ -27,13 +32,47 @@ describe('NotificationService', () => { it('should be able to notify', async () => { spyOn(console, 'info'); - expect( - await service.notify('SAMPLE_TITLE', 'SAMPLE_MESSAGE') - ).toBeInstanceOf(NotificationItem); + expect(await service.notify(SAMPLE_TITLE, SAMPLE_MESSAGE)).toBeInstanceOf( + NotificationItem + ); }); it('should be able to notify error', async () => { spyOn(console, 'error'); - expect(await service.error(new Error())).toBeInstanceOf(NotificationItem); + expect(await service.error(SAMPLE_ERROR)).toBeInstanceOf(Error); + }); + + it('should be able to notify with on going action and cancel automatically', async () => { + spyOn(console, 'info'); + spyOn(localNotificationsPlugin, 'cancel').and.resolveTo(); + const expected = 2; + const result = await service.notifyOnGoing( + of(1, expected), + SAMPLE_TITLE, + SAMPLE_MESSAGE + ); + expect(localNotificationsPlugin.cancel).toHaveBeenCalled(); + expect(result).toEqual(expected); + }); + + it('should return an Error and do not cancel automatically when throw during on going action', async () => { + spyOn(console, 'info'); + spyOn(console, 'error'); + const expected = SAMPLE_ERROR; + spyOn(localNotificationsPlugin, 'cancel').and.resolveTo(); + try { + await service.notifyOnGoing( + throwError(expected), + SAMPLE_TITLE, + SAMPLE_MESSAGE + ); + } catch (err) { + expect(err).toEqual(expected); + } + expect(localNotificationsPlugin.cancel).not.toHaveBeenCalled(); }); }); + +const SAMPLE_TITLE = 'SAMPLE_TITLE'; +const SAMPLE_MESSAGE = 'SAMPLE_MESSAGE'; +const SAMPLE_ERROR = new Error('SAMPLE_ERROR'); diff --git a/src/app/services/notification/notification.service.ts b/src/app/services/notification/notification.service.ts index 9a43d4483..6c6f0e948 100644 --- a/src/app/services/notification/notification.service.ts +++ b/src/app/services/notification/notification.service.ts @@ -1,6 +1,7 @@ import { Inject, Injectable } from '@angular/core'; import { LocalNotificationsPlugin } from '@capacitor/core'; import { TranslocoService } from '@ngneat/transloco'; +import { Observable } from 'rxjs'; import { LOCAL_NOTIFICATIONS_PLUGIN } from '../../shared/capacitor-plugins/capacitor-plugins.module'; import { NotificationItem } from './notification-item'; @@ -16,7 +17,6 @@ export class NotificationService { private readonly translocoService: TranslocoService ) {} - // tslint:disable-next-line: prefer-function-over-method async requestPermission() { return this.localNotificationsPlugin.requestPermission(); } @@ -41,4 +41,8 @@ export class NotificationService { async error(error: Error) { return this.createNotification().error(error); } + + async notifyOnGoing(action$: Observable, title: string, body: string) { + return this.createNotification().notifyOnGoing(action$, title, body); + } } diff --git a/src/app/services/publisher/numbers-storage/numbers-storage-api.service.spec.ts b/src/app/services/publisher/numbers-storage/numbers-storage-api.service.spec.ts deleted file mode 100644 index f4f0dc945..000000000 --- a/src/app/services/publisher/numbers-storage/numbers-storage-api.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { SharedTestingModule } from '../../../shared/shared-testing.module'; -import { NumbersStorageApi } from './numbers-storage-api.service'; - -describe('NumbersStorageApi', () => { - let service: NumbersStorageApi; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [SharedTestingModule], - }); - service = TestBed.inject(NumbersStorageApi); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/services/publisher/numbers-storage/numbers-storage-api.service.ts b/src/app/services/publisher/numbers-storage/numbers-storage-api.service.ts deleted file mode 100644 index 6c75d1c09..000000000 --- a/src/app/services/publisher/numbers-storage/numbers-storage-api.service.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { defer, of, zip } from 'rxjs'; -import { concatMap, concatMapTo, pluck } from 'rxjs/operators'; -import { secret } from '../../../../environments/secret'; -import { base64ToBlob } from '../../../utils/encoding/encoding'; -import { PreferenceManager } from '../../preference-manager/preference-manager.service'; -import { - getSortedProofInformation, - OldDefaultInformationName, - OldSignature, - SortedProofInformation, -} from '../../repositories/proof/old-proof-adapter'; -import { DefaultFactId, Proof } from '../../repositories/proof/proof'; -import { Asset } from './repositories/asset/asset'; - -@Injectable({ - providedIn: 'root', -}) -export class NumbersStorageApi { - private readonly preferences = this.preferenceManager.getPreferences( - NumbersStorageApi.name - ); - - constructor( - private readonly httpClient: HttpClient, - private readonly preferenceManager: PreferenceManager - ) {} - - createUser$(username: string, email: string, password: string) { - const formData = new FormData(); - formData.append('username', username); - formData.append('email', email); - formData.append('password', password); - return this.httpClient.post( - `${baseUrl}/auth/users/`, - formData - ); - } - - login$(email: string, password: string) { - const formData = new FormData(); - formData.append('email', email); - formData.append('password', password); - return this.httpClient - .post(`${baseUrl}/auth/token/login/`, formData) - .pipe( - pluck('auth_token'), - concatMap(authToken => this.storeAuthToken(`token ${authToken}`)), - concatMapTo(this.getUserInformation$()), - concatMap(user => - zip(this.setUsername(user.username), this.setEmail(user.email)) - ), - concatMapTo(defer(() => this.setEnabled(true))) - ); - } - - getUserInformation$() { - return defer(() => this.getHttpHeadersWithAuthToken()).pipe( - concatMap(headers => - this.httpClient.get(`${baseUrl}/auth/users/me/`, { - headers, - }) - ) - ); - } - - logout$() { - return defer(() => this.setEnabled(false)).pipe( - concatMapTo(defer(() => this.getHttpHeadersWithAuthToken())), - concatMap(headers => - this.httpClient.post(`${baseUrl}/auth/token/logout/`, {}, { headers }) - ), - concatMapTo( - defer(() => - Promise.all([ - this.setUsername('has-logged-out'), - this.setEmail('has-logged-out'), - this.storeAuthToken(''), - ]) - ) - ) - ); - } - - createOrUpdateDevice$( - platform: string, - deviceIdentifier: string, - fcmToken: string - ) { - const formData = new FormData(); - formData.append('platform', platform); - formData.append('device_identifier', deviceIdentifier); - formData.append('fcm_token', fcmToken); - return defer(() => this.getHttpHeadersWithAuthToken()).pipe( - concatMap(headers => - this.httpClient.post( - `${baseUrl}/auth/devices/`, - formData, - { headers } - ) - ) - ); - } - - readAsset$(id: string) { - return defer(() => this.getHttpHeadersWithAuthToken()).pipe( - concatMap(headers => - this.httpClient.get(`${baseUrl}/api/v2/assets/${id}/`, { - headers, - }) - ) - ); - } - - createAsset$( - rawFileBase64: string, - proof: Proof, - targetProvider: TargetProvider, - caption: string, - signatures: OldSignature[], - tag: string - ) { - const proofMimeType = Object.values(proof.indexedAssets)[0].mimeType; - return defer(() => this.getHttpHeadersWithAuthToken()).pipe( - concatMap(headers => - zip( - defer(() => base64ToBlob(rawFileBase64, proofMimeType)), - getSortedProofInformation(proof), - of(headers) - ) - ), - concatMap(([rawFile, sortedProofInformation, headers]) => { - const oldSortedProofInformation = replaceDefaultFactIdWithOldDefaultInformationName( - sortedProofInformation - ); - const formData = new FormData(); - formData.append('asset_file', rawFile); - formData.append('asset_file_mime_type', proofMimeType); - formData.append('meta', JSON.stringify(oldSortedProofInformation)); - formData.append('target_provider', targetProvider); - formData.append('caption', caption); - formData.append('signature', JSON.stringify(signatures)); - formData.append('tag', tag); - return this.httpClient.post( - `${baseUrl}/api/v2/assets/`, - formData, - { headers } - ); - }) - ); - } - - listTransactions$() { - return defer(() => this.getHttpHeadersWithAuthToken()).pipe( - concatMap(headers => - this.httpClient.get( - `${baseUrl}/api/v2/transactions/`, - { headers } - ) - ) - ); - } - - createTransaction$(assetId: string, email: string, caption: string) { - return defer(() => this.getHttpHeadersWithAuthToken()).pipe( - concatMap(headers => - this.httpClient.post( - `${baseUrl}/api/v2/transactions/`, - { asset_id: assetId, email, caption }, - { headers } - ) - ) - ); - } - - listInbox$() { - return defer(() => this.getHttpHeadersWithAuthToken()).pipe( - concatMap(headers => - this.httpClient.get( - `${baseUrl}/api/v2/transactions/inbox/`, - { headers } - ) - ) - ); - } - - acceptTransaction$(id: string) { - return defer(() => this.getHttpHeadersWithAuthToken()).pipe( - concatMap(headers => - this.httpClient.post( - `${baseUrl}/api/v2/transactions/${id}/accept/`, - {}, - { headers } - ) - ) - ); - } - - getImage$(url: string) { - return this.httpClient.get(url, { responseType: 'blob' }); - } - - private async getHttpHeadersWithAuthToken() { - const authToken = await this.preferences.getString(PrefKeys.AUTH_TOKEN); - return new HttpHeaders({ Authorization: authToken }); - } - - private async storeAuthToken(value: string) { - return this.preferences.setString(PrefKeys.AUTH_TOKEN, value); - } - - isEnabled$() { - return this.preferences.getBoolean$(PrefKeys.ENABLED); - } - - async setEnabled(value: boolean) { - return this.preferences.setBoolean(PrefKeys.ENABLED, value); - } - - getUsername$() { - return this.preferences.getString$(PrefKeys.USERNAME); - } - - async setUsername(value: string) { - return this.preferences.setString(PrefKeys.USERNAME, value); - } - - getEmail$() { - return this.preferences.getString$(PrefKeys.EMAIL); - } - - async setEmail(value: string) { - return this.preferences.setString(PrefKeys.EMAIL, value); - } -} - -function replaceDefaultFactIdWithOldDefaultInformationName( - sortedProofInformation: SortedProofInformation -): SortedProofInformation { - return { - proof: sortedProofInformation.proof, - information: sortedProofInformation.information.map(info => { - if (info.name === DefaultFactId.DEVICE_NAME) { - return { - provider: info.provider, - value: info.value, - name: OldDefaultInformationName.DEVICE_NAME, - }; - } - if (info.name === DefaultFactId.GEOLOCATION_LATITUDE) { - return { - provider: info.provider, - value: info.value, - name: OldDefaultInformationName.GEOLOCATION_LATITUDE, - }; - } - if (info.name === DefaultFactId.GEOLOCATION_LONGITUDE) { - return { - provider: info.provider, - value: info.value, - name: OldDefaultInformationName.GEOLOCATION_LONGITUDE, - }; - } - return info; - }), - }; -} - -const baseUrl = secret.numbersStorageBaseUrl; - -export const enum TargetProvider { - Numbers = 'Numbers', -} - -interface UserResponse { - readonly username: string; - readonly email: string; - readonly id: number; -} - -interface TokenCreateResponse { - readonly auth_token: string; -} - -interface TransactionListResponse { - readonly results: Transaction[]; -} - -interface InboxReponse { - readonly results: Transaction[]; -} - -export interface Transaction { - id: string; - asset: { - asset_file_thumbnail: string; - caption: string; - id: string; - }; - sender: string; - created_at: string; - expired: boolean; - fulfilled_at?: null | string; -} - -interface TransactionCreateResponse { - readonly id: string; - readonly asset_id: string; - readonly email: string; - readonly caption: string; -} - -interface DeviceResponse { - readonly id: string; - readonly owner: string; - readonly platform: string; - readonly device_identifier: string; - readonly registered_at: string; - readonly last_updated_at: string; -} - -const enum PrefKeys { - ENABLED = 'ENABLED', - AUTH_TOKEN = 'AUTH_TOKEN', - USERNAME = 'USERNAME', - EMAIL = 'EMAIL', -} diff --git a/src/app/services/publisher/numbers-storage/numbers-storage-publisher.ts b/src/app/services/publisher/numbers-storage/numbers-storage-publisher.ts deleted file mode 100644 index 97e208743..000000000 --- a/src/app/services/publisher/numbers-storage/numbers-storage-publisher.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { TranslocoService } from '@ngneat/transloco'; -import { NotificationService } from '../../notification/notification.service'; -import { getOldSignatures } from '../../repositories/proof/old-proof-adapter'; -import { Proof } from '../../repositories/proof/proof'; -import { Publisher } from '../publisher'; -import { - NumbersStorageApi, - TargetProvider, -} from './numbers-storage-api.service'; -import { AssetRepository } from './repositories/asset/asset-repository.service'; - -export class NumbersStoragePublisher extends Publisher { - readonly id = NumbersStoragePublisher.ID; - - constructor( - translocoService: TranslocoService, - notificationService: NotificationService, - private readonly numbersStorageApi: NumbersStorageApi, - private readonly assetRepository: AssetRepository - ) { - super(translocoService, notificationService); - } - - isEnabled$() { - return this.numbersStorageApi.isEnabled$(); - } - - async run(proof: Proof) { - const oldSignatures = getOldSignatures(proof); - const assetResponse = await this.numbersStorageApi - .createAsset$( - Object.keys(await proof.getAssets())[0], - proof, - TargetProvider.Numbers, - '', - oldSignatures, - 'capture-lite' - ) - .toPromise(); - await this.assetRepository.add(assetResponse); - return proof; - } - - static readonly ID = 'Numbers Storage'; -} diff --git a/src/app/services/publisher/numbers-storage/repositories/asset/asset-repository.service.spec.ts b/src/app/services/publisher/numbers-storage/repositories/asset/asset-repository.service.spec.ts deleted file mode 100644 index 34a33ae4b..000000000 --- a/src/app/services/publisher/numbers-storage/repositories/asset/asset-repository.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { SharedTestingModule } from '../../../../../shared/shared-testing.module'; -import { AssetRepository } from './asset-repository.service'; - -describe('AssetRepository', () => { - let service: AssetRepository; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [SharedTestingModule], - }); - service = TestBed.inject(AssetRepository); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/services/publisher/numbers-storage/repositories/asset/asset-repository.service.ts b/src/app/services/publisher/numbers-storage/repositories/asset/asset-repository.service.ts deleted file mode 100644 index 56bb6f514..000000000 --- a/src/app/services/publisher/numbers-storage/repositories/asset/asset-repository.service.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Injectable } from '@angular/core'; -import { defer } from 'rxjs'; -import { concatMap, first, map } from 'rxjs/operators'; -import { Database } from '../../../../../services/database/database.service'; -import { forkJoinWithDefault } from '../../../../../utils/rx-operators'; -import { NumbersStoragePublisher } from '../../numbers-storage-publisher'; -import { Asset } from './asset'; - -@Injectable({ - providedIn: 'root', -}) -export class AssetRepository { - private readonly id = `${NumbersStoragePublisher.ID}_asset`; - private readonly table = this.database.getTable(this.id); - - constructor(private readonly database: Database) {} - - getAll$() { - return this.table.queryAll$(); - } - - getById$(id: string) { - return this.getAll$().pipe( - map(assets => assets.find(asset => asset.id === id)) - ); - } - - async add(asset: Asset) { - return this.table.insert([asset]); - } - - remove$(asset: Asset) { - return defer(() => this.table.delete([asset])); - } - - removeAll$() { - return this.table.queryAll$().pipe( - concatMap(assets => - forkJoinWithDefault(assets.map(asset => this.remove$(asset))) - ), - first() - ); - } -} diff --git a/src/app/services/publisher/numbers-storage/repositories/asset/asset.ts b/src/app/services/publisher/numbers-storage/repositories/asset/asset.ts deleted file mode 100644 index 91728c652..000000000 --- a/src/app/services/publisher/numbers-storage/repositories/asset/asset.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Tuple } from '../../../../../services/database/table/table'; -import { - OldSignature, - SortedProofInformation, -} from '../../../../../services/repositories/proof/old-proof-adapter'; - -export interface Asset extends Tuple { - readonly id: string; - readonly proof_hash: string; - readonly owner: string; - readonly asset_file: string; - readonly information: SortedProofInformation; - readonly signature: OldSignature[]; - readonly caption: string; - readonly uploaded_at: string; - readonly is_original_owner: boolean; -} diff --git a/src/app/services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction-repository.service.ts b/src/app/services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction-repository.service.ts deleted file mode 100644 index e39c7b547..000000000 --- a/src/app/services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction-repository.service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Injectable } from '@angular/core'; -import { defer } from 'rxjs'; -import { concatMap, first } from 'rxjs/operators'; -import { Database } from '../../../../../services/database/database.service'; -import { NumbersStoragePublisher } from '../../numbers-storage-publisher'; -import { IgnoredTransaction } from './ignored-transaction'; - -@Injectable({ - providedIn: 'root', -}) -export class IgnoredTransactionRepository { - private readonly id = `${NumbersStoragePublisher.ID}_ignoredTransaction`; - private readonly table = this.database.getTable(this.id); - - constructor(private readonly database: Database) {} - - getAll$() { - return this.table.queryAll$(); - } - - add$(...ignoredTransactions: IgnoredTransaction[]) { - return defer(() => this.table.insert(ignoredTransactions)); - } - - removeAll$() { - return this.table.queryAll$().pipe( - concatMap(ignoredTransactions => - defer(() => this.table.delete(ignoredTransactions)) - ), - first() - ); - } -} diff --git a/src/app/services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction.ts b/src/app/services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction.ts deleted file mode 100644 index ab1882a6b..000000000 --- a/src/app/services/publisher/numbers-storage/repositories/ignored-transaction/ignored-transaction.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Tuple } from '../../../../../services/database/table/table'; - -export interface IgnoredTransaction extends Tuple { - readonly id: string; -} diff --git a/src/app/services/publisher/publisher.ts b/src/app/services/publisher/publisher.ts deleted file mode 100644 index 5a4dfc508..000000000 --- a/src/app/services/publisher/publisher.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { TranslocoService } from '@ngneat/transloco'; -import { Observable } from 'rxjs'; -import { NotificationService } from '../notification/notification.service'; -import { Proof } from '../repositories/proof/proof'; - -export abstract class Publisher { - abstract readonly id: string; - - constructor( - private readonly translocoService: TranslocoService, - private readonly notificationService: NotificationService - ) {} - - abstract isEnabled$(): Observable; - - async publish(proof: Proof) { - const simplifiedIdLength = 6; - const notification = this.notificationService.createNotification(); - try { - notification.notify( - this.translocoService.translate('registeringProof'), - this.translocoService.translate('message.registeringProof', { - id: (await proof.getId()).substr(0, simplifiedIdLength), - publisherName: this.id, - }) - ); - - await this.run(proof); - notification.cancel(); - return proof; - } catch (e) { - this.notificationService.error(e); - } - } - - protected abstract async run(proof: Proof): Promise; -} diff --git a/src/app/services/publisher/publishers-alert/publishers-alert.service.spec.ts b/src/app/services/publisher/publishers-alert/publishers-alert.service.spec.ts deleted file mode 100644 index 97900c40f..000000000 --- a/src/app/services/publisher/publishers-alert/publishers-alert.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { SharedTestingModule } from '../../../shared/shared-testing.module'; -import { PublishersAlert } from './publishers-alert.service'; - -describe('PublishersAlert', () => { - let service: PublishersAlert; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [SharedTestingModule], - }); - service = TestBed.inject(PublishersAlert); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/services/publisher/publishers-alert/publishers-alert.service.ts b/src/app/services/publisher/publishers-alert/publishers-alert.service.ts deleted file mode 100644 index e556fedf3..000000000 --- a/src/app/services/publisher/publishers-alert/publishers-alert.service.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Injectable } from '@angular/core'; -import { AlertController } from '@ionic/angular'; -import { TranslocoService } from '@ngneat/transloco'; -import { from, of, zip } from 'rxjs'; -import { filter, first, pluck, switchMap, toArray } from 'rxjs/operators'; -import { Proof } from '../../repositories/proof/proof'; -import { Publisher } from '../publisher'; - -@Injectable({ - providedIn: 'root', -}) -export class PublishersAlert { - private readonly publishers: Publisher[] = []; - - constructor( - private readonly alertController: AlertController, - private readonly translocoService: TranslocoService - ) {} - - addPublisher(publisher: Publisher) { - this.publishers.push(publisher); - } - - async presentOrPublish(proof: Proof) { - const publishers = await this.getEnabledPublishers$().toPromise(); - - if (publishers.length > 1) { - const alert = await this.alertController.create({ - header: this.translocoService.translate('selectAPublisher'), - inputs: publishers.map((publisher, index) => ({ - name: publisher.id, - type: 'radio', - label: publisher.id, - value: publisher.id, - checked: index === 0, - })), - buttons: [ - { - text: this.translocoService.translate('cancel'), - role: 'cancel', - }, - { - text: this.translocoService.translate('ok'), - handler: name => this.getPublisherByName(name)?.publish(proof), - }, - ], - mode: 'md', - }); - alert.present(); - } else { - return publishers[0].publish(proof); - } - } - - private getEnabledPublishers$() { - return from(this.publishers).pipe( - switchMap(publisher => - zip(of(publisher), publisher.isEnabled$().pipe(first())) - ), - filter(([_, isEnabled]) => isEnabled), - pluck(0), - toArray() - ); - } - - private getPublisherByName(name: string) { - return this.publishers.find(publisher => publisher.id === name); - } -} diff --git a/src/app/services/push-notification/push-notification.service.ts b/src/app/services/push-notification/push-notification.service.ts index 6c1107b68..84e499477 100644 --- a/src/app/services/push-notification/push-notification.service.ts +++ b/src/app/services/push-notification/push-notification.service.ts @@ -1,3 +1,4 @@ +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Capacitor, @@ -5,11 +6,10 @@ import { PushNotification, PushNotificationToken, } from '@capacitor/core'; -import { defer } from 'rxjs'; -import { concatMap, tap } from 'rxjs/operators'; +import { tap } from 'rxjs/operators'; +import { DiaBackendAssetRepository } from '../dia-backend/asset/dia-backend-asset-repository.service'; +import { DiaBackendAuthService } from '../dia-backend/auth/dia-backend-auth.service'; import { ImageStore } from '../image-store/image-store.service'; -import { NumbersStorageApi } from '../publisher/numbers-storage/numbers-storage-api.service'; -import { AssetRepository } from '../publisher/numbers-storage/repositories/asset/asset-repository.service'; import { getProof } from '../repositories/proof/old-proof-adapter'; import { ProofRepository } from '../repositories/proof/proof-repository.service'; @@ -24,9 +24,10 @@ const { Device, PushNotifications } = Plugins; }) export class PushNotificationService { constructor( - private readonly numbersStorageApi: NumbersStorageApi, + private readonly diaBackendAuthService: DiaBackendAuthService, + private readonly diaBackendAssetRepository: DiaBackendAssetRepository, private readonly proofRepository: ProofRepository, - private readonly assetRepository: AssetRepository, + private readonly httpClient: HttpClient, private readonly imageStore: ImageStore ) {} @@ -55,14 +56,7 @@ export class PushNotificationService { } private uploadToken$(token: string) { - return defer(() => Device.getInfo()).pipe( - concatMap(deviceInfo => - this.numbersStorageApi.createOrUpdateDevice$( - deviceInfo.platform, - deviceInfo.uuid, - token - ) - ), + return this.diaBackendAuthService.createDevice$(token).pipe( // tslint:disable-next-line: no-console tap(() => console.log('Token Uploaded!')) ); @@ -105,10 +99,12 @@ export class PushNotificationService { return; } - const asset = await this.numbersStorageApi.readAsset$(data.id).toPromise(); - await this.assetRepository.add(asset); - const rawImage = await this.numbersStorageApi - .getImage$(asset.asset_file) + const asset = await this.diaBackendAssetRepository + .getById$(data.id) + .toPromise(); + await this.diaBackendAssetRepository.addAssetDirectly(asset); + const rawImage = await this.httpClient + .get(asset.asset_file, { responseType: 'blob' }) .toPromise(); const proof = await getProof( this.imageStore, @@ -121,9 +117,9 @@ export class PushNotificationService { } interface NumbersStorageNotification { - app_message_type: + readonly app_message_type: | 'transaction_received' | 'transaction_accepted' | 'transaction_expired'; - id: string; + readonly id: string; } diff --git a/src/app/services/repositories/proof/proof.ts b/src/app/services/repositories/proof/proof.ts index bd4f404b5..4df28c6e8 100644 --- a/src/app/services/repositories/proof/proof.ts +++ b/src/app/services/repositories/proof/proof.ts @@ -182,11 +182,11 @@ export class Proof { } export interface Assets { - [base64: string]: AssetMeta; + readonly [base64: string]: AssetMeta; } interface IndexedAssets extends Tuple { - [index: string]: AssetMeta; + readonly [index: string]: AssetMeta; } export interface AssetMeta extends Tuple { @@ -199,11 +199,11 @@ export interface Truth extends Tuple { } interface TruthProviders extends Tuple { - [id: string]: Facts; + readonly [id: string]: Facts; } export interface Facts extends Tuple { - [id: string]: boolean | number | string | undefined; + readonly [id: string]: boolean | number | string | undefined; } export function isFacts(value: any): value is Facts { @@ -231,7 +231,7 @@ export const enum DefaultFactId { } export interface Signatures extends Tuple { - [id: string]: Signature; + readonly [id: string]: Signature; } export interface Signature extends Tuple { @@ -255,9 +255,9 @@ export function isSignature(value: any): value is Signature { } interface SerializedProof { - assets: Assets; - truth: Truth; - signatures: Signatures; + readonly assets: Assets; + readonly truth: Truth; + readonly signatures: Signatures; } export type SignedTargets = Pick; @@ -275,7 +275,7 @@ interface SignatureVerifier { } export interface IndexedProofView extends Tuple { - indexedAssets: IndexedAssets; - truth: Truth; - signatures: Signatures; + readonly indexedAssets: IndexedAssets; + readonly truth: Truth; + readonly signatures: Signatures; } diff --git a/src/app/shared/post-capture-card/post-capture-card.component.spec.ts b/src/app/shared/post-capture-card/post-capture-card.component.spec.ts index 8f69f41d2..e21921572 100644 --- a/src/app/shared/post-capture-card/post-capture-card.component.spec.ts +++ b/src/app/shared/post-capture-card/post-capture-card.component.spec.ts @@ -1,13 +1,13 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { Transaction } from '../../services/publisher/numbers-storage/numbers-storage-api.service'; -import { Asset } from '../../services/publisher/numbers-storage/repositories/asset/asset'; +import { DiaBackendAsset } from '../../services/dia-backend/asset/dia-backend-asset-repository.service'; +import { DiaBackendTransaction } from '../../services/dia-backend/transaction/dia-backend-transaction-repository.service'; import { SharedTestingModule } from '../shared-testing.module'; import { PostCaptureCardComponent } from './post-capture-card.component'; describe('PostCaptureCardComponent', () => { let component: PostCaptureCardComponent; let fixture: ComponentFixture; - const expectedAsset: Asset = { + const expectedAsset: DiaBackendAsset = { id: 'abcd-efgh-ijkl', proof_hash: 'abcdef1234567890', owner: 'me', @@ -21,9 +21,10 @@ describe('PostCaptureCardComponent', () => { uploaded_at: '', is_original_owner: true, }; - const expectedTranasction: Transaction = { + const expectedTranasction: DiaBackendTransaction = { id: '', sender: '', + receiver_email: '', asset: { asset_file_thumbnail: 'https://picsum.photos/200/300', caption: '', diff --git a/src/app/shared/post-capture-card/post-capture-card.component.ts b/src/app/shared/post-capture-card/post-capture-card.component.ts index 8bb0c1744..7f56e3607 100644 --- a/src/app/shared/post-capture-card/post-capture-card.component.ts +++ b/src/app/shared/post-capture-card/post-capture-card.component.ts @@ -1,6 +1,6 @@ import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; -import { Transaction } from '../../services/publisher/numbers-storage/numbers-storage-api.service'; -import { Asset } from '../../services/publisher/numbers-storage/repositories/asset/asset'; +import { DiaBackendAsset } from '../../services/dia-backend/asset/dia-backend-asset-repository.service'; +import { DiaBackendTransaction } from '../../services/dia-backend/transaction/dia-backend-transaction-repository.service'; import { OldDefaultInformationName } from '../../services/repositories/proof/old-proof-adapter'; @Component({ selector: 'app-post-capture-card', @@ -8,8 +8,8 @@ import { OldDefaultInformationName } from '../../services/repositories/proof/old styleUrls: ['./post-capture-card.component.scss'], }) export class PostCaptureCardComponent implements OnInit { - @Input() transaction!: Transaction; - @Input() asset!: Asset; + @Input() transaction!: DiaBackendTransaction; + @Input() asset!: DiaBackendAsset; @ViewChild('ratioImg', { static: true }) ratioImg!: ElementRef; latitude!: string; longitude!: string; diff --git a/src/app/utils/camera.ts b/src/app/utils/camera.ts index 95d21ce02..9000ef040 100644 --- a/src/app/utils/camera.ts +++ b/src/app/utils/camera.ts @@ -42,6 +42,6 @@ export async function restoreKilledCapture() { } interface Photo { - mimeType: MimeType; - base64: string; + readonly mimeType: MimeType; + readonly base64: string; } diff --git a/src/app/utils/crypto/crypto.ts b/src/app/utils/crypto/crypto.ts index abf1c5c97..d09f6ed04 100644 --- a/src/app/utils/crypto/crypto.ts +++ b/src/app/utils/crypto/crypto.ts @@ -25,8 +25,8 @@ const enum Format { } interface KeyPair { - publicKey: string; - privateKey: string; + readonly publicKey: string; + readonly privateKey: string; } export async function sha256WithString(str: string) { diff --git a/src/app/utils/immutable/immutable.ts b/src/app/utils/immutable/immutable.ts index 769c3976a..f28d9528d 100644 --- a/src/app/utils/immutable/immutable.ts +++ b/src/app/utils/immutable/immutable.ts @@ -11,5 +11,5 @@ export function sortObjectDeeplyByKey( } interface SortableMap { - [key: string]: boolean | number | string | SortableMap; + readonly [key: string]: boolean | number | string | SortableMap; } diff --git a/src/assets/i18n/en-us.json b/src/assets/i18n/en-us.json index 4f497d19f..f10cdd4ec 100644 --- a/src/assets/i18n/en-us.json +++ b/src/assets/i18n/en-us.json @@ -91,7 +91,7 @@ "emailOrPasswordIsInvalid": "The email address or password you entered is invalid.", "tooManyRetries": "You have entered the invalid email or password too many times. Please try again later.", "storingAssets": "Collecting environment variables, signing and storing assets...", - "registeringProof": "Registering asset {{ id }} to {{ publisherName }}.", + "registeringProof": "Registering the asset to Numbers DIA.", "forbiddenAllNumeric": "Cannot contain only numbers.", "isNotEmail": "Does not follow email format.", "pleaseWait": "Please wait...", diff --git a/src/assets/i18n/zh-tw.json b/src/assets/i18n/zh-tw.json index c243989d5..9ebfea7d6 100644 --- a/src/assets/i18n/zh-tw.json +++ b/src/assets/i18n/zh-tw.json @@ -91,7 +91,7 @@ "emailOrPasswordIsInvalid": "您輸入的電子郵件地址或密碼無效。", "tooManyRetries": "您輸入無效的電子郵件或密碼的次數過多。請稍後再試。", "storingAssets": "正在蒐集環境資料、簽章並保存拍攝...", - "registeringProof": "正在將影像資產 {{ hash }} 註冊至 {{ publisherName }}。", + "registeringProof": "正在將影像資產註冊至 Numbers DIA。", "forbiddenAllNumeric": "密碼不能只有數字。", "isNotEmail": "電子郵件格式不符。", "pleaseWait": "請稍候...", From 040e9a547c137cced13ede235246d796c3d97402 Mon Sep 17 00:00:00 2001 From: sddivid Date: Tue, 8 Dec 2020 12:39:38 +0800 Subject: [PATCH 09/11] Fix UI display issues from beta7.2. #297 - #289 (white margin on iOS 13.7 iPhone 6s lus) - #285 (background color of the PostCapture image) - #284 (unified font size on Capture tab) - #282 (cannot scroll on asset page) --- src/app/pages/home/asset/asset.page.html | 4 ++-- src/app/pages/home/asset/asset.page.scss | 19 ++++++++++++++++++- src/app/pages/home/home.page.scss | 2 +- .../post-capture-card.component.scss | 2 +- src/global.scss | 15 +++++++++++---- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/app/pages/home/asset/asset.page.html b/src/app/pages/home/asset/asset.page.html index f96fd6ee7..e2b1d91b5 100644 --- a/src/app/pages/home/asset/asset.page.html +++ b/src/app/pages/home/asset/asset.page.html @@ -12,8 +12,8 @@
- - + + person
{{ (asset$ | async)?.owner }}
diff --git a/src/app/pages/home/asset/asset.page.scss b/src/app/pages/home/asset/asset.page.scss index 2187360ea..a7b299fa5 100644 --- a/src/app/pages/home/asset/asset.page.scss +++ b/src/app/pages/home/asset/asset.page.scss @@ -20,7 +20,24 @@ mat-toolbar { display: flex; flex-direction: column; justify-content: center; - min-height: 100vh; + overflow: auto; +} + +.page-container { + padding-bottom: 16px; + display: flex; + flex-direction: column; + justify-content: center; + overflow: auto; +} + +.content { + flex: 1; +} + +.end-button { + flex: 1; + flex-grow: 1; } button.center { diff --git a/src/app/pages/home/home.page.scss b/src/app/pages/home/home.page.scss index c9de7c847..0dd4d2d3c 100644 --- a/src/app/pages/home/home.page.scss +++ b/src/app/pages/home/home.page.scss @@ -37,7 +37,7 @@ mat-toolbar { div.mat-title { margin: 26px 0 0; - font-size: larger; + font-size: large; } .square-image-tile { diff --git a/src/app/shared/post-capture-card/post-capture-card.component.scss b/src/app/shared/post-capture-card/post-capture-card.component.scss index 60d466cc3..67af4dee2 100644 --- a/src/app/shared/post-capture-card/post-capture-card.component.scss +++ b/src/app/shared/post-capture-card/post-capture-card.component.scss @@ -19,7 +19,7 @@ mat-card { mat-card .fixed-ratio { padding: 0; margin: 0; - background-color: #564dfc; + background-color: white; max-width: 100vw; width: 100vw; height: 100vw; diff --git a/src/global.scss b/src/global.scss index 584b9b9b5..146884212 100644 --- a/src/global.scss +++ b/src/global.scss @@ -46,14 +46,21 @@ mat-toolbar { // avoid overlapping with the status bar on most iOS devices @media (min-width: 380px) { .ios mat-toolbar { - margin-top: 30px; + padding-top: 30px; } } -// avoid overlapping with the status bar on iPhone 12 mini -@media (min-height: 810px) { +// avoid overlapping with the status bar on iPhone 6s plus/7plus/ 8 plus series +@media (min-height: 736px) and (max-width: 750px) { .ios mat-toolbar { - margin-top: 30px; + padding-top: 0; + } +} + +// avoid overlapping with the status bar on iPhone 12 mini & iPhone 12 series +@media (min-height: 750px) { + .ios mat-toolbar { + padding-top: 30px; } } From 088a52a78467395e288292d4b6cc2ab8b34451cd Mon Sep 17 00:00:00 2001 From: sddivid Date: Tue, 8 Dec 2020 14:22:48 +0800 Subject: [PATCH 10/11] Add transaction-detail page. #286 - Display different status badge to distinguish different "In Progress" activity. #199 Co-authored-by: Sean Wu --- .../home/activity/activity-routing.module.ts | 7 ++ .../pages/home/activity/activity.page.html | 10 +- .../pages/home/activity/activity.page.scss | 6 + src/app/pages/home/activity/activity.page.ts | 22 ++-- .../transaction-details-routing.module.ts | 16 +++ .../transaction-details.module.ts | 10 ++ .../transaction-details.page.html | 31 +++++ .../transaction-details.page.scss | 115 ++++++++++++++++++ .../transaction-details.page.spec.ts | 23 ++++ .../transaction-details.page.ts | 15 +++ src/assets/i18n/en-us.json | 3 + src/assets/i18n/zh-tw.json | 3 + 12 files changed, 246 insertions(+), 15 deletions(-) create mode 100644 src/app/pages/home/activity/transaction-details/transaction-details-routing.module.ts create mode 100644 src/app/pages/home/activity/transaction-details/transaction-details.module.ts create mode 100644 src/app/pages/home/activity/transaction-details/transaction-details.page.html create mode 100644 src/app/pages/home/activity/transaction-details/transaction-details.page.scss create mode 100644 src/app/pages/home/activity/transaction-details/transaction-details.page.spec.ts create mode 100644 src/app/pages/home/activity/transaction-details/transaction-details.page.ts diff --git a/src/app/pages/home/activity/activity-routing.module.ts b/src/app/pages/home/activity/activity-routing.module.ts index d9b785358..aa64348a2 100644 --- a/src/app/pages/home/activity/activity-routing.module.ts +++ b/src/app/pages/home/activity/activity-routing.module.ts @@ -7,6 +7,13 @@ const routes: Routes = [ path: '', component: ActivityPage, }, + { + path: 'transaction-details', + loadChildren: () => + import('./transaction-details/transaction-details.module').then( + m => m.TransactionDetailsPageModule + ), + }, ]; @NgModule({ diff --git a/src/app/pages/home/activity/activity.page.html b/src/app/pages/home/activity/activity.page.html index 89bf5460d..7c740c366 100644 --- a/src/app/pages/home/activity/activity.page.html +++ b/src/app/pages/home/activity/activity.page.html @@ -10,16 +10,12 @@ - +
{{ activity.asset.id }}
{{ activity.created_at | date: 'short' }}
-
diff --git a/src/app/pages/home/activity/activity.page.scss b/src/app/pages/home/activity/activity.page.scss index 3e9e98a4a..dcfee7d88 100644 --- a/src/app/pages/home/activity/activity.page.scss +++ b/src/app/pages/home/activity/activity.page.scss @@ -49,6 +49,12 @@ mat-toolbar { border-color: deepskyblue; } + button.waitingToBeAccepted { + color: white; + border-color: deepskyblue; + background-color: deepskyblue; + } + button.returned { color: white; border-color: var(--ion-color-danger); diff --git a/src/app/pages/home/activity/activity.page.ts b/src/app/pages/home/activity/activity.page.ts index 1d2f54707..17df72b80 100644 --- a/src/app/pages/home/activity/activity.page.ts +++ b/src/app/pages/home/activity/activity.page.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { UntilDestroy } from '@ngneat/until-destroy'; -import { map, pluck } from 'rxjs/operators'; +import { concatMap, pluck } from 'rxjs/operators'; import { DiaBackendAuthService } from '../../../services/dia-backend/auth/dia-backend-auth.service'; import { DiaBackendTransaction, @@ -19,11 +19,13 @@ export class ActivityPage { .getAll$() .pipe( pluck('results'), - map(activities => - activities.map(activity => ({ - ...activity, - status: this.getStatus(activity), - })) + concatMap(activities => + Promise.all( + activities.map(async activity => ({ + ...activity, + status: await this.getStatus(activity), + })) + ) ) ); @@ -38,7 +40,10 @@ export class ActivityPage { return Status.Returned; } if (!activity.fulfilled_at) { - return Status.InProgress; + if (activity.receiver_email === email) { + return Status.InProgress; + } + return Status.waitingToBeAccepted; } if (activity.sender === email) { return Status.Delivered; @@ -47,7 +52,8 @@ export class ActivityPage { } } -enum Status { +export enum Status { + waitingToBeAccepted = 'waitingToBeAccepted', InProgress = 'inProgress', Returned = 'returned', Delivered = 'delivered', diff --git a/src/app/pages/home/activity/transaction-details/transaction-details-routing.module.ts b/src/app/pages/home/activity/transaction-details/transaction-details-routing.module.ts new file mode 100644 index 000000000..b662d9605 --- /dev/null +++ b/src/app/pages/home/activity/transaction-details/transaction-details-routing.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { TransactionDetailsPage } from './transaction-details.page'; + +const routes: Routes = [ + { + path: '', + component: TransactionDetailsPage, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class TransactionDetailsPageRoutingModule {} diff --git a/src/app/pages/home/activity/transaction-details/transaction-details.module.ts b/src/app/pages/home/activity/transaction-details/transaction-details.module.ts new file mode 100644 index 000000000..4c6812e69 --- /dev/null +++ b/src/app/pages/home/activity/transaction-details/transaction-details.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { SharedModule } from '../../../../shared/shared.module'; +import { TransactionDetailsPageRoutingModule } from './transaction-details-routing.module'; +import { TransactionDetailsPage } from './transaction-details.page'; + +@NgModule({ + imports: [SharedModule, TransactionDetailsPageRoutingModule], + declarations: [TransactionDetailsPage], +}) +export class TransactionDetailsPageModule {} diff --git a/src/app/pages/home/activity/transaction-details/transaction-details.page.html b/src/app/pages/home/activity/transaction-details/transaction-details.page.html new file mode 100644 index 000000000..013430fb6 --- /dev/null +++ b/src/app/pages/home/activity/transaction-details/transaction-details.page.html @@ -0,0 +1,31 @@ + + + {{ t('transactionDetails') }} + +
+

{{ (details$ | async)?.created_at | date: 'short' }}

+ + + {{ (details$ | async)?.asset.id }} + + + + + {{ t('sentFrom') }} : {{ (details$ | async)?.sender }} + {{ t('receiver') }} : + {{ (details$ | async)?.receiver_email }} + + +
diff --git a/src/app/pages/home/activity/transaction-details/transaction-details.page.scss b/src/app/pages/home/activity/transaction-details/transaction-details.page.scss new file mode 100644 index 000000000..40b1a1188 --- /dev/null +++ b/src/app/pages/home/activity/transaction-details/transaction-details.page.scss @@ -0,0 +1,115 @@ +mat-toolbar { + color: white; + + .mat-icon { + color: white; + } + + span { + color: white; + padding-right: 40px; + } +} + +.page-content { + padding: 0 0 16px; + + .top-time { + width: 90vw; + margin-top: 16px; + margin-left: 5vw; + margin-right: 5vw; + text-align: center; + } + + button.send-button { + width: 90vw; + margin-top: 16px; + margin-left: 5vw; + margin-right: 5vw; + + .send-button-wrapper { + display: flex; + align-items: center; + + .avatar { + height: 32px; + width: 32px; + margin: 8px; + border-radius: 50%; + flex-shrink: 0; + object-fit: cover; + background-image: url('https://gravatar.com/avatar/8db683da3c728970bb776f6cc4048fd2?s=400&d=mp&r=x'); + background-size: cover; + } + } + } + + button { + flex-shrink: 0; + } + + button.accepted { + color: white; + border-color: #4cd964; + background-color: #4cd964; + } + + button.delivered { + color: mediumseagreen; + border-color: mediumseagreen; + } + + button.inProgress { + color: deepskyblue; + border-color: deepskyblue; + } + + button.waitingToBeAccepted { + color: white; + border-color: deepskyblue; + background-color: deepskyblue; + } + + button.returned { + color: white; + border-color: var(--ion-color-danger); + background-color: #e31587; + } +} + +.column { + display: flex; + flex-direction: column; +} + +.row { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.transaction-card { + width: 90vw; + margin-top: 16px; + margin-left: 5vw; + margin-right: 5vw; +} + +mat-card img { + width: 50%; + margin: 10% 25%; +} + +form mat-form-field { + width: 100%; + height: 100%; + + textarea { + overflow: hidden; + } +} + +.spacer { + flex: 1 1 auto; +} diff --git a/src/app/pages/home/activity/transaction-details/transaction-details.page.spec.ts b/src/app/pages/home/activity/transaction-details/transaction-details.page.spec.ts new file mode 100644 index 000000000..c248ce362 --- /dev/null +++ b/src/app/pages/home/activity/transaction-details/transaction-details.page.spec.ts @@ -0,0 +1,23 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { SharedTestingModule } from '../../../../shared/shared-testing.module'; +import { TransactionDetailsPage } from './transaction-details.page'; + +describe('TransactionDetailsPage', () => { + let component: TransactionDetailsPage; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [TransactionDetailsPage], + imports: [SharedTestingModule], + }).compileComponents(); + + fixture = TestBed.createComponent(TransactionDetailsPage); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/home/activity/transaction-details/transaction-details.page.ts b/src/app/pages/home/activity/transaction-details/transaction-details.page.ts new file mode 100644 index 000000000..5f5108210 --- /dev/null +++ b/src/app/pages/home/activity/transaction-details/transaction-details.page.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { UntilDestroy } from '@ngneat/until-destroy'; +import { map } from 'rxjs/operators'; +@UntilDestroy({ checkProperties: true }) +@Component({ + selector: 'app-transaction-details', + templateUrl: './transaction-details.page.html', + styleUrls: ['./transaction-details.page.scss'], +}) +export class TransactionDetailsPage { + details$ = this.route.paramMap.pipe(map(() => history.state)); + + constructor(private readonly route: ActivatedRoute) {} +} diff --git a/src/assets/i18n/en-us.json b/src/assets/i18n/en-us.json index f10cdd4ec..eab8a1cb0 100644 --- a/src/assets/i18n/en-us.json +++ b/src/assets/i18n/en-us.json @@ -71,11 +71,14 @@ "ignore": "Ignore", "accept": "Accept", "activity": "Activity", + "transactionDetails": "Activity Details", "accepted": "Accepted", "delivered": "Delivered", "returned": "Returned", "inProgress": "In Progress", + "waitingToBeAccepted": "Waiting to be Accepted", "sentFrom": "Sent from", + "receiver": "Receiver", ".message": "Message", "location": "Location", "message": { diff --git a/src/assets/i18n/zh-tw.json b/src/assets/i18n/zh-tw.json index 9ebfea7d6..f29a5b6a0 100644 --- a/src/assets/i18n/zh-tw.json +++ b/src/assets/i18n/zh-tw.json @@ -71,11 +71,14 @@ "ignore": "忽略", "accept": "接收", "activity": "活動紀錄", + "transactionDetails": "活動資訊", "accepted": "已接收", "delivered": "已送出", "inProgress": "等待中", + "waitingToBeAccepted": "等待對方接收中", "returned": "已退回", "sentFrom": "發送自", + "receiver": "收件人", ".message": "訊息", "location": "位置", "message": { From 983a1a2d4b6325faba3054a0f3d3f5f4c36e047b Mon Sep 17 00:00:00 2001 From: Sean Wu Date: Tue, 8 Dec 2020 14:29:25 +0800 Subject: [PATCH 11/11] Bump version to 0.10.0. --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ android/app/build.gradle | 4 ++-- package-lock.json | 2 +- package.json | 2 +- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9308cc5..7fa8b259c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.10.0 - 2020-12-08 + +### Added + +- Add transaction-details page. #286 +- Display different status badge to distinguish different "In Progress" activity. #199 + +### Changed + +#### Code Quality + +- Rewrite the tests of `NotificationService` with `MockLocalNotificationsPlugin`. +- Rewrite tests for `PreferenceManager` service by mocking Capacitor `Storage` plugin. +- Rewrite the interface of `ConfirmAlert` with tests. +- Add tests for `LanguageService`. +- Change the pre-release branch to master. +- Rename `NumbersStorageBackend` with `DiaBackend`. +- Implement `NotificationService.notifyOnGoing` method. Close #254. +- Apply on-going notification to `CollectorService.runAndStore` method. +- Apply on-going notification to `DiaBackendAssetRepository.add` method. +- Reimplement simplified `IgnoredTransactionRepository`. +- Extract `/api/**/transactions` endpoints to standalone service. +- Extract `/api/**/assets` endpoints to standalone service. +- Extract `/auth` endpoints to standalone service with tests. +- Add readonly modifier to most dictionary interface. +- Improve the import location for `secret.ts` from set-secret preconfig. + +### Fixed + +- Fix white margin on iOS 13.7 iPhone 6s Plus. #289 +- Fix background color of the PostCapture image. #285 +- Fix unified font size on Capture tab. #284 +- Fix cannot scroll on asset page. #282 + ## 0.9.4 - 2020-12-04 ### Changed diff --git a/android/app/build.gradle b/android/app/build.gradle index 2311ec16e..84b450058 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -6,8 +6,8 @@ android { applicationId "io.numbersprotocol.capturelite" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 47 - versionName "0.9.4" + versionCode 48 + versionName "0.10.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/package-lock.json b/package-lock.json index 777a2fffb..1c5c3023e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "capture-lite", - "version": "0.9.4", + "version": "0.10.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4925b42ef..9a2896a39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "capture-lite", - "version": "0.9.4", + "version": "0.10.0", "author": "numbersprotocol", "homepage": "https://numbersprotocol.io/", "scripts": {