Skip to content
Merged
28 changes: 28 additions & 0 deletions src/app/features/home/capture-tab/capture-tab.component.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -7,10 +10,35 @@ describe('CaptureTabComponent', () => {
let component: CaptureTabComponent;
let fixture: ComponentFixture<CaptureTabComponent>;

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);
Expand Down
13 changes: 11 additions & 2 deletions src/app/features/home/capture-tab/capture-tab.component.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CollectionTabComponent>;

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);
Expand Down
19 changes: 19 additions & 0 deletions src/app/features/home/home.page.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,6 +12,17 @@ describe('HomePage', () => {
let component: HomePage;
let fixture: ComponentFixture<HomePage>;

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: [
Expand All @@ -19,6 +32,12 @@ describe('HomePage', () => {
UploadingBarComponent,
],
imports: [SharedTestingModule],
providers: [
{
provide: DiaBackendAuthService,
useValue: diaBackendAuthServiceStub,
},
],
}).compileComponents();

const router = TestBed.inject(Router);
Expand Down
23 changes: 23 additions & 0 deletions src/app/features/invitation/invitation.page.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
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';

describe('InvitationPage', () => {
let component: InvitationPage;
let fixture: ComponentFixture<InvitationPage>;

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);
Expand Down
23 changes: 23 additions & 0 deletions src/app/features/settings/settings.page.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,38 @@
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';

describe('SettingsPage', () => {
let component: SettingsPage;
let fixture: ComponentFixture<SettingsPage>;

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);
Expand Down
33 changes: 33 additions & 0 deletions src/app/features/wallets/wallets.page.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,49 @@
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';

describe('WalletsPage', () => {
let component: WalletsPage;
let fixture: ComponentFixture<WalletsPage>;

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);
Expand Down
14 changes: 4 additions & 10 deletions src/app/shared/dia-backend/auth/dia-backend-auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -435,15 +435,9 @@ export class DiaBackendAuthService {
}

private async getToken() {
return new Promise<string>(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) {
Expand Down
8 changes: 6 additions & 2 deletions src/app/shared/media/media-store/media-store.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SupportedTypes | undefined>(value)
);
const value = await this.loadValue(key, defaultValue);
this.subjects.set(
key,
new BehaviorSubject<SupportedTypes | undefined>(value)
);
});
}

private async loadValue(key: string, defaultValue: SupportedTypes) {
Expand Down
23 changes: 16 additions & 7 deletions src/app/shared/repositories/proof/proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand Down
Loading