diff --git a/src/app/features/home/capture-tab/capture-tab.component.spec.ts b/src/app/features/home/capture-tab/capture-tab.component.spec.ts index 5952e9911..b4446838c 100644 --- a/src/app/features/home/capture-tab/capture-tab.component.spec.ts +++ b/src/app/features/home/capture-tab/capture-tab.component.spec.ts @@ -1,4 +1,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { DiaBackendAuthService } from '../../../shared/dia-backend/auth/dia-backend-auth.service'; +import { DiaBackendTransactionRepository } from '../../../shared/dia-backend/transaction/dia-backend-transaction-repository.service'; import { SharedTestingModule } from '../../../shared/shared-testing.module'; import { CaptureTabComponent } from './capture-tab.component'; import { UploadingBarComponent } from './uploading-bar/uploading-bar.component'; @@ -7,10 +10,35 @@ describe('CaptureTabComponent', () => { let component: CaptureTabComponent; let fixture: ComponentFixture; + const diaBackendAuthServiceStub = { + username$: of(''), + email$: of(''), + profileName$: of(''), + profile$: of({ + description: '', + profile_background_thumbnail: '', + }), + syncUser$: jasmine.createSpy().and.returnValue(of(void 0)), + }; + + const diaBackendTransactionRepositoryStub = { + inbox$: of({ count: 0 }), + }; + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [CaptureTabComponent, UploadingBarComponent], imports: [SharedTestingModule], + providers: [ + { + provide: DiaBackendAuthService, + useValue: diaBackendAuthServiceStub, + }, + { + provide: DiaBackendTransactionRepository, + useValue: diaBackendTransactionRepositoryStub, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(CaptureTabComponent); diff --git a/src/app/features/home/capture-tab/capture-tab.component.ts b/src/app/features/home/capture-tab/capture-tab.component.ts index 258857222..de0ac0e7d 100644 --- a/src/app/features/home/capture-tab/capture-tab.component.ts +++ b/src/app/features/home/capture-tab/capture-tab.component.ts @@ -1,5 +1,10 @@ import { formatDate, KeyValue } from '@angular/common'; -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnInit, +} from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { Browser } from '@capacitor/browser'; @@ -48,6 +53,7 @@ import { PrefetchingDialogComponent } from '../onboarding/prefetching-dialog/pre selector: 'app-capture-tab', templateUrl: './capture-tab.component.html', styleUrls: ['./capture-tab.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CaptureTabComponent implements OnInit { /** @@ -174,7 +180,10 @@ export class CaptureTabComponent implements OnInit { private initSegmentListener() { this.captureTabService.segment$ .pipe( - tap(segment => (this.segment = segment)), + tap(segment => { + this.segment = segment; + this.changeDetectorRef.markForCheck(); + }), untilDestroyed(this) ) .subscribe(); diff --git a/src/app/features/home/collection-tab/collection-tab/collection-tab.component.spec.ts b/src/app/features/home/collection-tab/collection-tab/collection-tab.component.spec.ts index 55c543502..3b301c827 100644 --- a/src/app/features/home/collection-tab/collection-tab/collection-tab.component.spec.ts +++ b/src/app/features/home/collection-tab/collection-tab/collection-tab.component.spec.ts @@ -1,16 +1,37 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { BehaviorSubject, of } from 'rxjs'; +import { DiaBackendAuthService } from '../../../../shared/dia-backend/auth/dia-backend-auth.service'; +import { IframeService } from '../../../../shared/iframe/iframe.service'; import { SharedTestingModule } from '../../../../shared/shared-testing.module'; - import { CollectionTabComponent } from './collection-tab.component'; describe('CollectionTabComponent', () => { let component: CollectionTabComponent; let fixture: ComponentFixture; + const diaBackendAuthServiceStub = { + cachedQueryJWTToken$: of({ access: 'token', refresh: 'refresh' }), + }; + + const iframeServiceStub = { + collectionTabRefreshRequested$: new BehaviorSubject(undefined), + collectionTabIframeNavigateBack$: of(void 0), + }; + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [CollectionTabComponent], imports: [SharedTestingModule], + providers: [ + { + provide: DiaBackendAuthService, + useValue: diaBackendAuthServiceStub, + }, + { + provide: IframeService, + useValue: iframeServiceStub, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(CollectionTabComponent); diff --git a/src/app/features/home/home.page.spec.ts b/src/app/features/home/home.page.spec.ts index 8f77adb31..86b167244 100644 --- a/src/app/features/home/home.page.spec.ts +++ b/src/app/features/home/home.page.spec.ts @@ -1,5 +1,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { Router } from '@angular/router'; +import { of } from 'rxjs'; +import { DiaBackendAuthService } from '../../shared/dia-backend/auth/dia-backend-auth.service'; import { SharedTestingModule } from '../../shared/shared-testing.module'; import { CaptureTabComponent } from './capture-tab/capture-tab.component'; import { UploadingBarComponent } from './capture-tab/uploading-bar/uploading-bar.component'; @@ -10,6 +12,17 @@ describe('HomePage', () => { let component: HomePage; let fixture: ComponentFixture; + const diaBackendAuthServiceStub = { + username$: of(''), + email$: of(''), + profileName$: of(''), + profile$: of({ + description: '', + profile_background_thumbnail: '', + }), + syncUser$: jasmine.createSpy().and.returnValue(of(void 0)), + }; + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ @@ -19,6 +32,12 @@ describe('HomePage', () => { UploadingBarComponent, ], imports: [SharedTestingModule], + providers: [ + { + provide: DiaBackendAuthService, + useValue: diaBackendAuthServiceStub, + }, + ], }).compileComponents(); const router = TestBed.inject(Router); diff --git a/src/app/features/invitation/invitation.page.spec.ts b/src/app/features/invitation/invitation.page.spec.ts index 99b2d567f..a9c1c2c7d 100644 --- a/src/app/features/invitation/invitation.page.spec.ts +++ b/src/app/features/invitation/invitation.page.spec.ts @@ -1,5 +1,8 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { IonicModule } from '@ionic/angular'; +import { of } from 'rxjs'; +import { DiaBackendAuthService } from '../../shared/dia-backend/auth/dia-backend-auth.service'; +import { ShareService } from '../../shared/share/share.service'; import { SharedTestingModule } from '../../shared/shared-testing.module'; import { InvitationPage } from './invitation.page'; @@ -7,10 +10,30 @@ describe('InvitationPage', () => { let component: InvitationPage; let fixture: ComponentFixture; + const diaBackendAuthServiceStub = { + referralCode$: of(''), + getReferralCode: jasmine.createSpy().and.resolveTo(''), + syncUser$: jasmine.createSpy().and.returnValue(of(void 0)), + }; + + const shareServiceStub = { + shareReferralCode: jasmine.createSpy(), + }; + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [InvitationPage], imports: [IonicModule.forRoot(), SharedTestingModule], + providers: [ + { + provide: DiaBackendAuthService, + useValue: diaBackendAuthServiceStub, + }, + { + provide: ShareService, + useValue: shareServiceStub, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(InvitationPage); diff --git a/src/app/features/settings/settings.page.spec.ts b/src/app/features/settings/settings.page.spec.ts index 532aa9b9d..cc3aaa5e1 100644 --- a/src/app/features/settings/settings.page.spec.ts +++ b/src/app/features/settings/settings.page.spec.ts @@ -1,4 +1,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { CaptureAppWebCryptoApiSignatureProvider } from '../../shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service'; +import { DiaBackendAuthService } from '../../shared/dia-backend/auth/dia-backend-auth.service'; import { SharedTestingModule } from '../../shared/shared-testing.module'; import { SettingsPage } from './settings.page'; @@ -6,10 +9,30 @@ describe('SettingsPage', () => { let component: SettingsPage; let fixture: ComponentFixture; + const diaBackendAuthServiceStub = { + email$: of('tester@example.com'), + emailVerified$: of(false), + syncUser$: jasmine.createSpy().and.returnValue(of(void 0)), + }; + + const signatureProviderStub = { + privateKey$: of('private-key-for-test'), + }; + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [SettingsPage], imports: [SharedTestingModule], + providers: [ + { + provide: DiaBackendAuthService, + useValue: diaBackendAuthServiceStub, + }, + { + provide: CaptureAppWebCryptoApiSignatureProvider, + useValue: signatureProviderStub, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(SettingsPage); diff --git a/src/app/features/wallets/wallets.page.spec.ts b/src/app/features/wallets/wallets.page.spec.ts index efa9740a7..da24d6fa9 100644 --- a/src/app/features/wallets/wallets.page.spec.ts +++ b/src/app/features/wallets/wallets.page.spec.ts @@ -1,5 +1,9 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { IonicModule } from '@ionic/angular'; +import { BehaviorSubject, of } from 'rxjs'; +import { CaptureAppWebCryptoApiSignatureProvider } from '../../shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service'; +import { DiaBackendAuthService } from '../../shared/dia-backend/auth/dia-backend-auth.service'; +import { DiaBackendWalletService } from '../../shared/dia-backend/wallet/dia-backend-wallet.service'; import { SharedTestingModule } from '../../shared/shared-testing.module'; import { WalletsPage } from './wallets.page'; @@ -7,10 +11,39 @@ describe('WalletsPage', () => { let component: WalletsPage; let fixture: ComponentFixture; + const diaBackendAuthServiceStub = { + cachedQueryJWTToken$: of({ access: 'token', refresh: 'refresh' }), + }; + + const diaBackendWalletServiceStub = { + assetWalletAddr$: of('asset-wallet-address'), + networkConnected$: of(true), + reloadWallet$: new BehaviorSubject(false), + }; + + const signatureProviderStub = { + publicKey$: of('public-key'), + privateKey$: of('private-key'), + }; + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [WalletsPage], imports: [IonicModule.forRoot(), SharedTestingModule], + providers: [ + { + provide: DiaBackendAuthService, + useValue: diaBackendAuthServiceStub, + }, + { + provide: DiaBackendWalletService, + useValue: diaBackendWalletServiceStub, + }, + { + provide: CaptureAppWebCryptoApiSignatureProvider, + useValue: signatureProviderStub, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(WalletsPage); diff --git a/src/app/shared/dia-backend/auth/dia-backend-auth.service.ts b/src/app/shared/dia-backend/auth/dia-backend-auth.service.ts index 90e077913..d0aeaf113 100644 --- a/src/app/shared/dia-backend/auth/dia-backend-auth.service.ts +++ b/src/app/shared/dia-backend/auth/dia-backend-auth.service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Device } from '@capacitor/device'; import { Preferences } from '@capacitor/preferences'; -import { isEqual, reject } from 'lodash-es'; +import { isEqual } from 'lodash-es'; import { Observable, ReplaySubject, @@ -435,15 +435,9 @@ export class DiaBackendAuthService { } private async getToken() { - return new Promise(resolve => { - this.preferences.getString(PrefKeys.TOKEN).then(token => { - if (token.length !== 0) { - resolve(token); - } else { - reject(new Error('Cannot get DIA backend token which is empty.')); - } - }); - }); + const token = await this.preferences.getString(PrefKeys.TOKEN); + if (!token) throw new Error('Cannot get DIA backend token which is empty.'); + return token; } private async setToken(value: string) { diff --git a/src/app/shared/media/media-store/media-store.service.ts b/src/app/shared/media/media-store/media-store.service.ts index c6a267043..218a37b89 100644 --- a/src/app/shared/media/media-store/media-store.service.ts +++ b/src/app/shared/media/media-store/media-store.service.ts @@ -100,10 +100,14 @@ export class MediaStore { private async _write(index: string, base64: string, mimeType: MimeType) { await this.initialize(); + // Perform expensive blob conversion before acquiring the lock so concurrent + // reads are not blocked during the conversion of large files. + const blob = Capacitor.isNativePlatform() + ? await base64ToBlob(base64, mimeType) + : undefined; return this.mutex.runExclusive(async () => { const mediaExtension = await this.setMediaExtension(index, mimeType); - if (Capacitor.isNativePlatform()) { - const blob = await base64ToBlob(base64, mimeType); + if (blob !== undefined) { await write_blob({ directory: this.directory, path: `${this.rootDir}/${index}.${mediaExtension.extension}`, diff --git a/src/app/shared/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.ts b/src/app/shared/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.ts index dc1007ece..a90e5a3d7 100644 --- a/src/app/shared/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.ts +++ b/src/app/shared/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.ts @@ -64,18 +64,20 @@ export class CapacitorStoragePreferences { } private async initializeValue(key: string, defaultValue: SupportedTypes) { - if (this.subjects.has(key)) { - const subject$ = this.subjects.get(key); - if (subject$?.value === undefined) { - subject$?.next(defaultValue); + return this.mutex.runExclusive(async () => { + if (this.subjects.has(key)) { + const subject$ = this.subjects.get(key); + if (subject$?.value === undefined) { + subject$?.next(defaultValue); + } + return; } - return; - } - const value = await this.loadValue(key, defaultValue); - this.subjects.set( - key, - new BehaviorSubject(value) - ); + const value = await this.loadValue(key, defaultValue); + this.subjects.set( + key, + new BehaviorSubject(value) + ); + }); } private async loadValue(key: string, defaultValue: SupportedTypes) { diff --git a/src/app/shared/repositories/proof/proof.ts b/src/app/shared/repositories/proof/proof.ts index 7646c9778..a178a3f9f 100644 --- a/src/app/shared/repositories/proof/proof.ts +++ b/src/app/shared/repositories/proof/proof.ts @@ -56,6 +56,8 @@ export class Proof { */ cameraSource: CameraSource = CameraSource.Camera; + private _timestamp?: number; + /** * Used to sort the assets in the VERIFIED tab either by timestamp or uploadedAt (if available). * Since timestamp getter now ensures values are always in milliseconds, we don't need @@ -79,15 +81,22 @@ export class Proof { * * This getter ensures timestamps are always returned in milliseconds regardless of * how they're stored (seconds or milliseconds). + * + * The computed value is cached because `truth` is a readonly reference and + * `truth.timestamp` is treated as immutable after Proof construction. */ get timestamp() { - const MILLISECONDS_PER_SECOND = 1000; - const MILLISECONDS_THRESHOLD = 10000000000; // 10^10, timestamps after March 2001 - - // Convert to milliseconds if the timestamp is in seconds - return this.truth.timestamp > MILLISECONDS_THRESHOLD - ? this.truth.timestamp - : this.truth.timestamp * MILLISECONDS_PER_SECOND; + if (this._timestamp === undefined) { + const MILLISECONDS_PER_SECOND = 1000; + const MILLISECONDS_THRESHOLD = 10000000000; // 10^10, timestamps after March 2001 + + // Convert to milliseconds if the timestamp is in seconds + this._timestamp = + this.truth.timestamp > MILLISECONDS_THRESHOLD + ? this.truth.timestamp + : this.truth.timestamp * MILLISECONDS_PER_SECOND; + } + return this._timestamp; } get deviceName() {