diff --git a/src/app/features/settings/email-verification/email-verification.page.html b/src/app/features/settings/email-verification/email-verification.page.html
index 8bf6cb4fe..01c31eba7 100644
--- a/src/app/features/settings/email-verification/email-verification.page.html
+++ b/src/app/features/settings/email-verification/email-verification.page.html
@@ -1,7 +1,5 @@
diff --git a/src/app/features/settings/go-pro/go-pro.page.html b/src/app/features/settings/go-pro/go-pro.page.html
index f31910d0c..d79a3f892 100644
--- a/src/app/features/settings/go-pro/go-pro.page.html
+++ b/src/app/features/settings/go-pro/go-pro.page.html
@@ -1,7 +1,5 @@
-
+
GoPro Setup
diff --git a/src/app/features/settings/go-pro/services/go-pro-media.service.ts b/src/app/features/settings/go-pro/services/go-pro-media.service.ts
index 83a8de1c0..4f4fec6e9 100644
--- a/src/app/features/settings/go-pro/services/go-pro-media.service.ts
+++ b/src/app/features/settings/go-pro/services/go-pro-media.service.ts
@@ -3,6 +3,7 @@ import { Inject, Injectable } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import '@capacitor-community/http';
import { Http } from '@capacitor-community/http';
+import { CameraSource } from '@capacitor/camera';
import { Capacitor } from '@capacitor/core';
import {
Directory as FilesystemDirectory,
@@ -84,7 +85,11 @@ export class GoProMediaService {
const mimeType = urlIsImage(mediaFile.url) ? 'image/jpeg' : 'video/mp4';
isDownloaded = true;
- await this.captureService.capture({ base64, mimeType });
+ await this.captureService.capture({
+ base64,
+ mimeType,
+ source: CameraSource.Camera,
+ });
isCaptured = true;
// delete temp downloaded file
diff --git a/src/app/features/settings/settings.page.html b/src/app/features/settings/settings.page.html
index 6a70e85c0..0b47e5a0f 100644
--- a/src/app/features/settings/settings.page.html
+++ b/src/app/features/settings/settings.page.html
@@ -1,12 +1,5 @@
-
+
{{ t('settings.settings') }}
diff --git a/src/app/features/settings/settings.page.scss b/src/app/features/settings/settings.page.scss
index 52b0daebc..13f383065 100644
--- a/src/app/features/settings/settings.page.scss
+++ b/src/app/features/settings/settings.page.scss
@@ -8,14 +8,6 @@ mat-toolbar {
text-align: center;
color: white;
}
-
- .capture-rebranded-button {
- margin: 0 4px;
- background: #ffffff40 !important; /* stylelint-disable-line declaration-no-important */
- color: white !important; /* stylelint-disable-line declaration-no-important */
- backdrop-filter: blur(4px);
- box-shadow: none;
- }
}
ion-select {
diff --git a/src/app/features/settings/settings.page.ts b/src/app/features/settings/settings.page.ts
index 07ea54409..9d67a36da 100644
--- a/src/app/features/settings/settings.page.ts
+++ b/src/app/features/settings/settings.page.ts
@@ -5,7 +5,7 @@ import { Clipboard } from '@capacitor/clipboard';
import { IonModal } from '@ionic/angular';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
-import { defer, EMPTY, Subject } from 'rxjs';
+import { EMPTY, Subject, defer } from 'rxjs';
import {
catchError,
concatMapTo,
@@ -18,7 +18,7 @@ import {
} from 'rxjs/operators';
import { BlockingActionService } from '../../shared/blocking-action/blocking-action.service';
import { CapacitorFactsProvider } from '../../shared/collector/facts/capacitor-facts-provider/capacitor-facts-provider.service';
-import { WebCryptoApiSignatureProvider } from '../../shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service';
+import { CaptureAppWebCryptoApiSignatureProvider } from '../../shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service';
import { ConfirmAlert } from '../../shared/confirm-alert/confirm-alert.service';
import { Database } from '../../shared/database/database.service';
import { DiaBackendAuthService } from '../../shared/dia-backend/auth/dia-backend-auth.service';
@@ -62,7 +62,8 @@ export class SettingsPage {
private readonly requiredClicks = 7;
showHiddenOption = false;
- private readonly privateKey$ = this.webCryptoApiSignatureProvider.privateKey$;
+ private readonly privateKey$ =
+ this.capAppWebCryptoApiSignatureProvider.privateKey$;
readonly privateKeyTruncated$ = this.privateKey$.pipe(
map(key => {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
@@ -86,7 +87,7 @@ export class SettingsPage {
private readonly versionService: VersionService,
private readonly router: Router,
private readonly route: ActivatedRoute,
- private readonly webCryptoApiSignatureProvider: WebCryptoApiSignatureProvider,
+ private readonly capAppWebCryptoApiSignatureProvider: CaptureAppWebCryptoApiSignatureProvider,
private readonly snackBar: MatSnackBar
) {}
@@ -158,14 +159,10 @@ export class SettingsPage {
.subscribe();
}
- /**
- * // TODO: Integrate Storage Backend delete function after it's ready.
- * Delete user account from Storage Backend.
- */
async deleteAccount() {
const email: string = await this.diaBackendAuthService.getEmail();
- const action$ = this.diaBackendAuthService.deleteUser$(email).pipe(
+ const action$ = this.diaBackendAuthService.deleteAccount$(email).pipe(
// logout
concatMapTo(defer(() => this.mediaStore.clear())),
concatMapTo(defer(() => this.database.clear())),
diff --git a/src/app/features/settings/user-guide/user-guide.page.html b/src/app/features/settings/user-guide/user-guide.page.html
index f7fff5225..d2bfa447d 100644
--- a/src/app/features/settings/user-guide/user-guide.page.html
+++ b/src/app/features/settings/user-guide/user-guide.page.html
@@ -1,7 +1,5 @@
-
+
User guide Preferences
diff --git a/src/app/features/terms-of-use/terms-of-use.page.html b/src/app/features/terms-of-use/terms-of-use.page.html
index b062c5a41..daf995a1b 100644
--- a/src/app/features/terms-of-use/terms-of-use.page.html
+++ b/src/app/features/terms-of-use/terms-of-use.page.html
@@ -1,13 +1,6 @@
-
+
{{ 'termsOfUse' | transloco }}
diff --git a/src/app/features/terms-of-use/terms-of-use.page.scss b/src/app/features/terms-of-use/terms-of-use.page.scss
index 044c5df02..447ed91db 100644
--- a/src/app/features/terms-of-use/terms-of-use.page.scss
+++ b/src/app/features/terms-of-use/terms-of-use.page.scss
@@ -8,14 +8,6 @@ mat-toolbar {
text-align: center;
color: white;
}
-
- .capture-rebranded-button {
- margin: 0 4px;
- background: #ffffff40 !important; /* stylelint-disable-line declaration-no-important */
- color: white !important; /* stylelint-disable-line declaration-no-important */
- backdrop-filter: blur(4px);
- box-shadow: none;
- }
}
.no-network-text {
diff --git a/src/app/features/wallets/buy-num/buy-num.page.html b/src/app/features/wallets/buy-num/buy-num.page.html
index db7a6e99d..2c7a832a9 100644
--- a/src/app/features/wallets/buy-num/buy-num.page.html
+++ b/src/app/features/wallets/buy-num/buy-num.page.html
@@ -1,7 +1,5 @@
-
+
{{ t('wallets.buyCredits.buyCredits') }}
diff --git a/src/app/features/wallets/wallets.page.html b/src/app/features/wallets/wallets.page.html
index c4107a8a7..020f7b1e4 100644
--- a/src/app/features/wallets/wallets.page.html
+++ b/src/app/features/wallets/wallets.page.html
@@ -1,13 +1,6 @@
-
+
{{ 'wallets.walets' | transloco }}
diff --git a/src/app/features/wallets/wallets.page.scss b/src/app/features/wallets/wallets.page.scss
index 044c5df02..447ed91db 100644
--- a/src/app/features/wallets/wallets.page.scss
+++ b/src/app/features/wallets/wallets.page.scss
@@ -8,14 +8,6 @@ mat-toolbar {
text-align: center;
color: white;
}
-
- .capture-rebranded-button {
- margin: 0 4px;
- background: #ffffff40 !important; /* stylelint-disable-line declaration-no-important */
- color: white !important; /* stylelint-disable-line declaration-no-important */
- backdrop-filter: blur(4px);
- box-shadow: none;
- }
}
.no-network-text {
diff --git a/src/app/features/wallets/wallets.page.ts b/src/app/features/wallets/wallets.page.ts
index cd290ee27..1690fb3f6 100644
--- a/src/app/features/wallets/wallets.page.ts
+++ b/src/app/features/wallets/wallets.page.ts
@@ -8,7 +8,7 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { NgxQrcodeElementTypes } from '@techiediaries/ngx-qrcode';
import { BehaviorSubject, fromEvent } from 'rxjs';
import { concatMap, first, map, tap } from 'rxjs/operators';
-import { WebCryptoApiSignatureProvider } from '../../shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service';
+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 { BUBBLE_IFRAME_URL } from '../../shared/dia-backend/secret';
import { DiaBackendWalletService } from '../../shared/dia-backend/wallet/dia-backend-wallet.service';
@@ -20,8 +20,8 @@ import { BubbleToIonicPostMessage } from '../../shared/iframe/iframe';
styleUrls: ['./wallets.page.scss'],
})
export class WalletsPage {
- readonly publicKey$ = this.webCryptoApiSignatureProvider.publicKey$;
- readonly privateKey$ = this.webCryptoApiSignatureProvider.privateKey$;
+ readonly publicKey$ = this.capAppWebCryptoApiSignatureProvider.publicKey$;
+ readonly privateKey$ = this.capAppWebCryptoApiSignatureProvider.privateKey$;
readonly assetWalletAddr$ = this.diaBackendWalletService.assetWalletAddr$;
readonly networkConnected$ = this.diaBackendWalletService.networkConnected$;
@@ -43,12 +43,10 @@ export class WalletsPage {
private readonly diaBackendAuthService: DiaBackendAuthService,
private readonly snackBar: MatSnackBar,
private readonly translocoService: TranslocoService,
- private readonly webCryptoApiSignatureProvider: WebCryptoApiSignatureProvider,
+ private readonly capAppWebCryptoApiSignatureProvider: CaptureAppWebCryptoApiSignatureProvider,
private readonly router: Router,
private readonly navController: NavController
- ) {}
-
- ionViewDidEnter() {
+ ) {
this.processIframeEvents();
}
@@ -63,7 +61,7 @@ export class WalletsPage {
this.iframeLoaded$.next(true);
break;
case BubbleToIonicPostMessage.IFRAME_BACK_BUTTON_CLICKED:
- this.navController.pop();
+ this.navController.back();
break;
case BubbleToIonicPostMessage.IFRAME_BUY_NUM_BUTTON_CLICKED:
this.navigateToBuyNumPage();
diff --git a/src/app/shared/actions/service/order-history.service.ts b/src/app/shared/actions/service/order-history.service.ts
index 9aa3f8d48..81d9ba909 100644
--- a/src/app/shared/actions/service/order-history.service.ts
+++ b/src/app/shared/actions/service/order-history.service.ts
@@ -1,9 +1,9 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
-import { BehaviorSubject, combineLatest, defer, EMPTY, Observable } from 'rxjs';
+import { BehaviorSubject, EMPTY, Observable, combineLatest, defer } from 'rxjs';
import { concatMap, first, map, pluck, tap } from 'rxjs/operators';
-import { WebCryptoApiSignatureProvider } from '../../collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service';
+import { CaptureAppWebCryptoApiSignatureProvider } from '../../collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service';
import { DiaBackendAssetRepository } from '../../dia-backend/asset/dia-backend-asset-repository.service';
import { BUBBLE_DB_URL } from '../../dia-backend/secret';
import { NetworkAppOrder } from '../../dia-backend/store/dia-backend-store.service';
@@ -25,7 +25,7 @@ export class OrderHistoryService {
constructor(
private readonly httpClient: HttpClient,
- private readonly webCryptoApiSignatureProvider: WebCryptoApiSignatureProvider,
+ private readonly capAppWebCryptoApiSignatureProvider: CaptureAppWebCryptoApiSignatureProvider,
private readonly proofRepository: ProofRepository,
private readonly sanitizer: DomSanitizer,
private readonly diaBackendTransactionRepository: DiaBackendTransactionRepository,
@@ -50,7 +50,7 @@ export class OrderHistoryService {
createOrderHistory$(networkAppOrder: NetworkAppOrder, cid: string) {
return defer(() =>
- this.webCryptoApiSignatureProvider.publicKey$.pipe(
+ this.capAppWebCryptoApiSignatureProvider.publicKey$.pipe(
concatMap(publicKey =>
this.httpClient.post
(
`${BUBBLE_DB_URL}/api/1.1/obj/order`,
@@ -77,7 +77,7 @@ export class OrderHistoryService {
*/
getOrdersHistory$() {
return defer(() =>
- this.webCryptoApiSignatureProvider.publicKey$.pipe(
+ this.capAppWebCryptoApiSignatureProvider.publicKey$.pipe(
concatMap(publicKey =>
this.httpClient
.get>(
diff --git a/src/app/shared/android-back-button/android-back-button.service.ts b/src/app/shared/android-back-button/android-back-button.service.ts
index 2e543810c..65871fe38 100644
--- a/src/app/shared/android-back-button/android-back-button.service.ts
+++ b/src/app/shared/android-back-button/android-back-button.service.ts
@@ -21,4 +21,26 @@ export class AndroidBackButtonService {
mapTo(dialogRef)
);
}
+
+ /**
+ *
+ * @param callback to be run when android back button event is occured.
+ * @param priority Ionic Framework uses something similar to a priority
+ * queue to manage hardware back button handlers. The handler with the
+ * largest priority value will be called first. Prioriy for navigatoin is 0.
+ * By default `priority` param is set to 1 to override routing navigation (i.e. Angular Routing).
+ *
+ * To see complete list of priority list check out Ionic's official docs:
+ * https://ionicframework.com/docs/developing/hardware-back-button#internal-framework-handlers
+ *
+ */
+ overrideAndroidBackButtonBehavior$(callback: () => void, priority = 1) {
+ return this.androidBackButtonEvent$.pipe(
+ tap((event: any) => {
+ event.detail.register(priority, () => {
+ this.zone.run(() => callback());
+ });
+ })
+ );
+ }
}
diff --git a/src/app/shared/camera/camera.service.ts b/src/app/shared/camera/camera.service.ts
index 42ed05359..6696bdf43 100644
--- a/src/app/shared/camera/camera.service.ts
+++ b/src/app/shared/camera/camera.service.ts
@@ -1,14 +1,14 @@
import { Inject, Injectable } from '@angular/core';
import { AppPlugin } from '@capacitor/app';
import {
+ Photo as CameraPhoto,
CameraPlugin,
CameraResultType,
CameraSource,
- Photo as CameraPhoto,
} from '@capacitor/camera';
import { Subject } from 'rxjs';
import { blobToBase64 } from '../../utils/encoding/encoding';
-import { fromExtension, MimeType } from '../../utils/mime-type';
+import { MimeType, fromExtension } from '../../utils/mime-type';
import {
APP_PLUGIN,
CAMERA_PLUGIN,
@@ -51,6 +51,15 @@ export class CameraService {
return cameraPhotoToPhoto(cameraPhoto);
}
+ async pickPhoto(): Promise {
+ return this.cameraPlugin.getPhoto({
+ resultType: CameraResultType.Uri,
+ source: CameraSource.Photos,
+ quality: 100,
+ allowEditing: false,
+ });
+ }
+
// eslint-disable-next-line class-methods-use-this
async recordVideo(): Promise {
return new Promise((resolve, reject) => {
@@ -76,6 +85,7 @@ export class CameraService {
resolve({
base64,
mimeType: file.type as MimeType,
+ source: CameraSource.Camera,
})
);
};
@@ -99,6 +109,7 @@ function cameraPhotoToPhoto(cameraPhoto: CameraPhoto): Media {
mimeType: fromExtension(cameraPhoto.format),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
base64: cameraPhoto.base64String!,
+ source: CameraSource.Camera,
};
}
diff --git a/src/app/shared/capture-back-button/capture-back-button.component.html b/src/app/shared/capture-back-button/capture-back-button.component.html
new file mode 100644
index 000000000..53d5caed8
--- /dev/null
+++ b/src/app/shared/capture-back-button/capture-back-button.component.html
@@ -0,0 +1,3 @@
+
diff --git a/src/app/shared/capture-back-button/capture-back-button.component.scss b/src/app/shared/capture-back-button/capture-back-button.component.scss
new file mode 100644
index 000000000..934933ff6
--- /dev/null
+++ b/src/app/shared/capture-back-button/capture-back-button.component.scss
@@ -0,0 +1,4 @@
+.mat-mini-fab {
+ margin: 0 4px;
+ box-shadow: none;
+}
diff --git a/src/app/shared/capture-back-button/capture-back-button.component.spec.ts b/src/app/shared/capture-back-button/capture-back-button.component.spec.ts
new file mode 100644
index 000000000..aa1399a1e
--- /dev/null
+++ b/src/app/shared/capture-back-button/capture-back-button.component.spec.ts
@@ -0,0 +1,26 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { IonicModule } from '@ionic/angular';
+
+import { CaptureBackButtonComponent } from './capture-back-button.component';
+
+describe('CaptureBackButtonComponent', () => {
+ let component: CaptureBackButtonComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(
+ waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [CaptureBackButtonComponent],
+ imports: [IonicModule.forRoot()],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(CaptureBackButtonComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ })
+ );
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/capture-back-button/capture-back-button.component.ts b/src/app/shared/capture-back-button/capture-back-button.component.ts
new file mode 100644
index 000000000..f1c04803a
--- /dev/null
+++ b/src/app/shared/capture-back-button/capture-back-button.component.ts
@@ -0,0 +1,30 @@
+import { Component } from '@angular/core';
+import { NavController } from '@ionic/angular';
+
+@Component({
+ selector: 'app-capture-back-button',
+ templateUrl: './capture-back-button.component.html',
+ styleUrls: ['./capture-back-button.component.scss'],
+})
+export class CaptureBackButtonComponent {
+ constructor(private readonly navController: NavController) {}
+
+ /**
+ * WORKAROUND: capture app ionic (angular) navigationTrigger
+ * is `imperative`, however capture app iframe navigationTrigger
+ * is `popstate` (`popstate` because iframe uses `window.history.back()`).
+ *
+ * Using `imperative` with `popstate` together
+ * results to unexpected navigation behavior. Since we can
+ * not change how capture app iframe navigatioin works we need
+ * to change capture app ionic's navigation to `popstate`.
+ */
+ back() {
+ /**
+ * When capture app iframe navigates back it uses `windows.history.back()`.
+ * Angular provides `NavController.back()` method that uses
+ * `windows.history.back()` under the hood, which is also featuring a back animation.
+ */
+ this.navController.back();
+ }
+}
diff --git a/src/app/shared/capture/capture.service.ts b/src/app/shared/capture/capture.service.ts
index bb7287dad..b5cc22c7f 100644
--- a/src/app/shared/capture/capture.service.ts
+++ b/src/app/shared/capture/capture.service.ts
@@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
+import { CameraSource } from '@capacitor/camera';
import { BehaviorSubject } from 'rxjs';
import { MimeType } from '../../utils/mime-type';
import { CollectorService } from '../collector/collector.service';
@@ -22,10 +23,10 @@ export class CaptureService {
private readonly collectorService: CollectorService
) {}
- async capture(source: Media) {
+ async capture(media: Media) {
const proof = await Proof.from(
this.mediaStore,
- { [source.base64]: { mimeType: source.mimeType } },
+ { [media.base64]: { mimeType: media.mimeType } },
{ timestamp: Date.now(), providers: {} },
{}
);
@@ -37,7 +38,8 @@ export class CaptureService {
);
const collected = await this.collectorService.run(
await proof.getAssets(),
- proof.timestamp
+ proof.timestamp,
+ media.source
);
// eslint-disable-next-line rxjs/no-subject-value
const newCollectingOldProofHashes = this._collectingOldProofHashes$.value;
@@ -54,4 +56,5 @@ export class CaptureService {
export interface Media {
readonly mimeType: MimeType;
readonly base64: string;
+ readonly source: CameraSource;
}
diff --git a/src/app/shared/collector/collector.service.spec.ts b/src/app/shared/collector/collector.service.spec.ts
index d60452f16..673faea3b 100644
--- a/src/app/shared/collector/collector.service.spec.ts
+++ b/src/app/shared/collector/collector.service.spec.ts
@@ -1,5 +1,6 @@
/* eslint-disable class-methods-use-this */
import { TestBed } from '@angular/core/testing';
+import { CameraSource } from '@capacitor/camera';
import { MimeType } from '../../utils/mime-type';
import {
AssetMeta,
@@ -27,7 +28,7 @@ describe('CollectorService', () => {
it('should be created', () => expect(service).toBeTruthy());
it('should get the stored proof after run', async () => {
- const proof = await service.run(ASSETS, Date.now());
+ const proof = await service.run(ASSETS, Date.now(), CameraSource.Camera);
expect(await proof.getAssets()).toEqual(ASSETS);
});
@@ -35,7 +36,7 @@ describe('CollectorService', () => {
service.addFactsProvider(mockFactsProvider);
service.removeFactsProvider(mockFactsProvider);
- const proof = await service.run(ASSETS, Date.now());
+ const proof = await service.run(ASSETS, Date.now(), CameraSource.Camera);
expect(proof.truth.providers).toEqual({});
});
@@ -44,20 +45,20 @@ describe('CollectorService', () => {
service.addSignatureProvider(mockSignatureProvider);
service.removeSignatureProvider(mockSignatureProvider);
- const proof = await service.run(ASSETS, Date.now());
+ const proof = await service.run(ASSETS, Date.now(), CameraSource.Camera);
expect(proof.signatures).toEqual({});
});
it('should get the stored proof with provided facts', async () => {
service.addFactsProvider(mockFactsProvider);
- const proof = await service.run(ASSETS, Date.now());
+ const proof = await service.run(ASSETS, Date.now(), CameraSource.Camera);
expect(proof.truth.providers).toEqual({ [mockFactsProvider.id]: FACTS });
});
it('should get the stored proof with provided signature', async () => {
service.addSignatureProvider(mockSignatureProvider);
- const proof = await service.run(ASSETS, Date.now());
+ const proof = await service.run(ASSETS, Date.now(), CameraSource.Camera);
expect(proof.signatures).toEqual({ [mockSignatureProvider.id]: SIGNATURE });
});
});
@@ -104,6 +105,9 @@ const SIGNATURE: Signature = {
};
class MockSignatureProvider implements SignatureProvider {
readonly id = 'MockSignatureProvider';
+ idFor(_source: any): string {
+ return this.id;
+ }
// eslint-disable-next-line @typescript-eslint/require-await
async provide(_: string) {
return SIGNATURE;
diff --git a/src/app/shared/collector/collector.service.ts b/src/app/shared/collector/collector.service.ts
index d46875117..e8f8754f8 100644
--- a/src/app/shared/collector/collector.service.ts
+++ b/src/app/shared/collector/collector.service.ts
@@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
+import { CameraSource } from '@capacitor/camera';
import { MediaStore } from '../media/media-store/media-store.service';
import {
Assets,
@@ -9,6 +10,7 @@ import {
Truth,
} from '../repositories/proof/proof';
import { FactsProvider } from './facts/facts-provider';
+import { CaptureAppWebCryptoApiSignatureProvider } from './signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service';
import { SignatureProvider } from './signature/signature-provider';
@Injectable({
@@ -20,17 +22,19 @@ export class CollectorService {
constructor(private readonly mediaStore: MediaStore) {}
- async run(assets: Assets, capturedTimestamp: number) {
+ async run(assets: Assets, capturedTimestamp: number, source: CameraSource) {
const truth = await this.collectTruth(assets, capturedTimestamp);
const proof = await Proof.from(this.mediaStore, assets, truth);
- await this.generateSignature(proof);
+ await this.generateSignature(proof, source);
proof.isCollected = true;
return proof;
}
- async generateSignature(proof: Proof) {
- const signedMessage = await proof.generateSignedMessage();
- const signatures = await this.signMessage(signedMessage);
+ async generateSignature(proof: Proof, source: CameraSource) {
+ const recorder =
+ CaptureAppWebCryptoApiSignatureProvider.recorderFor(source);
+ const signedMessage = await proof.generateSignedMessage(recorder);
+ const signatures = await this.signMessage(signedMessage, source);
proof.setSignatures(signatures);
return proof;
}
@@ -52,13 +56,16 @@ export class CollectorService {
};
}
- private async signMessage(message: SignedMessage): Promise {
+ private async signMessage(
+ message: SignedMessage,
+ source: CameraSource
+ ): Promise {
const serializedSortedSignedMessage =
getSerializedSortedSignedMessage(message);
return Object.fromEntries(
await Promise.all(
[...this.signatureProviders].map(async provider => [
- provider.id,
+ provider.idFor(source),
await provider.provide(serializedSortedSignedMessage),
])
)
diff --git a/src/app/shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service.spec.ts b/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.spec.ts
similarity index 82%
rename from src/app/shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service.spec.ts
rename to src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.spec.ts
index 9ca199875..0c1effc84 100644
--- a/src/app/shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service.spec.ts
+++ b/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.spec.ts
@@ -2,18 +2,22 @@ import { TestBed } from '@angular/core/testing';
import { defer } from 'rxjs';
import { concatMapTo } from 'rxjs/operators';
import { sortObjectDeeplyByKey } from '../../../../utils/immutable/immutable';
-import { isSignature, SignedMessage } from '../../../repositories/proof/proof';
+import {
+ isSignature,
+ RecorderType,
+ SignedMessage,
+} from '../../../repositories/proof/proof';
import { SharedTestingModule } from '../../../shared-testing.module';
-import { WebCryptoApiSignatureProvider } from './web-crypto-api-signature-provider.service';
+import { CaptureAppWebCryptoApiSignatureProvider } from './capture-app-web-crypto-api-signature-provider.service';
-describe('WebCryptoApiSignatureProvider', () => {
- let provider: WebCryptoApiSignatureProvider;
+describe('CaptureAppWebCryptoApiSignatureProvider', () => {
+ let provider: CaptureAppWebCryptoApiSignatureProvider;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [SharedTestingModule],
});
- provider = TestBed.inject(WebCryptoApiSignatureProvider);
+ provider = TestBed.inject(CaptureAppWebCryptoApiSignatureProvider);
});
it('should be created', () => expect(provider).toBeTruthy());
@@ -58,7 +62,7 @@ describe('WebCryptoApiSignatureProvider', () => {
it('should provide signature', async () => {
const signedMessage: SignedMessage = {
spec_version: '',
- recorder: '',
+ recorder: RecorderType.Capture,
created_at: 0,
proof_hash: '',
asset_mime_type: '',
diff --git a/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.ts b/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.ts
new file mode 100644
index 000000000..34e18e27c
--- /dev/null
+++ b/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.ts
@@ -0,0 +1,124 @@
+import { Injectable } from '@angular/core';
+import { CameraSource } from '@capacitor/camera';
+import {
+ createEthAccount,
+ loadEthAccount,
+} from '../../../../utils/crypto/crypto';
+import { PreferenceManager } from '../../../preference-manager/preference-manager.service';
+import { RecorderType, Signature } from '../../../repositories/proof/proof';
+import { SignatureProvider } from '../signature-provider';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class CaptureAppWebCryptoApiSignatureProvider
+ implements SignatureProvider
+{
+ readonly deprecatedProviderId = 'WebCryptoApiSignatureProvider';
+ readonly id = 'CaptureAppWebCryptoApiSignatureProvider';
+
+ private readonly preferences = this.preferenceManager.getPreferences(this.id);
+
+ readonly publicKey$ = this.preferences.getString$(PrefKeys.PUBLIC_KEY);
+
+ readonly privateKey$ = this.preferences.getString$(PrefKeys.PRIVATE_KEY);
+
+ constructor(private readonly preferenceManager: PreferenceManager) {}
+
+ idFor(source: any): string {
+ switch (source) {
+ case CameraSource.Photos:
+ return 'UploaderWebCryptoApiSignatureProvider';
+ case CameraSource.Camera:
+ return this.id;
+ default:
+ return this.id;
+ }
+ }
+
+ /**
+ * Determines the appropriate recorder type based on the camera source.
+ *
+ * @param cameraSource - The CameraSource used for determining the recorder type
+ * @returns The RecorderType associated with the given camera source
+ */
+ // eslint-disable-next-line @typescript-eslint/member-ordering
+ static recorderFor(source: CameraSource): RecorderType {
+ switch (source) {
+ case CameraSource.Photos:
+ return RecorderType.UploaderWebCryptoApiSignatureProvider;
+ case CameraSource.Camera:
+ return RecorderType.CaptureAppWebCryptoApiSignatureProvider;
+ default:
+ return RecorderType.Capture;
+ }
+ }
+
+ async initialize() {
+ await this.copyKeysFromWebCryptoApiSignatureProviderIfAny();
+ const originalPublicKey = await this.getPublicKey();
+ const originalPrivateKey = await this.getPrivateKey();
+ if (
+ originalPublicKey.length === 0 ||
+ originalPrivateKey.length === 0 ||
+ !originalPublicKey.startsWith('0x')
+ ) {
+ const account = createEthAccount();
+ await this.preferences.setString(PrefKeys.PUBLIC_KEY, account.address);
+ await this.preferences.setString(
+ PrefKeys.PRIVATE_KEY,
+ account.privateKey
+ );
+ }
+ }
+
+ async provide(serializedSortedSignedTargets: string): Promise {
+ await this.initialize();
+ const account = loadEthAccount(await this.getPrivateKey());
+ const sign = account.sign(serializedSortedSignedTargets);
+ const publicKey = await this.getPublicKey();
+ return { signature: sign.signature, publicKey };
+ }
+
+ async getPublicKey() {
+ return this.preferences.getString(PrefKeys.PUBLIC_KEY);
+ }
+
+ async getPrivateKey() {
+ return this.preferences.getString(PrefKeys.PRIVATE_KEY);
+ }
+
+ async importKeys(publicKey: string, privateKey: string) {
+ await this.preferences.setString(PrefKeys.PUBLIC_KEY, publicKey);
+ await this.preferences.setString(PrefKeys.PRIVATE_KEY, privateKey);
+ }
+
+ /**
+ * Will copy public, private key from WebCryptoApiSignatureProvider preferences
+ * to CaptureAppWebCryptoApiSignatureProvider preferences if there are any keys
+ */
+ private async copyKeysFromWebCryptoApiSignatureProviderIfAny() {
+ const publicKey = await this.getWebCryptoApiSignatureProviderPublicKey();
+ const privateKey = await this.getWebCryptoApiSignatureProviderPrivateKey();
+ if (!!publicKey && !!privateKey) {
+ await this.importKeys(publicKey, privateKey);
+ }
+ }
+
+ private async getWebCryptoApiSignatureProviderPublicKey() {
+ return this.preferenceManager
+ .getPreferences(this.deprecatedProviderId)
+ .getString(PrefKeys.PUBLIC_KEY);
+ }
+
+ private async getWebCryptoApiSignatureProviderPrivateKey() {
+ return this.preferenceManager
+ .getPreferences(this.deprecatedProviderId)
+ .getString(PrefKeys.PRIVATE_KEY);
+ }
+}
+
+const enum PrefKeys {
+ PUBLIC_KEY = 'PUBLIC_KEY',
+ PRIVATE_KEY = 'PRIVATE_KEY',
+}
diff --git a/src/app/shared/collector/signature/signature-provider.ts b/src/app/shared/collector/signature/signature-provider.ts
index f563299c7..f0743255c 100644
--- a/src/app/shared/collector/signature/signature-provider.ts
+++ b/src/app/shared/collector/signature/signature-provider.ts
@@ -2,5 +2,6 @@ import { Signature } from '../../repositories/proof/proof';
export interface SignatureProvider {
readonly id: string;
+ idFor(source: any): string;
provide(serializedSortedSignedTargets: string): Promise;
}
diff --git a/src/app/shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service.ts b/src/app/shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service.ts
deleted file mode 100644
index fb1647291..000000000
--- a/src/app/shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { Injectable } from '@angular/core';
-import {
- createEthAccount,
- loadEthAccount,
-} from '../../../../utils/crypto/crypto';
-import { PreferenceManager } from '../../../preference-manager/preference-manager.service';
-import { Signature } from '../../../repositories/proof/proof';
-import { SignatureProvider } from '../signature-provider';
-
-@Injectable({
- providedIn: 'root',
-})
-export class WebCryptoApiSignatureProvider implements SignatureProvider {
- readonly id = 'WebCryptoApiSignatureProvider';
-
- private readonly preferences = this.preferenceManager.getPreferences(this.id);
-
- readonly publicKey$ = this.preferences.getString$(PrefKeys.PUBLIC_KEY);
-
- readonly privateKey$ = this.preferences.getString$(PrefKeys.PRIVATE_KEY);
-
- constructor(private readonly preferenceManager: PreferenceManager) {}
-
- async initialize() {
- const originalPublicKey = await this.getPublicKey();
- const originalPrivateKey = await this.getPrivateKey();
- if (
- originalPublicKey.length === 0 ||
- originalPrivateKey.length === 0 ||
- !originalPublicKey.startsWith('0x')
- ) {
- const account = createEthAccount();
- await this.preferences.setString(PrefKeys.PUBLIC_KEY, account.address);
- await this.preferences.setString(
- PrefKeys.PRIVATE_KEY,
- account.privateKey
- );
- }
- }
-
- async provide(serializedSortedSignedTargets: string): Promise {
- await this.initialize();
- const account = loadEthAccount(await this.getPrivateKey());
- const sign = account.sign(serializedSortedSignedTargets);
- const publicKey = await this.getPublicKey();
- return { signature: sign.signature, publicKey };
- }
-
- async getPublicKey() {
- return this.preferences.getString(PrefKeys.PUBLIC_KEY);
- }
-
- async getPrivateKey() {
- return this.preferences.getString(PrefKeys.PRIVATE_KEY);
- }
-
- async importKeys(publicKey: string, privateKey: string) {
- await this.preferences.setString(PrefKeys.PUBLIC_KEY, publicKey);
- await this.preferences.setString(PrefKeys.PRIVATE_KEY, privateKey);
- }
-}
-
-const enum PrefKeys {
- PUBLIC_KEY = 'PUBLIC_KEY',
- PRIVATE_KEY = 'PRIVATE_KEY',
-}
diff --git a/src/app/shared/dia-backend/asset/uploading/dia-backend-asset-uploading.service.ts b/src/app/shared/dia-backend/asset/uploading/dia-backend-asset-uploading.service.ts
index 99f96034a..2cb55da5d 100644
--- a/src/app/shared/dia-backend/asset/uploading/dia-backend-asset-uploading.service.ts
+++ b/src/app/shared/dia-backend/asset/uploading/dia-backend-asset-uploading.service.ts
@@ -1,5 +1,6 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
+import { TranslocoService } from '@ngneat/transloco';
import {
BehaviorSubject,
combineLatest,
@@ -22,6 +23,7 @@ import {
tap,
} from 'rxjs/operators';
import { isNonNullable } from '../../../../utils/rx-operators/rx-operators';
+import { ErrorService } from '../../../error/error.service';
import { NetworkService } from '../../../network/network.service';
import { PreferenceManager } from '../../../preference-manager/preference-manager.service';
import { getOldProof } from '../../../repositories/proof/old-proof-adapter';
@@ -57,7 +59,9 @@ export class DiaBackendAssetUploadingService {
private readonly diaBackendAssetRepository: DiaBackendAssetRepository,
private readonly networkService: NetworkService,
private readonly preferenceManager: PreferenceManager,
- private readonly proofRepository: ProofRepository
+ private readonly proofRepository: ProofRepository,
+ private readonly errorService: ErrorService,
+ private readonly translocoService: TranslocoService
) {}
initialize$() {
@@ -127,6 +131,16 @@ export class DiaBackendAssetUploadingService {
) {
return this.diaBackendAssetRepository.fetchByProof$(proof);
}
+ if (
+ err instanceof HttpErrorResponse &&
+ err.error.error.type === 'asset_commit_insufficient_fund'
+ ) {
+ const toastError = this.translocoService.translate(
+ `error.diaBackend.${err.error.error.type}`
+ );
+ this.errorService.toastError$(toastError).subscribe();
+ this.pause();
+ }
return throwError(err);
}),
map(diaBackendAsset => {
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 f1e534a0b..457c271c3 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
@@ -1,4 +1,4 @@
-import { HttpClient } from '@angular/common/http';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Device } from '@capacitor/device';
import { Storage } from '@capacitor/storage';
@@ -20,7 +20,7 @@ import { isNonNullable } from '../../../utils/rx-operators/rx-operators';
import { LanguageService } from '../../language/service/language.service';
import { PreferenceManager } from '../../preference-manager/preference-manager.service';
import { PushNotificationService } from '../../push-notification/push-notification.service';
-import { BASE_URL, TRUSTED_CLIENT_KEY } from '../secret';
+import { BASE_URL, PIPEDREAM_URL, TRUSTED_CLIENT_KEY } from '../secret';
@Injectable({
providedIn: 'root',
@@ -291,6 +291,19 @@ export class DiaBackendAuthService {
);
}
+ deleteAccount$(email: string) {
+ return defer(() => this.getAuthHeaders()).pipe(
+ concatMap(authHeaders => {
+ const body = { email };
+ return this.httpClient.post(`${PIPEDREAM_URL}`, body, {
+ headers: new HttpHeaders()
+ .set('Authorization', `${authHeaders.authorization}`)
+ .set('Content-Type', 'application/json'),
+ });
+ })
+ );
+ }
+
uploadAvatar$({ picture }: { picture: File }) {
const formData = new FormData();
formData.append('profile_picture', picture);
diff --git a/src/app/shared/migration/service/migration.service.ts b/src/app/shared/migration/service/migration.service.ts
index 7d3a98df5..7095f5b80 100644
--- a/src/app/shared/migration/service/migration.service.ts
+++ b/src/app/shared/migration/service/migration.service.ts
@@ -1,6 +1,7 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
+import { CameraSource } from '@capacitor/camera';
import { defer, forkJoin, iif } from 'rxjs';
import {
catchError,
@@ -12,7 +13,7 @@ import {
} from 'rxjs/operators';
import { VOID$ } from '../../../utils/rx-operators/rx-operators';
import { CollectorService } from '../../collector/collector.service';
-import { WebCryptoApiSignatureProvider } from '../../collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service';
+import { CaptureAppWebCryptoApiSignatureProvider } from '../../collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service';
import {
DiaBackendAsset,
DiaBackendAssetRepository,
@@ -46,7 +47,7 @@ export class MigrationService {
private readonly preferenceManager: PreferenceManager,
private readonly onboardingService: OnboardingService,
private readonly versionService: VersionService,
- private readonly webCryptoApiSignatureProvider: WebCryptoApiSignatureProvider
+ private readonly capAppWebCryptoApiSignatureProvider: CaptureAppWebCryptoApiSignatureProvider
) {}
migrate$(skip?: boolean) {
@@ -161,7 +162,7 @@ export class MigrationService {
err.status === HttpErrorCode.NOT_FOUND
) {
return defer(() =>
- this.webCryptoApiSignatureProvider.getPrivateKey()
+ this.capAppWebCryptoApiSignatureProvider.getPrivateKey()
).pipe(
concatMap(privateKey =>
this.diaBackendWalletService.setIntegrityWallet$(privateKey)
@@ -171,7 +172,7 @@ export class MigrationService {
throw err;
}),
concatMap(assetWallet =>
- this.webCryptoApiSignatureProvider.importKeys(
+ this.capAppWebCryptoApiSignatureProvider.importKeys(
assetWallet.address,
assetWallet.private_key
)
@@ -185,7 +186,9 @@ export class MigrationService {
map(proofs => proofs.filter(proof => !proof.signatureVersion)),
concatMap(proofs =>
forkJoin(
- proofs.map(proof => this.collectorService.generateSignature(proof))
+ proofs.map(proof =>
+ this.collectorService.generateSignature(proof, CameraSource.Camera)
+ )
).pipe(defaultIfEmpty(proofs))
),
concatMap(proofs =>
diff --git a/src/app/shared/repositories/proof/proof.ts b/src/app/shared/repositories/proof/proof.ts
index 705bcb03e..d4dd39ec4 100644
--- a/src/app/shared/repositories/proof/proof.ts
+++ b/src/app/shared/repositories/proof/proof.ts
@@ -11,7 +11,11 @@ import {
OnWriteExistStrategy,
} from '../../media/media-store/media-store.service';
-const RECORDER = 'Capture';
+export enum RecorderType {
+ Capture = 'Capture',
+ CaptureAppWebCryptoApiSignatureProvider = 'CaptureAppWebCryptoApiSignatureProvider',
+ UploaderWebCryptoApiSignatureProvider = 'UploaderWebCryptoApiSignatureProvider',
+}
const SIGNATURE_VERSION = '2.0.0';
export class Proof {
@@ -200,10 +204,21 @@ export class Proof {
return Object.fromEntries(factEntries) as Facts;
}
- async generateSignedMessage() {
+ /**
+ * Generates a signed message with the provided recorder type.
+ *
+ * @param recorderType - The type of recorder used for signing the message
+ * (default is RecorderType.CAPTURE). Related discussion comments:
+ * - https://github.com/numbersprotocol/capture-lite/issues/779#issuecomment-880330292
+ * - https://app.asana.com/0/0/1204012493522134/1204289040001270/f
+ * @returns A promise that resolves to the generated signed message
+ */
+ async generateSignedMessage(
+ recorder: RecorderType = RecorderType.Capture
+ ): Promise {
const signedMessage: SignedMessage = {
spec_version: SIGNATURE_VERSION,
- recorder: RECORDER,
+ recorder: recorder,
created_at: this.truth.timestamp,
location_latitude: this.geolocationLatitude,
location_longitude: this.geolocationLongitude,
@@ -387,7 +402,7 @@ export interface IndexedProofView extends Tuple {
*/
export interface SignedMessage {
spec_version: string;
- recorder: string;
+ recorder: RecorderType;
created_at: number;
location_latitude?: number;
location_longitude?: number;
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index c0f5b555f..a7c998ee9 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -14,6 +14,7 @@ import { GoProWifiService } from '../features/settings/go-pro/services/go-pro-wi
import { ActionsDialogComponent } from './actions/actions-dialog/actions-dialog.component';
import { AvatarComponent } from './avatar/avatar.component';
import { CapacitorPluginsModule } from './capacitor-plugins/capacitor-plugins.module';
+import { CaptureBackButtonComponent } from './capture-back-button/capture-back-button.component';
import { ContactSelectionDialogComponent } from './contact-selection-dialog/contact-selection-dialog.component';
import { FriendInvitationDialogComponent } from './contact-selection-dialog/friend-invitation-dialog/friend-invitation-dialog.component';
import { ExportPrivateKeyModalComponent } from './export-private-key-modal/export-private-key-modal.component';
@@ -39,6 +40,7 @@ const declarations = [
FriendInvitationDialogComponent,
ExportPrivateKeyModalComponent,
OrderDetailDialogComponent,
+ CaptureBackButtonComponent,
];
const imports = [
diff --git a/src/assets/i18n/en-us.json b/src/assets/i18n/en-us.json
index 87eff9ab2..ef7fd1c51 100644
--- a/src/assets/i18n/en-us.json
+++ b/src/assets/i18n/en-us.json
@@ -232,7 +232,8 @@
"invalid_network_app_name": "Invalid network app.",
"invalid_referral_code": "Invalid referral code",
"duplicate_email": "The email has already been registered",
- "duplicate_username": "User with this username already exists"
+ "duplicate_username": "User with this username already exists",
+ "asset_commit_insufficient_fund": "Insufficient NUM balance. Please top up your balance to complete the asset registration."
},
"wallets": {
"emptyTransferAmount": "Please enter a valid transfer amount.",
diff --git a/src/assets/i18n/zh-tw.json b/src/assets/i18n/zh-tw.json
index ca33a1045..d72c32f9e 100644
--- a/src/assets/i18n/zh-tw.json
+++ b/src/assets/i18n/zh-tw.json
@@ -232,7 +232,8 @@
"invalid_network_app_name": "無效的網絡動作。",
"invalid_referral_code": "無效的推薦代碼",
"duplicate_email": "該電子郵件已註冊",
- "duplicate_username": "用戶名已被其他使用者使用"
+ "duplicate_username": "用戶名已被其他使用者使用",
+ "asset_commit_insufficient_fund": "NUM 餘額不足。 請加值您的帳戶以完成區塊鏈註冊。"
},
"wallets": {
"emptyTransferAmount": "請輸入有效轉帳金額。",