diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 5e9832f96..7ada78dc6 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -19,7 +19,7 @@ jobs: - name: Get release version id: version_check - run: echo "version_new=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT + run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT - name: Build Ionic env: @@ -90,7 +90,7 @@ jobs: - name: Get release version id: version_check - run: echo "version_new=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT + run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT - name: Build Ionic env: @@ -166,7 +166,7 @@ jobs: - name: Get release version id: version_check - run: echo "version_new=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT + run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT - name: Build Ionic env: @@ -286,7 +286,7 @@ jobs: - name: Get release version id: version_check - run: echo "version_new=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT + run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT - name: Create GitHub prerelease id: create_release diff --git a/CHANGELOG.md b/CHANGELOG.md index 572035fec..ac75f7d2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 To check the difference between the last releaes and the latest dev status, click the link above. +## [0.77.0] - 2023-04-03 + +### Added + +1. Feature upload image (#2671) +1. Feature show clear error handling of change username on my capture page (#2664) +1. Feature special provider for photos taken from capture app (#2667) + +### Fixed + +1. Fix for ios action of capture ci fail to upload ipa to test flight (#2668) +1. Fix show message if asset registration insufficient num (#2669) +1. Fix ionic navigation with iframe back button (#2670) +1. Fix issue capture details swipe left right is not showing expected capture (#2665) +1. Fix delete account should work real (#2666) + ## [0.75.2] - 2023-03-13 ### Fixed @@ -2069,7 +2085,8 @@ This is the first release! _Capture Lite_ is a cross-platform app adapted from [ - Web - see the demo [here](https://github.com/numbersprotocol/capture-lite#demo-app) - Android - the APK file `app-debug.apk` is attached to this release -[unreleased]: https://github.com/numbersprotocol/capture-lite/compare/0.75.2...HEAD +[unreleased]: https://github.com/numbersprotocol/capture-lite/compare/0.77.0...HEAD +[0.77.0]: https://github.com/numbersprotocol/capture-lite/compare/0.75.2...0.77.0 [0.75.2]: https://github.com/numbersprotocol/capture-lite/compare/0.75.1...0.75.2 [0.75.1]: https://github.com/numbersprotocol/capture-lite/compare/0.75.0...0.75.1 [0.75.0]: https://github.com/numbersprotocol/capture-lite/compare/0.74.2...0.75.0 diff --git a/android/app/build.gradle b/android/app/build.gradle index 0b37b62c2..eee6eb1cb 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 752 - versionName "0.75.2" + versionCode 770 + versionName "0.77.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildFeatures { diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 774d9fbef..424254259 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -368,13 +368,13 @@ CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 752; + CURRENT_PROJECT_VERSION = 770; DEVELOPMENT_TEAM = G7NB5YCKAP; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = G7NB5YCKAP; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.75.2; + MARKETING_VERSION = 0.77.0; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = io.numbersprotocol.capturelite; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -395,13 +395,13 @@ CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 752; + CURRENT_PROJECT_VERSION = 770; DEVELOPMENT_TEAM = G7NB5YCKAP; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = G7NB5YCKAP; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.75.2; + MARKETING_VERSION = 0.77.0; PRODUCT_BUNDLE_IDENTIFIER = io.numbersprotocol.capturelite; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = NumbersAppDistributionV4; diff --git a/package-lock.json b/package-lock.json index 4e4e07e1a..43d70e7f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "capture-lite", - "version": "0.75.2", + "version": "0.77.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "capture-lite", - "version": "0.75.2", + "version": "0.77.0", "dependencies": { "packages": "^0.0.8", "@angular/animations": "^14.2.0", diff --git a/package.json b/package.json index c31f5f0b9..66e4bdf48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "capture-lite", - "version": "0.75.2", + "version": "0.77.0", "author": "numbersprotocol", "homepage": "https://numbersprotocol.io/", "scripts": { diff --git a/set-secret.js b/set-secret.js index dad020fac..fd23cf0b8 100644 --- a/set-secret.js +++ b/set-secret.js @@ -11,6 +11,7 @@ export const BUBBLE_DB_URL = '${process.env.NUMBERS_BUBBLE_DB_URL}'; export const BUBBLE_IFRAME_URL = '${process.env.NUMBERS_BUBBLE_IFRAME_URL}'; export const BUBBLE_API_URL = '${process.env.BUBBLE_API_URL}'; export const APPS_FLYER_DEV_KEY = '${process.env.APPS_FLYER_DEV_KEY}' +export const PIPEDREAM_URL = '${process.env.PIPEDREAM_URL}' `; fs.writeFile(targetPath, envConfigFile, err => { if (err) { diff --git a/src/app/app.component.ts b/src/app/app.component.ts index cc5e0dcb1..876f1d4f1 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -10,7 +10,7 @@ import { CameraService } from './shared/camera/camera.service'; import { CaptureService } from './shared/capture/capture.service'; import { CollectorService } from './shared/collector/collector.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 { DiaBackendAssetUploadingService } from './shared/dia-backend/asset/uploading/dia-backend-asset-uploading.service'; import { DiaBackendAuthService } from './shared/dia-backend/auth/dia-backend-auth.service'; import { DiaBackendNotificationService } from './shared/dia-backend/notification/dia-backend-notification.service'; @@ -33,7 +33,7 @@ export class AppComponent { private readonly iconRegistry: MatIconRegistry, private readonly sanitizer: DomSanitizer, private readonly capacitorFactsProvider: CapacitorFactsProvider, - private readonly webCryptoApiSignatureProvider: WebCryptoApiSignatureProvider, + private readonly capAppWebCryptoApiSignatureProvider: CaptureAppWebCryptoApiSignatureProvider, private readonly captureService: CaptureService, private readonly cameraService: CameraService, private readonly errorService: ErrorService, @@ -92,10 +92,10 @@ export class AppComponent { } initializeCollector() { - this.webCryptoApiSignatureProvider.initialize(); + this.capAppWebCryptoApiSignatureProvider.initialize(); this.collectorService.addFactsProvider(this.capacitorFactsProvider); this.collectorService.addSignatureProvider( - this.webCryptoApiSignatureProvider + this.capAppWebCryptoApiSignatureProvider ); } diff --git a/src/app/features/about/about.page.html b/src/app/features/about/about.page.html index 79dedc4c4..1ac9604df 100644 --- a/src/app/features/about/about.page.html +++ b/src/app/features/about/about.page.html @@ -1,7 +1,5 @@ - + {{ t('about') }} diff --git a/src/app/features/contacts/contacts.page.html b/src/app/features/contacts/contacts.page.html index d474de92f..b728c479f 100644 --- a/src/app/features/contacts/contacts.page.html +++ b/src/app/features/contacts/contacts.page.html @@ -1,7 +1,5 @@ - + {{ t('friends') }} diff --git a/src/app/features/data-policy/data-policy.page.html b/src/app/features/data-policy/data-policy.page.html index 79b948a2b..d358a7467 100644 --- a/src/app/features/data-policy/data-policy.page.html +++ b/src/app/features/data-policy/data-policy.page.html @@ -1,13 +1,6 @@ - + {{ 'dataPolicy' | transloco }}
diff --git a/src/app/features/data-policy/data-policy.page.scss b/src/app/features/data-policy/data-policy.page.scss index 044c5df02..447ed91db 100644 --- a/src/app/features/data-policy/data-policy.page.scss +++ b/src/app/features/data-policy/data-policy.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/faq/faq.page.html b/src/app/features/faq/faq.page.html index 691c44b86..63ad280d5 100644 --- a/src/app/features/faq/faq.page.html +++ b/src/app/features/faq/faq.page.html @@ -1,13 +1,6 @@ - + {{ 'faq' | transloco }}
diff --git a/src/app/features/faq/faq.page.scss b/src/app/features/faq/faq.page.scss index 044c5df02..447ed91db 100644 --- a/src/app/features/faq/faq.page.scss +++ b/src/app/features/faq/faq.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/home/activities/activities.page.html b/src/app/features/home/activities/activities.page.html index a6c3f0698..1cf546521 100644 --- a/src/app/features/home/activities/activities.page.html +++ b/src/app/features/home/activities/activities.page.html @@ -1,7 +1,5 @@ - + {{ t('activity') }} diff --git a/src/app/features/home/activities/capture-transaction-details/capture-transaction-details.page.html b/src/app/features/home/activities/capture-transaction-details/capture-transaction-details.page.html index 90b790f2a..33f02c325 100644 --- a/src/app/features/home/activities/capture-transaction-details/capture-transaction-details.page.html +++ b/src/app/features/home/activities/capture-transaction-details/capture-transaction-details.page.html @@ -1,7 +1,5 @@ - + {{ t('transactionDetails') }}
diff --git a/src/app/features/home/activities/network-action-order-details/network-action-order-details.page.html b/src/app/features/home/activities/network-action-order-details/network-action-order-details.page.html index b7396f83a..ff5fff471 100644 --- a/src/app/features/home/activities/network-action-order-details/network-action-order-details.page.html +++ b/src/app/features/home/activities/network-action-order-details/network-action-order-details.page.html @@ -1,7 +1,5 @@ - + {{ t('networkActionOrderDetails') }} 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 f6c65fa7d..560fac335 100644 --- a/src/app/features/home/capture-tab/capture-tab.component.ts +++ b/src/app/features/home/capture-tab/capture-tab.component.ts @@ -1,4 +1,5 @@ import { formatDate, KeyValue } from '@angular/common'; +import { HttpErrorResponse } from '@angular/common/http'; import { Component } from '@angular/core'; import { AlertController } from '@ionic/angular'; import { TranslocoService } from '@ngneat/transloco'; @@ -136,13 +137,25 @@ export class CaptureTabComponent { private updateUsername(username: string) { const action$ = this.diaBackendAuthService .updateUser$({ username }) - .pipe(catchError((err: unknown) => this.errorService.toastError$(err))); + .pipe(catchError((err: unknown) => this.handleUpdateUsernameError$(err))); return this.blockingActionService .run$(action$) .pipe(untilDestroyed(this)) .subscribe(); } + private handleUpdateUsernameError$(err: unknown) { + if (err instanceof HttpErrorResponse) { + const errorType = err.error.error?.type; + if (errorType === 'duplicate_username') { + return this.errorService.toastError$( + this.translocoService.translate(`error.diaBackend.${errorType}`) + ); + } + } + return this.errorService.toastError$(err); + } + // eslint-disable-next-line class-methods-use-this keyDescendingOrder( a: KeyValue, diff --git a/src/app/features/home/custom-camera/custom-camera.page.html b/src/app/features/home/custom-camera/custom-camera.page.html index bfbf594b3..9062db707 100644 --- a/src/app/features/home/custom-camera/custom-camera.page.html +++ b/src/app/features/home/custom-camera/custom-camera.page.html @@ -100,9 +100,7 @@
+
diff --git a/src/app/features/invitation/invitation.page.html b/src/app/features/invitation/invitation.page.html index 4aace173d..78d6823d4 100644 --- a/src/app/features/invitation/invitation.page.html +++ b/src/app/features/invitation/invitation.page.html @@ -1,7 +1,5 @@ - + {{ t('invitation.invitation') }} diff --git a/src/app/features/privacy/privacy.page.html b/src/app/features/privacy/privacy.page.html index 51bd4cb4f..f6e22a311 100644 --- a/src/app/features/privacy/privacy.page.html +++ b/src/app/features/privacy/privacy.page.html @@ -1,7 +1,5 @@ - + {{ t('privacy') }} diff --git a/src/app/features/profile/phone-verification/phone-verification.page.html b/src/app/features/profile/phone-verification/phone-verification.page.html index 0ae2a96e1..e82174850 100644 --- a/src/app/features/profile/phone-verification/phone-verification.page.html +++ b/src/app/features/profile/phone-verification/phone-verification.page.html @@ -1,7 +1,5 @@ - + {{ t('verification.verification') }}
diff --git a/src/app/features/profile/profile.page.html b/src/app/features/profile/profile.page.html index 66f3857ce..f6ab43a07 100644 --- a/src/app/features/profile/profile.page.html +++ b/src/app/features/profile/profile.page.html @@ -1,7 +1,5 @@ - + {{ t('profile') }}
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 @@ - + {{ t('verification.verification') }}
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": "請輸入有效轉帳金額。",