diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a0be3b59..48cd5d047 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.87.1] - 2023-12-04
+
+### Fixed
+
+1. Fix in app purchase by migrating cordove-plugin-purchase to v13 (#3125)
+
## [0.87.0] - 2023-11-30
### Added
@@ -2220,7 +2226,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.87.0...HEAD
+[unreleased]: https://github.com/numbersprotocol/capture-lite/compare/0.87.1...HEAD
+[0.87.1]: https://github.com/numbersprotocol/capture-lite/compare/0.87.0...0.87.1
[0.87.0]: https://github.com/numbersprotocol/capture-lite/compare/0.86.4...0.87.0
[0.86.4]: https://github.com/numbersprotocol/capture-lite/compare/0.83.2...0.86.4
[0.83.2]: https://github.com/numbersprotocol/capture-lite/compare/0.82.4...0.85.2
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 1aa66ef2c..c2d30e65b 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -7,8 +7,8 @@ android {
applicationId "io.numbersprotocol.capturelite"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 870
- versionName "0.87.0"
+ versionCode 871
+ versionName "0.87.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle
index 8c2576eae..6b66551e2 100644
--- a/android/app/capacitor.build.gradle
+++ b/android/app/capacitor.build.gradle
@@ -33,7 +33,7 @@ dependencies {
implementation project(':appsflyer-capacitor-plugin')
implementation project(':capacitor-blob-writer')
implementation project(':capacitor-native-settings')
- implementation "com.android.billingclient:billing:4.0.0"
+ implementation "com.android.billingclient:billing:5.2.1"
}
diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj
index fa79a5bc1..d08c49ea4 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 = 870;
+ CURRENT_PROJECT_VERSION = 871;
DEVELOPMENT_TEAM = G7NB5YCKAP;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = G7NB5YCKAP;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- MARKETING_VERSION = 0.87.0;
+ MARKETING_VERSION = 0.87.1;
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 = 870;
+ CURRENT_PROJECT_VERSION = 871;
DEVELOPMENT_TEAM = G7NB5YCKAP;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = G7NB5YCKAP;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- MARKETING_VERSION = 0.87.0;
+ MARKETING_VERSION = 0.87.1;
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 5979d24e5..25b71405e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "capture-lite",
- "version": "0.87.0",
+ "version": "0.87.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "capture-lite",
- "version": "0.87.0",
+ "version": "0.87.1",
"dependencies": {
"packages": "^0.0.8",
"@angular/animations": "^14.2.0",
@@ -19,7 +19,6 @@
"@angular/platform-browser-dynamic": "^14.2.0",
"@angular/router": "^14.2.0",
"@awesome-cordova-plugins/core": "^5.46.0",
- "@awesome-cordova-plugins/in-app-purchase-2": "^5.43.0",
"@capacitor-community/advertising-id": "^5.0.0",
"@capacitor-community/bluetooth-le": "^2.2.3",
"@capacitor-community/http": "github:numbersprotocol/http#fix-catch-disabled-Local-Network-case-on-iOS",
@@ -63,7 +62,7 @@
"capacitor-blob-writer": "^1.0.4",
"capacitor-native-settings": "^4.0.3",
"compressorjs": "^1.0.7",
- "cordova-plugin-purchase": "^11.0.0",
+ "cordova-plugin-purchase": "^13.9.0",
"ethers": "^6.8.1",
"hammerjs": "^2.0.8",
"immutable": "^4.0.0-rc.14",
@@ -1431,18 +1430,6 @@
"rxjs": "^5.5.0 || ^6.5.0 || ^7.3.0"
}
},
- "node_modules/@awesome-cordova-plugins/in-app-purchase-2": {
- "version": "5.43.0",
- "resolved": "https://registry.npmjs.org/@awesome-cordova-plugins/in-app-purchase-2/-/in-app-purchase-2-5.43.0.tgz",
- "integrity": "sha512-y292xt+DSqsIpFCe0X7yjTZkC4moPT+gYpo7C59yBR8OeZPx0LHNTth3rzeSYsVityyCjnpEIf08x0hGfKr3bw==",
- "dependencies": {
- "@types/cordova": "latest"
- },
- "peerDependencies": {
- "@awesome-cordova-plugins/core": "^5.1.0",
- "rxjs": "^5.5.0 || ^6.5.0 || ^7.3.0"
- }
- },
"node_modules/@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
@@ -8414,9 +8401,9 @@
}
},
"node_modules/cordova-plugin-purchase": {
- "version": "11.0.0",
- "resolved": "https://registry.npmjs.org/cordova-plugin-purchase/-/cordova-plugin-purchase-11.0.0.tgz",
- "integrity": "sha512-FkgOyWBYS989dYpM6d3R36VrdsHweTbmp5y7ELa1GwMYq2AG+ImdB6DJcVpyU/se/zpTndBWH/zN9s3AFKfsLg=="
+ "version": "13.9.0",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-purchase/-/cordova-plugin-purchase-13.9.0.tgz",
+ "integrity": "sha512-dzN81dlfxYMidhPQRjpo8dNeQO7Fifwsf2fsi6AWMT+aiPBf5MVtwbzRBMiSuHJnMHCuNY9oIml37zrIXwbI/w=="
},
"node_modules/core-js-compat": {
"version": "3.25.0",
@@ -25065,14 +25052,6 @@
"@types/cordova": "latest"
}
},
- "@awesome-cordova-plugins/in-app-purchase-2": {
- "version": "5.43.0",
- "resolved": "https://registry.npmjs.org/@awesome-cordova-plugins/in-app-purchase-2/-/in-app-purchase-2-5.43.0.tgz",
- "integrity": "sha512-y292xt+DSqsIpFCe0X7yjTZkC4moPT+gYpo7C59yBR8OeZPx0LHNTth3rzeSYsVityyCjnpEIf08x0hGfKr3bw==",
- "requires": {
- "@types/cordova": "latest"
- }
- },
"@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
@@ -30177,9 +30156,9 @@
}
},
"cordova-plugin-purchase": {
- "version": "11.0.0",
- "resolved": "https://registry.npmjs.org/cordova-plugin-purchase/-/cordova-plugin-purchase-11.0.0.tgz",
- "integrity": "sha512-FkgOyWBYS989dYpM6d3R36VrdsHweTbmp5y7ELa1GwMYq2AG+ImdB6DJcVpyU/se/zpTndBWH/zN9s3AFKfsLg=="
+ "version": "13.9.0",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-purchase/-/cordova-plugin-purchase-13.9.0.tgz",
+ "integrity": "sha512-dzN81dlfxYMidhPQRjpo8dNeQO7Fifwsf2fsi6AWMT+aiPBf5MVtwbzRBMiSuHJnMHCuNY9oIml37zrIXwbI/w=="
},
"core-js-compat": {
"version": "3.25.0",
diff --git a/package.json b/package.json
index 0fb03c52c..aeac9c949 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "capture-lite",
- "version": "0.87.0",
+ "version": "0.87.1",
"author": "numbersprotocol",
"homepage": "https://numbersprotocol.io/",
"scripts": {
@@ -30,7 +30,6 @@
"@angular/platform-browser-dynamic": "^14.2.0",
"@angular/router": "^14.2.0",
"@awesome-cordova-plugins/core": "^5.46.0",
- "@awesome-cordova-plugins/in-app-purchase-2": "^5.43.0",
"@capacitor-community/advertising-id": "^5.0.0",
"@capacitor-community/bluetooth-le": "^2.2.3",
"@capacitor-community/http": "github:numbersprotocol/http#fix-catch-disabled-Local-Network-case-on-iOS",
@@ -74,7 +73,7 @@
"capacitor-blob-writer": "^1.0.4",
"capacitor-native-settings": "^4.0.3",
"compressorjs": "^1.0.7",
- "cordova-plugin-purchase": "^11.0.0",
+ "cordova-plugin-purchase": "^13.9.0",
"ethers": "^6.8.1",
"hammerjs": "^2.0.8",
"immutable": "^4.0.0-rc.14",
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
index d535634eb..4d3c14ca8 100644
--- a/src/app/app.component.spec.ts
+++ b/src/app/app.component.spec.ts
@@ -1,7 +1,6 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TestBed, waitForAsync } from '@angular/core/testing';
-import { InAppPurchase2 } from '@awesome-cordova-plugins/in-app-purchase-2/ngx';
import { Platform } from '@ionic/angular';
import { AppComponent } from './app.component';
import { CapacitorPluginsTestingModule } from './shared/capacitor-plugins/capacitor-plugins-testing.module';
@@ -22,12 +21,6 @@ describe('AppComponent', () => {
is: platformIsSpy,
});
- const iap2SpyMethods = ['error', 'ready', 'when', 'refresh', 'off'];
- const inAppPurchase2Spy = jasmine.createSpyObj(
- 'InAppPurchase2',
- iap2SpyMethods
- );
-
TestBed.configureTestingModule({
declarations: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
@@ -37,10 +30,7 @@ describe('AppComponent', () => {
getTranslocoTestingModule(),
MaterialTestingModule,
],
- providers: [
- { provide: Platform, useValue: platformSpy },
- { provide: InAppPurchase2, useValue: inAppPurchase2Spy },
- ],
+ providers: [{ provide: Platform, useValue: platformSpy }],
}).compileComponents();
})
);
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index ea69db6d5..ed941933e 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -3,7 +3,6 @@ import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar';
import { BrowserModule, HammerModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouteReuseStrategy } from '@angular/router';
-import { InAppPurchase2 } from '@awesome-cordova-plugins/in-app-purchase-2/ngx';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { FormlyModule } from '@ngx-formly/core';
import { FormlyMaterialModule } from '@ngx-formly/material';
@@ -36,7 +35,6 @@ import { SharedModule } from './shared/shared.module';
provide: MAT_SNACK_BAR_DEFAULT_OPTIONS,
useValue: { duration: 2500 },
},
- InAppPurchase2,
],
bootstrap: [AppComponent],
})
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 2c7a832a9..9ef6dc1ea 100644
--- a/src/app/features/wallets/buy-num/buy-num.page.html
+++ b/src/app/features/wallets/buy-num/buy-num.page.html
@@ -25,7 +25,7 @@
>
- {{ product.inAppProduct.price }}
+ {{ product.inAppProduct.pricing?.price }}
@@ -38,9 +38,6 @@
{{
t('wallets.buyCredits.buyXCredits', { credits: product.numPoints })
}}
-
diff --git a/src/app/features/wallets/buy-num/buy-num.page.spec.ts b/src/app/features/wallets/buy-num/buy-num.page.spec.ts
index a55dc6b13..9429bd718 100644
--- a/src/app/features/wallets/buy-num/buy-num.page.spec.ts
+++ b/src/app/features/wallets/buy-num/buy-num.page.spec.ts
@@ -1,5 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { InAppPurchase2 } from '@awesome-cordova-plugins/in-app-purchase-2/ngx';
import { SharedTestingModule } from '../../../shared/shared-testing.module';
import { BuyNumPage } from './buy-num.page';
@@ -10,16 +9,9 @@ describe('BuyNumPage', () => {
beforeEach(
waitForAsync(() => {
- const iap2SpyMethods = ['error', 'ready', 'when', 'refresh', 'off'];
- const inAppPurchase2Spy = jasmine.createSpyObj(
- 'InAppPurchase2',
- iap2SpyMethods
- );
-
TestBed.configureTestingModule({
declarations: [BuyNumPage],
imports: [SharedTestingModule],
- providers: [{ provide: InAppPurchase2, useValue: inAppPurchase2Spy }],
}).compileComponents();
fixture = TestBed.createComponent(BuyNumPage);
diff --git a/src/app/features/wallets/buy-num/buy-num.page.ts b/src/app/features/wallets/buy-num/buy-num.page.ts
index 8eaae3e31..800e0a896 100644
--- a/src/app/features/wallets/buy-num/buy-num.page.ts
+++ b/src/app/features/wallets/buy-num/buy-num.page.ts
@@ -1,11 +1,13 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
-import { IAPProduct } from '@awesome-cordova-plugins/in-app-purchase-2/ngx';
import { AlertController } from '@ionic/angular';
import { TranslocoService } from '@ngneat/transloco';
+import { UntilDestroy } from '@ngneat/until-destroy';
import { combineLatest } from 'rxjs';
-import { map, tap } from 'rxjs/operators';
+import { filter, first, map, tap } from 'rxjs/operators';
+import { BlockingActionService } from '../../../shared/blocking-action/blocking-action.service';
import { InAppStoreService } from '../../../shared/in-app-store/in-app-store.service';
+@UntilDestroy()
@Component({
selector: 'app-buy-num',
templateUrl: './buy-num.page.html',
@@ -34,21 +36,33 @@ export class BuyNumPage implements OnInit {
tap(_ => this.ref.detectChanges())
);
+ readonly isProcessingOrder$ = this.store.isProcessingOrder$;
+
constructor(
private readonly store: InAppStoreService,
private readonly ref: ChangeDetectorRef,
private readonly alertController: AlertController,
- private readonly translocoService: TranslocoService
+ private readonly translocoService: TranslocoService,
+ private readonly blockingActionService: BlockingActionService
) {}
ngOnInit() {
this.store.refreshNumPointsPricing();
}
- purchase(product: IAPProduct) {
+ purchase(product: CdvPurchase.Product) {
+ this.showLoadingIndicatorUntillOrderIsProcessed();
this.store.purchase(product);
}
+ private showLoadingIndicatorUntillOrderIsProcessed() {
+ const action$ = this.isProcessingOrder$.pipe(
+ filter(isProcessing => isProcessing === false),
+ first()
+ );
+ this.blockingActionService.run$(action$).subscribe();
+ }
+
async showNumPointsQuantity(numPoints: number) {
const info = this.translocoService.translate(
'wallets.buyCredits.thisPackageIncludeXCredits',
diff --git a/src/app/shared/in-app-store/in-app-store.service.spec.ts b/src/app/shared/in-app-store/in-app-store.service.spec.ts
index 37e304a94..f479b7174 100644
--- a/src/app/shared/in-app-store/in-app-store.service.spec.ts
+++ b/src/app/shared/in-app-store/in-app-store.service.spec.ts
@@ -1,5 +1,4 @@
import { TestBed } from '@angular/core/testing';
-import { InAppPurchase2 } from '@awesome-cordova-plugins/in-app-purchase-2/ngx';
import { SharedModule } from '../shared.module';
import { InAppStoreService } from './in-app-store.service';
@@ -10,7 +9,6 @@ describe('InAppStoreService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [SharedModule],
- providers: [InAppPurchase2],
});
service = TestBed.inject(InAppStoreService);
});
diff --git a/src/app/shared/in-app-store/in-app-store.service.ts b/src/app/shared/in-app-store/in-app-store.service.ts
index f8e33b64d..71ae412b3 100644
--- a/src/app/shared/in-app-store/in-app-store.service.ts
+++ b/src/app/shared/in-app-store/in-app-store.service.ts
@@ -1,11 +1,8 @@
+import { HttpErrorResponse } from '@angular/common/http';
import { Injectable, OnDestroy } from '@angular/core';
-import {
- IAPError,
- IAPProduct,
- InAppPurchase2,
-} from '@awesome-cordova-plugins/in-app-purchase-2/ngx';
import { Platform, ToastController } from '@ionic/angular';
import { TranslocoService } from '@ngneat/transloco';
+import 'cordova-plugin-purchase';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import {
@@ -24,9 +21,11 @@ import { ErrorService } from '../error/error.service';
export class InAppStoreService implements OnDestroy {
debugPrint = setupInAppPurchaseDebugPrint('InAppStoreService');
- readonly inAppProducts$ = new BehaviorSubject([]);
+ readonly inAppProducts$ = new BehaviorSubject([]);
readonly numPointPricesById$ = new BehaviorSubject({});
+ private store!: CdvPurchase.Store;
+
readonly inAppProductsWithNumpoints$ = combineLatest([
this.inAppProducts$,
this.numPointPricesById$,
@@ -34,7 +33,7 @@ export class InAppStoreService implements OnDestroy {
map(([inAppProducts, numPointPricesById]) => {
return inAppProducts.map(inAppProduct => {
const numPoints = this.numPointsForProduct(
- inAppProduct,
+ inAppProduct.id,
numPointPricesById
);
return { inAppProduct, numPoints };
@@ -42,10 +41,11 @@ export class InAppStoreService implements OnDestroy {
})
);
+ readonly isProcessingOrder$ = new BehaviorSubject(false);
+
private readonly appId = 'io.numbersprotocol.capturelite';
constructor(
- private readonly store: InAppPurchase2,
private readonly platform: Platform,
private readonly errorService: ErrorService,
private readonly toastController: ToastController,
@@ -63,11 +63,10 @@ export class InAppStoreService implements OnDestroy {
try {
await this.platform.ready();
-
+ this.store = CdvPurchase.store;
this.regiseterStoreListeners();
this.registerStoreProducts();
-
- this.store.refresh();
+ this.store.initialize();
} catch (error) {
const errorMessage = this.translocoService.translate(
'inAppPurchase.failedToInitInAppStore'
@@ -98,30 +97,40 @@ export class InAppStoreService implements OnDestroy {
}
}
- purchase(product: IAPProduct) {
- this.store.order(product);
+ purchase(product: CdvPurchase.Product) {
+ const offer = product.getOffer();
+ if (!offer) return;
+ this.isProcessingOrder$.next(true);
+ this.store.order(offer);
}
- private async finishPurchase(inAppProduct: IAPProduct) {
- const pointsToAdd = this.numPointsForProduct(
- inAppProduct,
- this.numPointPricesById$.value
- );
-
- let receipt;
- if (inAppProduct.transaction?.type === 'ios-appstore') {
- receipt = inAppProduct.transaction.appStoreReceipt;
+ private async finishPurchase(receipt: CdvPurchase.VerifiedReceipt) {
+ const product = this.extractProductFromReceipt(receipt);
+ if (!product) {
+ receipt.finish();
+ this.isProcessingOrder$.next(false);
+ return;
}
- if (inAppProduct.transaction?.type === 'android-playstore') {
- receipt = inAppProduct.transaction.receipt;
+
+ const storeReceipt = this.extractStoreReceipt(receipt);
+ if (!storeReceipt) {
+ receipt.finish();
+ this.isProcessingOrder$.next(false);
+ return;
}
- if (!receipt) return;
try {
+ const pointsToAdd = this.numPointsForProduct(
+ product.id,
+ this.numPointPricesById$.value
+ );
+
await this.diaBackendNumService
- .purchaseNumPoints$(pointsToAdd, receipt)
+ .purchaseNumPoints$(pointsToAdd, storeReceipt)
.toPromise();
- inAppProduct.finish();
+
+ receipt.finish();
+ this.isProcessingOrder$.next(false);
this.notifyUser(
this.translocoService.translate('wallets.buyCredits.xCreditsAdded', {
@@ -129,10 +138,63 @@ export class InAppStoreService implements OnDestroy {
})
);
} catch (error) {
- const errorMessage = this.translocoService.translate(
- 'wallets.buyCredits.failedToAddCredits'
- );
- this.errorService.toastError$(errorMessage).toPromise();
+ if (
+ error instanceof HttpErrorResponse &&
+ error.error.error?.type === 'duplicate_receipt_id'
+ ) {
+ /**
+ * The receipt has already been used to get NUM points.
+ *
+ * In case of duplicate receipt id, the user has already received the points
+ * and we can ignore the error. Duplicate receipt can happen if callbacks
+ * registered in CdvPurchase is called twice. Issue is more related to plugin itslef:
+ * https://github.com/j3k0/cordova-plugin-purchase/issues/1458
+ *
+ * Thanks to our backend implementation, the user will not be given NUMs twice.
+ * In this case we just finish the receipt and make sure loading indicator is hidden.
+ */
+ receipt.finish();
+ this.isProcessingOrder$.next(false);
+ } else {
+ this.errorService
+ .toastError$(
+ this.translocoService.translate(
+ 'wallets.buyCredits.failedToAddCredits'
+ )
+ )
+ .toPromise();
+ }
+ }
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ private extractProductFromReceipt(receipt: CdvPurchase.VerifiedReceipt) {
+ for (const transaction of receipt.sourceReceipt.transactions) {
+ for (const product of transaction.products) {
+ const isIncluded = Object.values(
+ CaptureInAppProductIds
+ ).includes(product.id);
+ if (isIncluded) return product;
+ }
+ }
+ return null;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ private extractStoreReceipt(
+ receipt: CdvPurchase.VerifiedReceipt
+ ): string | undefined {
+ const platform = receipt.sourceReceipt.platform;
+
+ if (platform === CdvPurchase.Platform.APPLE_APPSTORE) {
+ // nativeData is not documented, but it is there (can be veified by console.log(receipt))
+ return (receipt.sourceReceipt as any).nativeData.appStoreReceipt;
+ }
+
+ if (platform === CdvPurchase.Platform.GOOGLE_PLAY) {
+ // nativePurchase is not documented, but it is there (can be veified by console.log(receipt))
+ return (receipt.sourceReceipt.transactions[0] as any).nativePurchase
+ .receipt;
}
}
@@ -145,9 +207,9 @@ export class InAppStoreService implements OnDestroy {
private regiseterStoreListeners() {
this.store.error(this.onStoreError);
this.store.ready(this.onStoreReady);
- this.store.when('product').approved(this.onStoreProductApproved);
- this.store.when('product').updated(this.onStoreProductUpdated);
- this.store.when('product').verified(this.onStoreProductVerified);
+ this.store.when().approved(this.onStoreProductApproved);
+ this.store.when().productUpdated(this.onStoreProductUpdated);
+ this.store.when().verified(this.onStoreProductVerified);
}
private unregisterStoreListeners() {
@@ -159,78 +221,64 @@ export class InAppStoreService implements OnDestroy {
}
private registerStoreProducts() {
- const consumableProductIds = [
- CaptureInAppProductIds.BRONZE_PACK,
- CaptureInAppProductIds.SLIVER_PACK,
- CaptureInAppProductIds.GOLD_PACK,
- CaptureInAppProductIds.PLATINUM_PACK,
- ];
- const type = this.store.CONSUMABLE;
+ const consumableProductIds = Object.values(CaptureInAppProductIds);
+ const type = CdvPurchase.ProductType.CONSUMABLE;
+ const appstorePlatform = CdvPurchase.Platform.APPLE_APPSTORE;
+ const googlePlayPlatform = CdvPurchase.Platform.GOOGLE_PLAY;
+ const productsToRegister: CdvPurchase.IRegisterProduct[] = [];
for (const id of consumableProductIds) {
- this.store.register({ id, type });
+ productsToRegister.push({ id, type, platform: appstorePlatform });
+ productsToRegister.push({ id, type, platform: googlePlayPlatform });
}
+
+ this.store.register(productsToRegister);
}
- private readonly onStoreError = (_: IAPError) => {
+ private readonly onStoreError = (error: CdvPurchase.IError) => {
+ this.isProcessingOrder$.next(false);
+
+ if (error.message === 'The user cancelled the order.') return;
+
const errorMessage = this.translocoService.translate(
'inAppPurchase.inAppPurchaseErrorOcurred'
);
this.errorService.toastError$(errorMessage).toPromise();
+ // TODO: report to remote error service
};
private readonly onStoreReady = () => {
- const inAppProducts = this.store.products.filter(
- product => this.shouldIgnoreProduct(product) === false
- );
- this.inAppProducts$.next(inAppProducts);
+ this.inAppProducts$.next(this.store.products);
};
- private readonly onStoreProductUpdated = (updatedProduct: IAPProduct) => {
- if (this.shouldIgnoreProduct(updatedProduct)) {
- return;
- }
-
+ private readonly onStoreProductUpdated = (
+ updatedProduct: CdvPurchase.Product
+ ) => {
this.debugPrint('onStoreProductUpdated', updatedProduct);
-
- const inAppProducts = this.inAppProducts$.value.map(product =>
- product.id === updatedProduct.id ? updatedProduct : product
- );
-
- this.inAppProducts$.next(inAppProducts);
+ this.inAppProducts$.next(this.store.products);
};
- private readonly onStoreProductApproved = (product: IAPProduct) => {
- if (this.shouldIgnoreProduct(product)) {
- return;
- }
- this.debugPrint('onStoreProductApproved', product);
- // TODO: in the future add validation logic here
- product.verify();
+ private readonly onStoreProductApproved = (
+ transacction: CdvPurchase.Transaction
+ ) => {
+ this.debugPrint('onStoreProductApproved', transacction);
+ transacction.verify();
};
- private readonly onStoreProductVerified = (product: IAPProduct) => {
- if (this.shouldIgnoreProduct(product)) {
- return;
- }
- this.debugPrint('onStoreProductVerified', product);
- this.finishPurchase(product);
+ private readonly onStoreProductVerified = (
+ receipt: CdvPurchase.VerifiedReceipt
+ ) => {
+ this.debugPrint('onStoreProductVerified', receipt);
+ this.finishPurchase(receipt);
};
- private shouldIgnoreProduct(product: IAPProduct) {
- // For some reason on iOS there will be 1 in app product
- // with product.id === io.numbersprotocol.capturelite
- // we should ignore that product
- return product.id === this.appId;
- }
-
// eslint-disable-next-line class-methods-use-this
private numPointsForProduct(
- product: IAPProduct,
+ productId: string,
numPriceListById: NumPointPricesById
) {
- if (product.id in numPriceListById) {
- return numPriceListById[product.id].quantitiy;
+ if (productId in numPriceListById) {
+ return numPriceListById[productId].quantitiy;
}
return 0;
}
@@ -248,7 +296,7 @@ export enum CaptureInAppProductIds {
}
interface InAppProductsWithNumPoint {
- inAppProduct: IAPProduct;
+ inAppProduct: CdvPurchase.Product;
numPoints: number;
}
diff --git a/src/app/utils/in-app-purchase.ts b/src/app/utils/in-app-purchase.ts
index 38b0539d0..de74a70a2 100644
--- a/src/app/utils/in-app-purchase.ts
+++ b/src/app/utils/in-app-purchase.ts
@@ -1,5 +1,4 @@
import { isDevMode } from '@angular/core';
-import { IAPProduct } from '@awesome-cordova-plugins/in-app-purchase-2/ngx';
import { CaptureInAppProductIds } from '../shared/in-app-store/in-app-store.service';
export function truncateReceipt(recipt: string) {
@@ -32,72 +31,66 @@ export function setupInAppPurchaseDebugPrint(tag: string) {
* does not work in Web environment therefore we can use this util function
* to pupulate with mock product to develop UI with different product states
*/
-export function generateMockInAppProducts(): IAPProduct[] {
- const mockInAppProductSample: IAPProduct = {
+export function generateMockInAppProducts(): CdvPurchase.Product[] {
+ const mockInAppProductSample: CdvPurchase.Product = {
id: 'string',
- alias: 'string',
- type: 'string',
- state: 'string',
+ className: 'Product',
+ platform: CdvPurchase.Platform.TEST,
+ offers: [],
+ pricing: undefined,
+ type: CdvPurchase.ProductType.CONSUMABLE,
title: 'string',
description: 'string',
- priceMicros: 0,
- price: 'string',
- currency: 'string',
- loaded: true,
- valid: true,
canPurchase: true,
owned: true,
- finish: () => ({}),
- verify: () => ({}),
- set: (_: string, __: any) => ({}),
- stateChanged: () => ({}),
- on: (_: string, __: any) => ({}),
- once: (_: string, __: any) => ({}),
- off: (_: any) => ({}),
- trigger: (_: string, __: any) => ({}),
+ getOffer: () => undefined,
+ addOffer: () => ({} as CdvPurchase.Product),
};
return [
{
...mockInAppProductSample,
id: CaptureInAppProductIds.BRONZE_PACK,
- title: 'Bronze Pack',
- price: '0.99',
- currency: 'USD',
+ title: '100 NumPoints',
+ description: '100 NumPoints',
+ pricing: undefined,
canPurchase: true,
- state: 'valid',
- type: 'CONSUMABLE',
+ owned: true,
+ getOffer: () => undefined,
+ addOffer: () => ({} as CdvPurchase.Product),
},
{
...mockInAppProductSample,
-
id: CaptureInAppProductIds.SLIVER_PACK,
- title: 'Silver Pack',
- price: '1.99',
- state: 'valid',
- canPurchase: false,
- currency: 'USD',
- type: 'CONSUMABLE',
+ title: '500 NumPoints',
+ description: '500 NumPoints',
+ pricing: undefined,
+ canPurchase: true,
+ owned: true,
+ getOffer: () => undefined,
+ addOffer: () => ({} as CdvPurchase.Product),
},
{
...mockInAppProductSample,
id: CaptureInAppProductIds.GOLD_PACK,
- title: 'Gold Pack',
- price: '2.99',
- state: 'valid',
- currency: 'USD',
- type: 'CONSUMABLE',
+ title: '1000 NumPoints',
+ description: '1000 NumPoints',
+ pricing: undefined,
canPurchase: true,
+ owned: true,
+ getOffer: () => undefined,
+ addOffer: () => ({} as CdvPurchase.Product),
},
{
...mockInAppProductSample,
id: CaptureInAppProductIds.PLATINUM_PACK,
- title: 'Platinum Pack',
- price: '3.99',
- state: 'valid',
- currency: 'USD',
- type: 'CONSUMABLE',
+ title: '5000 NumPoints',
+ description: '5000 NumPoints',
+ pricing: undefined,
canPurchase: true,
+ owned: true,
+ getOffer: () => undefined,
+ addOffer: () => ({} as CdvPurchase.Product),
},
];
}