From 0fbce999705b8971d10650cfa72f6e941f9db192 Mon Sep 17 00:00:00 2001 From: sultanmyrza Date: Mon, 20 Jun 2022 19:22:08 +0800 Subject: [PATCH 01/17] install in-app-purchase-2 plugin --- android/app/capacitor.build.gradle | 2 +- android/app/src/main/res/xml/config.xml | 4 ++ ios/App/App/config.xml | 5 ++ ios/App/Podfile | 1 + package-lock.json | 63 +++++++++++++++++++++++++ package.json | 2 + 6 files changed, 76 insertions(+), 1 deletion(-) diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 3f80fddfe..797a56675 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -28,7 +28,7 @@ dependencies { implementation project(':numbersprotocol-preview-camera') implementation project(':numbersprotocol-preview-video') implementation project(':capacitor-blob-writer') - + implementation "com.android.billingclient:billing:4.0.0" } diff --git a/android/app/src/main/res/xml/config.xml b/android/app/src/main/res/xml/config.xml index 1b1b0e0dc..404a1f136 100644 --- a/android/app/src/main/res/xml/config.xml +++ b/android/app/src/main/res/xml/config.xml @@ -2,5 +2,9 @@ + + + + \ No newline at end of file diff --git a/ios/App/App/config.xml b/ios/App/App/config.xml index 1b1b0e0dc..1a9662ebc 100644 --- a/ios/App/App/config.xml +++ b/ios/App/App/config.xml @@ -2,5 +2,10 @@ + + + + + \ No newline at end of file diff --git a/ios/App/Podfile b/ios/App/Podfile index 3a6b06b05..b3c63f989 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -28,6 +28,7 @@ def capacitor_pods pod 'NumbersprotocolPreviewCamera', :path => '../../node_modules/@numbersprotocol/preview-camera' pod 'NumbersprotocolPreviewVideo', :path => '../../node_modules/@numbersprotocol/preview-video' pod 'CapacitorBlobWriter', :path => '../../node_modules/capacitor-blob-writer' + pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins' end target 'App' do diff --git a/package-lock.json b/package-lock.json index d3041c73f..ccaba08bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@angular/platform-browser": "^12.2.4", "@angular/platform-browser-dynamic": "^12.2.4", "@angular/router": "^12.2.4", + "@awesome-cordova-plugins/in-app-purchase-2": "^5.43.0", "@capacitor-community/bluetooth-le": "^1.7.0", "@capacitor-community/http": "github:numbersprotocol/http#fix-catch-disabled-Local-Network-case-on-iOS", "@capacitor-community/wifi": "github:numbersprotocol/community-capacitor-wifi#capacitor3", @@ -52,6 +53,7 @@ "buffer": "^5.7.1", "capacitor-blob-writer": "^1.0.4", "compressorjs": "^1.0.7", + "cordova-plugin-purchase": "^11.0.0", "immutable": "^4.0.0-rc.14", "lodash-es": "^4.17.21", "material-design-icons-iconfont": "^6.1.0", @@ -912,6 +914,30 @@ "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", "dev": true }, + "node_modules/@awesome-cordova-plugins/core": { + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@awesome-cordova-plugins/core/-/core-5.43.0.tgz", + "integrity": "sha512-DDLzEYtP6fDqyVORwuzXH64VFYTcW5qoaXAvghWtc5O+wciqeX1hFO7WY7l+1Ytkf6J4IbyMhrsOgZb3bC1eMQ==", + "peer": true, + "dependencies": { + "@types/cordova": "latest" + }, + "peerDependencies": { + "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.14.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", @@ -4271,6 +4297,11 @@ "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==", "dev": true }, + "node_modules/@types/cordova": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", + "integrity": "sha512-rkiiTuf/z2wTd4RxFOb+clE7PF4AEJU0hsczbUdkHHBtkUmpWQpEddynNfJYKYtZFJKbq4F+brfekt1kx85IZA==" + }, "node_modules/@types/cors": { "version": "2.8.10", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz", @@ -7225,6 +7256,11 @@ "url": "https://opencollective.com/webpack" } }, + "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==" + }, "node_modules/core-js": { "version": "3.16.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.0.tgz", @@ -25967,6 +26003,23 @@ "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", "dev": true }, + "@awesome-cordova-plugins/core": { + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@awesome-cordova-plugins/core/-/core-5.43.0.tgz", + "integrity": "sha512-DDLzEYtP6fDqyVORwuzXH64VFYTcW5qoaXAvghWtc5O+wciqeX1hFO7WY7l+1Ytkf6J4IbyMhrsOgZb3bC1eMQ==", + "peer": true, + "requires": { + "@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.14.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", @@ -28370,6 +28423,11 @@ "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==", "dev": true }, + "@types/cordova": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", + "integrity": "sha512-rkiiTuf/z2wTd4RxFOb+clE7PF4AEJU0hsczbUdkHHBtkUmpWQpEddynNfJYKYtZFJKbq4F+brfekt1kx85IZA==" + }, "@types/cors": { "version": "2.8.10", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz", @@ -30700,6 +30758,11 @@ } } }, + "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==" + }, "core-js": { "version": "3.16.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.0.tgz", diff --git a/package.json b/package.json index 219821e8a..3640b5d18 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@angular/platform-browser": "^12.2.4", "@angular/platform-browser-dynamic": "^12.2.4", "@angular/router": "^12.2.4", + "@awesome-cordova-plugins/in-app-purchase-2": "^5.43.0", "@capacitor-community/bluetooth-le": "^1.7.0", "@capacitor-community/http": "github:numbersprotocol/http#fix-catch-disabled-Local-Network-case-on-iOS", "@capacitor-community/wifi": "github:numbersprotocol/community-capacitor-wifi#capacitor3", @@ -63,6 +64,7 @@ "buffer": "^5.7.1", "capacitor-blob-writer": "^1.0.4", "compressorjs": "^1.0.7", + "cordova-plugin-purchase": "^11.0.0", "immutable": "^4.0.0-rc.14", "lodash-es": "^4.17.21", "material-design-icons-iconfont": "^6.1.0", From 42c5ab5d9a0cf13c8e6e5e407265c2c1dce5334f Mon Sep 17 00:00:00 2001 From: sultanmyrza Date: Mon, 20 Jun 2022 19:22:33 +0800 Subject: [PATCH 02/17] add android billing permission --- android/app/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 580396b24..2ceb870d7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -44,6 +44,8 @@ + + From 086a962a99827fe9c75149ffb57eee4e5b5a2cb4 Mon Sep 17 00:00:00 2001 From: sultanmyrza Date: Mon, 20 Jun 2022 19:23:25 +0800 Subject: [PATCH 03/17] add StoreKit config to test in-app purchases locally on iOS --- ios/App/App.xcodeproj/project.pbxproj | 2 + ios/App/Configuration.storekit | 78 +++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 ios/App/Configuration.storekit diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 063bf1ee4..f9c84841e 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; DE095CFF277AF00900242276 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + DE2706742854C3960046512E /* Configuration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Configuration.storekit; sourceTree = ""; }; DE28998F27B790A900F6581C /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -58,6 +59,7 @@ 504EC2FB1FED79650016851F = { isa = PBXGroup; children = ( + DE2706742854C3960046512E /* Configuration.storekit */, 504EC3061FED79650016851F /* App */, 504EC3051FED79650016851F /* Products */, 7F8756D8B27F46E3366F6CEA /* Pods */, diff --git a/ios/App/Configuration.storekit b/ios/App/Configuration.storekit new file mode 100644 index 000000000..ac072982e --- /dev/null +++ b/ios/App/Configuration.storekit @@ -0,0 +1,78 @@ +{ + "identifier" : "509552BA", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + { + "displayPrice" : "0.99", + "familyShareable" : false, + "internalID" : "68D9071C", + "localizations" : [ + { + "description" : "Bronze Pack (Description)", + "displayName" : "Bronze Pack", + "locale" : "en_US" + } + ], + "productID" : "cap_lite_consumable_bronze_pack_099", + "referenceName" : "Bronze Pack", + "type" : "Consumable" + }, + { + "displayPrice" : "1.99", + "familyShareable" : false, + "internalID" : "9A0F041B", + "localizations" : [ + { + "description" : "Silver Pack (Description)", + "displayName" : "Silver Pack", + "locale" : "en_US" + } + ], + "productID" : "cap_lite_consumable_silver_pack_199", + "referenceName" : "Silver Pack", + "type" : "Consumable" + }, + { + "displayPrice" : "2.99", + "familyShareable" : false, + "internalID" : "D92B12C1", + "localizations" : [ + { + "description" : "Gold Pack (Description)", + "displayName" : "Gold Pack", + "locale" : "en_US" + } + ], + "productID" : "cap_lite_consumable_gold_pack_299", + "referenceName" : "Gold Pack", + "type" : "Consumable" + }, + { + "displayPrice" : "3.99", + "familyShareable" : false, + "internalID" : "D0441E1B", + "localizations" : [ + { + "description" : "Platinum Pack (Description)", + "displayName" : "Platinum Pack", + "locale" : "en_US" + } + ], + "productID" : "cap_lite_consumable_platinum_pack_399", + "referenceName" : "Platinum Pack", + "type" : "Consumable" + } + ], + "settings" : { + + }, + "subscriptionGroups" : [ + + ], + "version" : { + "major" : 1, + "minor" : 2 + } +} From 578db44f626383e1b90dec7fdd7537574c358a02 Mon Sep 17 00:00:00 2001 From: sultanmyrza Date: Mon, 20 Jun 2022 19:24:01 +0800 Subject: [PATCH 04/17] add translations for In App Purchases --- src/assets/i18n/en-us.json | 12 +++++++++++- src/assets/i18n/zh-tw.json | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/assets/i18n/en-us.json b/src/assets/i18n/en-us.json index d3a2671eb..b2342bc1b 100644 --- a/src/assets/i18n/en-us.json +++ b/src/assets/i18n/en-us.json @@ -277,7 +277,13 @@ "gasFee": "Gas Fee", "pending": "Pending", "total": "Total", - "calculateGasFee": "Calculate Gas" + "calculateGasFee": "Calculate Gas", + "buyNum": { + "buyNum": "Buy NUM", + "buyPoints": "Buy {{points}} Points", + "pointsAdded": "{{points}} points added", + "failedToAddPoints": "Failed to add points" + } }, "invitation": { "invitation": "Invitation", @@ -337,5 +343,9 @@ "pleaseUpdateTheAppForProperFunctioning": "Please update the app for proper functioning", "remindMeTomorrow": "REMIND ME TOMORROW", "updateNow": "UPDATE NOW" + }, + "inAppPurchase": { + "failedToInitInAppStore": "An error occurred during the initialization of the In-App Store. Please check your Internet connection quality.", + "inAppPurchaseErrorOcurred": "In-App Purchase error occurred. Please try again later." } } diff --git a/src/assets/i18n/zh-tw.json b/src/assets/i18n/zh-tw.json index 47d4fc667..497d57a4e 100644 --- a/src/assets/i18n/zh-tw.json +++ b/src/assets/i18n/zh-tw.json @@ -277,7 +277,13 @@ "gasFee": "油費", "pending": "待計算", "total": "總額", - "calculateGasFee": "計算油費" + "calculateGasFee": "計算油費", + "buyNum": { + "buyNum": "購買 NUM", + "buyPoints": "買{{points}}分", + "pointsAdded": "加了{{points}}分", + "failedToAddPoints": "加分失敗" + } }, "invitation": { "invitation": "邀請", @@ -337,5 +343,9 @@ "pleaseUpdateTheAppForProperFunctioning": "請更新應用程序以使其正常運行", "remindMeTomorrow": "明天提醒我", "updateNow": "現在更新" + }, + "inAppPurchase": { + "failedToInitInAppStore": "In-App Store 初始化期間發生錯誤。 請檢查您的互聯網連接質量。", + "inAppPurchaseErrorOcurred": "發生應用內購買錯誤。 請稍後再試。" } } From f1d3c5583a19fe0cb9c1d19e064f4e4882fc335b Mon Sep 17 00:00:00 2001 From: sultanmyrza Date: Mon, 20 Jun 2022 19:24:43 +0800 Subject: [PATCH 05/17] add env var for BUBBLE_API endpoint --- set-secret.js | 1 + 1 file changed, 1 insertion(+) diff --git a/set-secret.js b/set-secret.js index 4f1ab3771..bc28b3298 100644 --- a/set-secret.js +++ b/set-secret.js @@ -8,6 +8,7 @@ const envConfigFile = ` export const BASE_URL = '${process.env.NUMBERS_STORAGE_BASE_URL}'; export const TRUSTED_CLIENT_KEY = '${process.env.NUMBERS_STORAGE_TRUSTED_CLIENT_KEY}'; export const BUBBLE_DB_URL = '${process.env.NUMBERS_BUBBLE_DB_URL}'; +export const BUBBLE_API_URL = '${process.env.BUBBLE_API_URL}'; `; fs.writeFile(targetPath, envConfigFile, err => { if (err) { From f3e7d8df8f0cee8c67bdbd1e1e4830b8dca8cec3 Mon Sep 17 00:00:00 2001 From: sultanmyrza Date: Mon, 20 Jun 2022 19:25:16 +0800 Subject: [PATCH 06/17] add utility functions for in app purchases --- src/app/utils/in-app-purchase.ts | 97 ++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/app/utils/in-app-purchase.ts diff --git a/src/app/utils/in-app-purchase.ts b/src/app/utils/in-app-purchase.ts new file mode 100644 index 000000000..7ce396de4 --- /dev/null +++ b/src/app/utils/in-app-purchase.ts @@ -0,0 +1,97 @@ +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'; + +/** + * Usefull to see in app product state changes in console for better debugging. + * It will pring to console only in dev mode aka isDevMode() === true + */ +export function setupInAppPurchaseDebugPrint(tag: string) { + return function (message: string, data?: any) { + if (!isDevMode()) return; + + // eslint-disable-next-line no-console + console.log(`${tag}: ${message}`); + + if (data) { + const tabIndent = 4; + // eslint-disable-next-line no-console + console.log(`${JSON.stringify(data, null, tabIndent)}`); + } + }; +} + +/** + * Usefull during UI development in Web environment. In App purchase plugin + * 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 = { + id: 'string', + alias: 'string', + type: 'string', + state: 'string', + 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) => ({}), + }; + + return [ + { + ...mockInAppProductSample, + id: CaptureInAppProductIds.BRONZE_PACK, + title: 'Bronze Pack', + price: '0.99', + currency: 'USD', + canPurchase: true, + state: 'valid', + type: 'CONSUMABLE', + }, + { + ...mockInAppProductSample, + + id: CaptureInAppProductIds.SLIVER_PACK, + title: 'Silver Pack', + price: '1.99', + state: 'valid', + canPurchase: false, + currency: 'USD', + type: 'CONSUMABLE', + }, + { + ...mockInAppProductSample, + id: CaptureInAppProductIds.GOLD_PACK, + title: 'Gold Pack', + price: '2.99', + state: 'valid', + currency: 'USD', + type: 'CONSUMABLE', + canPurchase: true, + }, + { + ...mockInAppProductSample, + id: CaptureInAppProductIds.PLATINUM_PACK, + title: 'Platinum Pack', + price: '3.99', + state: 'valid', + currency: 'USD', + type: 'CONSUMABLE', + canPurchase: true, + }, + ]; +} From 00fcfb36127692353ad35ea9d38a60931bcc1b6b Mon Sep 17 00:00:00 2001 From: sultanmyrza Date: Mon, 20 Jun 2022 19:53:58 +0800 Subject: [PATCH 07/17] add dia-backend-num service --- .../num/dia-backend-num.service.spec.ts | 17 +++++ .../num/dia-backend-num.service.ts | 73 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/app/shared/dia-backend/num/dia-backend-num.service.spec.ts create mode 100644 src/app/shared/dia-backend/num/dia-backend-num.service.ts diff --git a/src/app/shared/dia-backend/num/dia-backend-num.service.spec.ts b/src/app/shared/dia-backend/num/dia-backend-num.service.spec.ts new file mode 100644 index 000000000..39f47506b --- /dev/null +++ b/src/app/shared/dia-backend/num/dia-backend-num.service.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; +import { SharedModule } from '../../shared.module'; + +import { DiaBackendNumService } from './dia-backend-num.service'; + +describe('DiaBackendNumService', () => { + let service: DiaBackendNumService; + + beforeEach(() => { + TestBed.configureTestingModule({ imports: [SharedModule] }); + service = TestBed.inject(DiaBackendNumService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shared/dia-backend/num/dia-backend-num.service.ts b/src/app/shared/dia-backend/num/dia-backend-num.service.ts new file mode 100644 index 000000000..1c902310d --- /dev/null +++ b/src/app/shared/dia-backend/num/dia-backend-num.service.ts @@ -0,0 +1,73 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { defer } from 'rxjs'; +import { concatMap } from 'rxjs/operators'; +import { DiaBackendAuthService } from '../auth/dia-backend-auth.service'; +import { BASE_URL, BUBBLE_API_URL } from '../secret'; + +@Injectable({ + providedIn: 'root', +}) +export class DiaBackendNumService { + constructor( + private readonly httpClient: HttpClient, + private readonly authService: DiaBackendAuthService + ) {} + + purchaseNumPoints$(pointsToAdd: number, receiptId: string) { + return defer(() => this.authService.getAuthHeadersWithApiKey()).pipe( + concatMap(headers => { + const formData = new FormData(); + formData.set('points', pointsToAdd.toString()); + formData.set('receipt_id', receiptId); + return this.httpClient.post( + `${BASE_URL}/api/v3/num/points/purchase/`, + formData, + { headers } + ); + }) + ); + } + + numPointsPriceList$() { + // ask @ethan wu to change bubble endpoint from POST to GET + return defer(() => + this.httpClient.post( + `${BUBBLE_API_URL}/version-num-points-price-list/api/1.1/wf/num-points-price-list`, + {} + ) + ); + } +} + +export interface DiaBackendNumPointPurchaseResult { + id: string; + user: string; + type: + | 'airdrop' + | 'purchase' + | 'user_reward' + | 'order' + | 'network_app_order' + | 'existing'; + event_identifier: string; + receipt_id: string; + points: string; + spent: string; + created_at: string; + expired_at: string; +} + +export interface NumPointPrice { + id: number; + inAppPurchaseId: string; + // ask @ethan wu to fix type in rest api + quantitiy: number; +} + +export interface NumPointPriceListResponse { + status: 'success' | string; + response: { + price_list: NumPointPrice[]; + }; +} From 667fe19474919f98d55ae2725cdb77418d738c70 Mon Sep 17 00:00:00 2001 From: sultanmyrza Date: Mon, 20 Jun 2022 19:54:48 +0800 Subject: [PATCH 08/17] add in-app-store service --- .../in-app-store/in-app-store.service.spec.ts | 21 ++ .../in-app-store/in-app-store.service.ts | 255 ++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 src/app/shared/in-app-store/in-app-store.service.spec.ts create mode 100644 src/app/shared/in-app-store/in-app-store.service.ts 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 new file mode 100644 index 000000000..37e304a94 --- /dev/null +++ b/src/app/shared/in-app-store/in-app-store.service.spec.ts @@ -0,0 +1,21 @@ +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'; + +describe('InAppStoreService', () => { + let service: InAppStoreService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SharedModule], + providers: [InAppPurchase2], + }); + service = TestBed.inject(InAppStoreService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); 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 new file mode 100644 index 000000000..aa266bcd2 --- /dev/null +++ b/src/app/shared/in-app-store/in-app-store.service.ts @@ -0,0 +1,255 @@ +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 { BehaviorSubject, combineLatest } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { + generateMockInAppProducts, + setupInAppPurchaseDebugPrint, +} from '../../utils/in-app-purchase'; +import { + DiaBackendNumService, + NumPointPrice, +} from '../dia-backend/num/dia-backend-num.service'; +import { ErrorService } from '../error/error.service'; + +@Injectable({ + providedIn: 'root', +}) +export class InAppStoreService implements OnDestroy { + debugPrint = setupInAppPurchaseDebugPrint('InAppStoreService'); + + readonly inAppProducts$ = new BehaviorSubject([]); + readonly numPointPricesById$ = new BehaviorSubject({}); + + readonly inAppProductsWithNumpoints$ = combineLatest([ + this.inAppProducts$, + this.numPointPricesById$, + ]).pipe( + map(([inAppProducts, numPointPricesById]) => { + return inAppProducts.map(inAppProduct => { + const numPoints = this.numPointsForProduct( + inAppProduct, + numPointPricesById + ); + return { inAppProduct, numPoints }; + }); + }) + ); + + private readonly appId = 'io.numbersprotocol.capturelite'; + + constructor( + private readonly store: InAppPurchase2, + private readonly platform: Platform, + private readonly errorService: ErrorService, + private readonly toastController: ToastController, + private readonly diaBackendNumService: DiaBackendNumService, + private readonly translocoService: TranslocoService + ) {} + + async initialize() { + // Usefull for UI development in WEB environment + if (!this.isNativePlatform()) { + this.refreshNumPointsPricing(); + const mockData = generateMockInAppProducts(); + this.inAppProducts$.next(mockData); + return; + } + + try { + await this.platform.ready(); + await this.refreshNumPointsPricing(); + + this.regiseterStoreListeners(); + this.registerStoreProducts(); + + this.store.refresh(); + } catch (error) { + const errorMessage = this.translocoService.getTranslation( + 'inAppPurchase.failedToInitInAppStore' + ); + this.errorService.toastError$(errorMessage).toPromise(); + } + } + + ngOnDestroy(): void { + this.unregisterStoreListeners(); + } + + async refreshNumPointsPricing() { + const result = await this.diaBackendNumService + .numPointsPriceList$() + .toPromise(); + const priceListFromRestApi = result.response.price_list; + + const numPointPricesById: NumPointPricesById = {}; + for (const item of priceListFromRestApi) { + numPointPricesById[item.inAppPurchaseId] = item; + } + + this.numPointPricesById$.next(numPointPricesById); + } + + purchase(product: IAPProduct) { + this.store.order(product); + } + + 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; + } + if (inAppProduct.transaction?.type === 'android-playstore') { + receipt = inAppProduct.transaction.receipt; + } + if (!receipt) return; + + try { + await this.diaBackendNumService + .purchaseNumPoints$(pointsToAdd, receipt) + .toPromise(); + inAppProduct.finish(); + + this.notifyUser( + this.translocoService.translate('wallets.buyNum.pointsAdded', { + points: pointsToAdd, + }) + ); + } catch (error) { + const errorMessage = this.translocoService.getTranslation( + 'wallets.buyNum.failedToAddPoints' + ); + this.errorService.toastError$(errorMessage).toPromise(); + } + } + + private async notifyUser(message: string) { + return this.toastController + .create({ message, duration: 700 }) + .then(toast => toast.present()); + } + + 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); + } + + private unregisterStoreListeners() { + this.store.off(this.onStoreError); + this.store.off(this.onStoreReady); + this.store.off(this.onStoreProductApproved); + this.store.off(this.onStoreProductUpdated); + this.store.off(this.onStoreProductVerified); + } + + private registerStoreProducts() { + const consumableProductIds = [ + CaptureInAppProductIds.BRONZE_PACK, + CaptureInAppProductIds.SLIVER_PACK, + CaptureInAppProductIds.GOLD_PACK, + CaptureInAppProductIds.PLATINUM_PACK, + ]; + const type = this.store.CONSUMABLE; + + for (const id of consumableProductIds) { + this.store.register({ id, type }); + } + } + + private readonly onStoreError = (_: IAPError) => { + const errorMessage = this.translocoService.getTranslation( + 'inAppPurchase.inAppPurchaseErrorOcurred' + ); + this.errorService.toastError$(errorMessage).toPromise(); + }; + + private readonly onStoreReady = () => { + const inAppProducts = this.store.products.filter( + product => this.shouldIgnoreProduct(product) === false + ); + this.inAppProducts$.next(inAppProducts); + }; + + private readonly onStoreProductUpdated = (updatedProduct: IAPProduct) => { + if (this.shouldIgnoreProduct(updatedProduct)) { + return; + } + + this.debugPrint('onStoreProductUpdated', updatedProduct); + + const inAppProducts = this.inAppProducts$.value.map(product => + product.id === updatedProduct.id ? updatedProduct : product + ); + + this.inAppProducts$.next(inAppProducts); + }; + + 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 onStoreProductVerified = (product: IAPProduct) => { + if (this.shouldIgnoreProduct(product)) { + return; + } + this.debugPrint('onStoreProductVerified', product); + this.finishPurchase(product); + }; + + 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, + numPriceListById: NumPointPricesById + ) { + if (product.id in numPriceListById) { + return numPriceListById[product.id].quantitiy; + } + return 0; + } + + private isNativePlatform() { + return this.platform.is('hybrid'); + } +} + +export enum CaptureInAppProductIds { + BRONZE_PACK = 'cap_lite_consumable_bronze_pack_099', + SLIVER_PACK = 'cap_lite_consumable_silver_pack_199', + GOLD_PACK = 'cap_lite_consumable_gold_pack_299', + PLATINUM_PACK = 'cap_lite_consumable_platinum_pack_399', +} + +interface InAppProductsWithNumPoint { + inAppProduct: IAPProduct; + numPoints: number; +} + +interface NumPointPricesById { + [id: string]: NumPointPrice; +} From 6a637a1e9e8fd51d7021e2785b216c7ea27f318d Mon Sep 17 00:00:00 2001 From: sultanmyrza Date: Mon, 20 Jun 2022 20:03:23 +0800 Subject: [PATCH 09/17] add buy num page for in-app purchases --- .../wallets/buy-num/buy-num-routing.module.ts | 17 ++++ .../wallets/buy-num/buy-num.module.ts | 11 +++ .../wallets/buy-num/buy-num.page.html | 49 +++++++++++ .../wallets/buy-num/buy-num.page.scss | 85 +++++++++++++++++++ .../wallets/buy-num/buy-num.page.spec.ts | 34 ++++++++ .../features/wallets/buy-num/buy-num.page.ts | 52 ++++++++++++ .../wallets/wallets-routing.module.ts | 5 ++ src/app/features/wallets/wallets.page.html | 6 +- src/app/features/wallets/wallets.page.ts | 4 + src/assets/i18n/en-us.json | 4 +- src/assets/i18n/zh-tw.json | 4 +- 11 files changed, 266 insertions(+), 5 deletions(-) create mode 100644 src/app/features/wallets/buy-num/buy-num-routing.module.ts create mode 100644 src/app/features/wallets/buy-num/buy-num.module.ts create mode 100644 src/app/features/wallets/buy-num/buy-num.page.html create mode 100644 src/app/features/wallets/buy-num/buy-num.page.scss create mode 100644 src/app/features/wallets/buy-num/buy-num.page.spec.ts create mode 100644 src/app/features/wallets/buy-num/buy-num.page.ts diff --git a/src/app/features/wallets/buy-num/buy-num-routing.module.ts b/src/app/features/wallets/buy-num/buy-num-routing.module.ts new file mode 100644 index 000000000..95b19411f --- /dev/null +++ b/src/app/features/wallets/buy-num/buy-num-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { BuyNumPage } from './buy-num.page'; + +const routes: Routes = [ + { + path: '', + component: BuyNumPage, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class BuyNumPageRoutingModule {} diff --git a/src/app/features/wallets/buy-num/buy-num.module.ts b/src/app/features/wallets/buy-num/buy-num.module.ts new file mode 100644 index 000000000..c3981ec40 --- /dev/null +++ b/src/app/features/wallets/buy-num/buy-num.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; + +import { SharedModule } from '../../../shared/shared.module'; +import { BuyNumPageRoutingModule } from './buy-num-routing.module'; +import { BuyNumPage } from './buy-num.page'; + +@NgModule({ + imports: [SharedModule, BuyNumPageRoutingModule], + declarations: [BuyNumPage], +}) +export class BuyNumPageModule {} diff --git a/src/app/features/wallets/buy-num/buy-num.page.html b/src/app/features/wallets/buy-num/buy-num.page.html new file mode 100644 index 000000000..0120f7708 --- /dev/null +++ b/src/app/features/wallets/buy-num/buy-num.page.html @@ -0,0 +1,49 @@ + + + {{ t('wallets.buyNum.buyNum') }} + + + + + + In App Products are not available yet. Try again later + + + + + +
+
+ {{ product.inAppProduct.title }} + + +
+
+ {{ product.inAppProduct.price }} +
+
+
+ + {{ t('wallets.buyNum.buyPoints', { points: product.numPoints }) }} + + +
+
+
+
diff --git a/src/app/features/wallets/buy-num/buy-num.page.scss b/src/app/features/wallets/buy-num/buy-num.page.scss new file mode 100644 index 000000000..957e7b4a0 --- /dev/null +++ b/src/app/features/wallets/buy-num/buy-num.page.scss @@ -0,0 +1,85 @@ +mat-toolbar { + span { + padding-right: 40px; + } +} + +ion-card { + border-radius: 8px; +} + +ion-item { + --background: white; + --inner-padding-top: 0; +} + +ion-avatar { + width: 24px; + height: 24px; +} + +.in-app-product-description { + display: flex; + flex-direction: column; + justify-content: center; + min-height: 42px; +} + +.title-text { + font-weight: 700; + font-size: 18px; + color: black; + + @media (max-width: 360px) { + font-size: 16px; + } +} + +.title-text-large { + font-weight: 700; + font-size: 19px; + color: black; + + @media (max-width: 360px) { + font-size: 18px; + } +} + +ion-icon.num-point-info { + transform: translate(4px, 4px); + + @media (max-width: 360px) { + transform: translateY(4px); + } +} + +.subtitle-text { + color: black; + font-weight: 700; + font-size: 18px; + + @media (max-width: 360px) { + font-size: 16px; + } +} + +ion-button { + font-weight: 700; + font-size: 14px; + min-width: 140px; + + --border-radius: 8px; + --color: #00a84d; + --border-color: #00a84d; +} + +ion-button[slot='end'] { + margin: 0 0 0 4px; + padding: 4px; +} + +ion-spinner { + margin-left: 4px; + width: 12px; + height: 12px; +} 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 new file mode 100644 index 000000000..a55dc6b13 --- /dev/null +++ b/src/app/features/wallets/buy-num/buy-num.page.spec.ts @@ -0,0 +1,34 @@ +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'; + +describe('BuyNumPage', () => { + let component: BuyNumPage; + let fixture: ComponentFixture; + + 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); + component = fixture.componentInstance; + fixture.detectChanges(); + }) + ); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/wallets/buy-num/buy-num.page.ts b/src/app/features/wallets/buy-num/buy-num.page.ts new file mode 100644 index 000000000..4230cb206 --- /dev/null +++ b/src/app/features/wallets/buy-num/buy-num.page.ts @@ -0,0 +1,52 @@ +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 { map, tap } from 'rxjs/operators'; +import { InAppStoreService } from '../../../shared/in-app-store/in-app-store.service'; + +@Component({ + selector: 'app-buy-num', + templateUrl: './buy-num.page.html', + styleUrls: ['./buy-num.page.scss'], +}) +export class BuyNumPage implements OnInit { + readonly tmpIcon = '../../../../assets/images/num-icon.svg'; + + readonly inAppProducts$ = this.store.inAppProductsWithNumpoints$.pipe( + tap(_ => this.ref.detectChanges()) + ); + + readonly totalProducts$ = this.store.inAppProductsWithNumpoints$.pipe( + map(products => products.length), + tap(_ => this.ref.detectChanges()) + ); + + constructor( + private readonly store: InAppStoreService, + private readonly ref: ChangeDetectorRef, + private readonly alertController: AlertController, + private readonly translocoService: TranslocoService + ) {} + + ngOnInit() { + this.store.refreshNumPointsPricing(); + } + + purchase(product: IAPProduct) { + this.store.purchase(product); + } + + async showNumPointsQuantity(numPoints: number) { + const info = this.translocoService.translate( + 'wallets.buyNum.thisPackageIncludesXNumPoints', + { points: numPoints } + ); + const okText = this.translocoService.translate('wallets.buyNum.okIGotIt'); + const alert = await this.alertController.create({ + header: info, + buttons: [okText], + }); + await alert.present(); + } +} diff --git a/src/app/features/wallets/wallets-routing.module.ts b/src/app/features/wallets/wallets-routing.module.ts index be5aa70a6..31859663d 100644 --- a/src/app/features/wallets/wallets-routing.module.ts +++ b/src/app/features/wallets/wallets-routing.module.ts @@ -12,6 +12,11 @@ const routes: Routes = [ loadChildren: () => import('./transfer/transfer.module').then(m => m.TransferPageModule), }, + { + path: 'buy-num', + loadChildren: () => + import('./buy-num/buy-num.module').then(m => m.BuyNumPageModule), + }, ]; @NgModule({ diff --git a/src/app/features/wallets/wallets.page.html b/src/app/features/wallets/wallets.page.html index 805ec224a..822d520dd 100644 --- a/src/app/features/wallets/wallets.page.html +++ b/src/app/features/wallets/wallets.page.html @@ -49,13 +49,13 @@

NUM

> - + > Date: Mon, 20 Jun 2022 20:03:57 +0800 Subject: [PATCH 10/17] init in app purchases at app start --- src/app/app.component.spec.ts | 15 ++++++++++++++- src/app/app.component.ts | 3 +++ src/app/app.module.ts | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index b47d0a43b..d535634eb 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,6 +1,7 @@ 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'; @@ -9,15 +10,24 @@ import { MaterialTestingModule } from './shared/material/material-testing.module describe('AppComponent', () => { let platformReadySpy: Promise; + let platformIsSpy: boolean | undefined; let platformSpy: Platform; beforeEach( waitForAsync(() => { platformReadySpy = Promise.resolve(); + platformIsSpy = false; platformSpy = jasmine.createSpyObj('Platform', { ready: platformReadySpy, + is: platformIsSpy, }); + const iap2SpyMethods = ['error', 'ready', 'when', 'refresh', 'off']; + const inAppPurchase2Spy = jasmine.createSpyObj( + 'InAppPurchase2', + iap2SpyMethods + ); + TestBed.configureTestingModule({ declarations: [AppComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], @@ -27,7 +37,10 @@ describe('AppComponent', () => { getTranslocoTestingModule(), MaterialTestingModule, ], - providers: [{ provide: Platform, useValue: platformSpy }], + providers: [ + { provide: Platform, useValue: platformSpy }, + { provide: InAppPurchase2, useValue: inAppPurchase2Spy }, + ], }).compileComponents(); }) ); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 735edf1cc..2bceb0093 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -14,6 +14,7 @@ import { DiaBackendAssetUploadingService } from './shared/dia-backend/asset/uplo import { DiaBackendAuthService } from './shared/dia-backend/auth/dia-backend-auth.service'; import { DiaBackendNotificationService } from './shared/dia-backend/notification/dia-backend-notification.service'; import { ErrorService } from './shared/error/error.service'; +import { InAppStoreService } from './shared/in-app-store/in-app-store.service'; import { LanguageService } from './shared/language/service/language.service'; import { NotificationService } from './shared/notification/notification.service'; import { PushNotificationService } from './shared/push-notification/push-notification.service'; @@ -35,6 +36,7 @@ export class AppComponent { private readonly captureService: CaptureService, private readonly cameraService: CameraService, private readonly errorService: ErrorService, + private readonly inAppStoreService: InAppStoreService, notificationService: NotificationService, pushNotificationService: PushNotificationService, langaugeService: LanguageService, @@ -51,6 +53,7 @@ export class AppComponent { .initialize$() .pipe(untilDestroyed(this)) .subscribe(); + this.inAppStoreService.initialize(); this.initializeApp(); this.restoreAppState(); this.initializeCollector(); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ae93490a6..7d2b1982e 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -3,6 +3,7 @@ import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar'; import { BrowserModule } 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'; @@ -35,6 +36,7 @@ import { SharedModule } from './shared/shared.module'; provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: { duration: 2500 }, }, + InAppPurchase2, ], bootstrap: [AppComponent], }) From 854a2293022f21cda16c2e77ed5b600268625efa Mon Sep 17 00:00:00 2001 From: James Chien Date: Tue, 21 Jun 2022 10:09:39 +0800 Subject: [PATCH 11/17] ci(.github/workflows): add BUBBLE_API_URL build env var --- .github/workflows/build-apks.yml | 1 + .github/workflows/build.yml | 2 ++ .github/workflows/pre-release.yml | 4 ++++ .github/workflows/uiux-release.yml | 1 + 4 files changed, 8 insertions(+) diff --git a/.github/workflows/build-apks.yml b/.github/workflows/build-apks.yml index d4820ccc1..4c008e378 100644 --- a/.github/workflows/build-apks.yml +++ b/.github/workflows/build-apks.yml @@ -54,6 +54,7 @@ jobs: NUMBERS_STORAGE_BASE_URL: ${{ secrets[matrix.storage_base_url] }} NUMBERS_STORAGE_TRUSTED_CLIENT_KEY: ${{ secrets[matrix.storage_trusted_client_key] }} NUMBERS_BUBBLE_DB_URL: ${{ secrets[matrix.bubble_db_url] }} + BUBBLE_API_URL: ${{ secrets.BUBBLE_API_URL }} run: | npm install -g @ionic/cli npm install diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 10926e241..2406bdaa5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,6 +76,7 @@ jobs: NUMBERS_STORAGE_BASE_URL: ${{ secrets.NUMBERS_STORAGE_BASE_URL }} NUMBERS_STORAGE_TRUSTED_CLIENT_KEY: ${{ secrets.NUMBERS_STORAGE_TRUSTED_CLIENT_KEY }} NUMBERS_BUBBLE_DB_URL: ${{ secrets.NUMBERS_BUBBLE_DB_URL }} + BUBBLE_API_URL: ${{ secrets.BUBBLE_API_URL }} run: npm run build - name: Build Android @@ -109,6 +110,7 @@ jobs: NUMBERS_STORAGE_BASE_URL: ${{ secrets.NUMBERS_STORAGE_BASE_URL }} NUMBERS_STORAGE_TRUSTED_CLIENT_KEY: ${{ secrets.NUMBERS_STORAGE_TRUSTED_CLIENT_KEY }} NUMBERS_BUBBLE_DB_URL: ${{ secrets.NUMBERS_BUBBLE_DB_URL }} + BUBBLE_API_URL: ${{ secrets.BUBBLE_API_URL }} run: npm run build - name: Import the Code-Signing PKCS12 Certificate diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 2e2b7e501..bf692d6ed 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -30,6 +30,7 @@ jobs: NUMBERS_STORAGE_BASE_URL: ${{ secrets.NUMBERS_STORAGE_BASE_URL }} NUMBERS_STORAGE_TRUSTED_CLIENT_KEY: ${{ secrets.NUMBERS_STORAGE_TRUSTED_CLIENT_KEY }} NUMBERS_BUBBLE_DB_URL: ${{ secrets.NUMBERS_BUBBLE_DB_URL }} + BUBBLE_API_URL: ${{ secrets.BUBBLE_API_URL }} run: | npm install -g @ionic/cli npm install @@ -90,6 +91,7 @@ jobs: NUMBERS_STORAGE_BASE_URL: ${{ secrets.NUMBERS_STORAGE_BASE_URL }} NUMBERS_STORAGE_TRUSTED_CLIENT_KEY: ${{ secrets.NUMBERS_STORAGE_TRUSTED_CLIENT_KEY }} NUMBERS_BUBBLE_DB_URL: ${{ secrets.NUMBERS_BUBBLE_DB_URL }} + BUBBLE_API_URL: ${{ secrets.BUBBLE_API_URL }} run: | npm install -g @ionic/cli npm install @@ -199,6 +201,7 @@ jobs: NUMBERS_STORAGE_BASE_URL: ${{ secrets[matrix.storage_base_url] }} NUMBERS_STORAGE_TRUSTED_CLIENT_KEY: ${{ secrets[matrix.storage_trusted_client_key] }} NUMBERS_BUBBLE_DB_URL: ${{ secrets[matrix.bubble_db_url] }} + BUBBLE_API_URL: ${{ secrets.BUBBLE_API_URL }} run: | npm install -g @ionic/cli npm install @@ -242,6 +245,7 @@ jobs: NUMBERS_STORAGE_BASE_URL: ${{ secrets.NUMBERS_STORAGE_BASE_URL }} NUMBERS_STORAGE_TRUSTED_CLIENT_KEY: ${{ secrets.NUMBERS_STORAGE_TRUSTED_CLIENT_KEY }} NUMBERS_BUBBLE_DB_URL: ${{ secrets.NUMBERS_BUBBLE_DB_URL }} + BUBBLE_API_URL: ${{ secrets.BUBBLE_API_URL }} run: | npm install -g @ionic/cli npm install diff --git a/.github/workflows/uiux-release.yml b/.github/workflows/uiux-release.yml index 8cb6516a9..e7603f49f 100644 --- a/.github/workflows/uiux-release.yml +++ b/.github/workflows/uiux-release.yml @@ -20,6 +20,7 @@ jobs: NUMBERS_STORAGE_BASE_URL: ${{ secrets.NUMBERS_STORAGE_BASE_URL }} NUMBERS_STORAGE_TRUSTED_CLIENT_KEY: ${{ secrets.NUMBERS_STORAGE_TRUSTED_CLIENT_KEY }} NUMBERS_BUBBLE_DB_URL: ${{ secrets.NUMBERS_BUBBLE_DB_URL }} + BUBBLE_API_URL: ${{ secrets.BUBBLE_API_URL }} run: | npm install -g @ionic/cli npm install From b464c68054559a2aa6bb17b0a25f09ea16c0c4d0 Mon Sep 17 00:00:00 2001 From: James Chien Date: Tue, 21 Jun 2022 10:17:42 +0800 Subject: [PATCH 12/17] build(set-secret.js): add BUBBLE_API_URL in secret.js generation --- set-secret.js | 1 + 1 file changed, 1 insertion(+) diff --git a/set-secret.js b/set-secret.js index 0ff04ccfb..e16d5bf77 100644 --- a/set-secret.js +++ b/set-secret.js @@ -8,6 +8,7 @@ const envConfigFile = ` export const BASE_URL = '${process.env.NUMBERS_STORAGE_BASE_URL}'; export const TRUSTED_CLIENT_KEY = '${process.env.NUMBERS_STORAGE_TRUSTED_CLIENT_KEY}'; export const BUBBLE_DB_URL = '${process.env.NUMBERS_BUBBLE_DB_URL}'; +export const BUBBLE_API_URL = '${process.env.BUBBLE_API_URL}'; export const APPS_FLYER_DEV_KEY = '${process.env.APPS_FLYER_DEV_KEY}' `; fs.writeFile(targetPath, envConfigFile, err => { From 68f40841bd729fb19d411297502a7b43b33ee74a Mon Sep 17 00:00:00 2001 From: sultanmyrza Date: Tue, 21 Jun 2022 12:16:02 +0800 Subject: [PATCH 13/17] fix(buy-num.page): add missing translation --- src/app/features/wallets/buy-num/buy-num.page.html | 5 ++--- src/assets/i18n/en-us.json | 3 ++- src/assets/i18n/zh-tw.json | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) 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 0120f7708..1cdb35665 100644 --- a/src/app/features/wallets/buy-num/buy-num.page.html +++ b/src/app/features/wallets/buy-num/buy-num.page.html @@ -7,8 +7,8 @@ - - In App Products are not available yet. Try again later + + {{ t('wallets.buyNum.inAppProductsNotAvailableYetPleaseTryAgainLater') }} @@ -19,7 +19,6 @@ [ngClass]="{ 'title-text': !first, 'title-text-large': first }" > {{ product.inAppProduct.title }} - Date: Tue, 21 Jun 2022 12:16:32 +0800 Subject: [PATCH 14/17] fix(set-secret.js): add missing env var for BUBBLE_API_URL --- set-secret.js | 1 + 1 file changed, 1 insertion(+) diff --git a/set-secret.js b/set-secret.js index 0ff04ccfb..cac6683d5 100644 --- a/set-secret.js +++ b/set-secret.js @@ -9,6 +9,7 @@ export const BASE_URL = '${process.env.NUMBERS_STORAGE_BASE_URL}'; export const TRUSTED_CLIENT_KEY = '${process.env.NUMBERS_STORAGE_TRUSTED_CLIENT_KEY}'; export const BUBBLE_DB_URL = '${process.env.NUMBERS_BUBBLE_DB_URL}'; export const APPS_FLYER_DEV_KEY = '${process.env.APPS_FLYER_DEV_KEY}' +export const BUBBLE_API_URL = '${process.env.BUBBLE_API_URL}' `; fs.writeFile(targetPath, envConfigFile, err => { if (err) { From f0d244c7d2b49fa5ef907af59bd7c576c3046dec Mon Sep 17 00:00:00 2001 From: sultanmyrza Date: Tue, 21 Jun 2022 12:17:58 +0800 Subject: [PATCH 15/17] fix(buy-num.page): catch error for refreshNumPointsPricing --- .../features/wallets/buy-num/buy-num.page.ts | 16 +++++++++++-- .../in-app-store/in-app-store.service.ts | 24 +++++++++++-------- 2 files changed, 28 insertions(+), 12 deletions(-) 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 4230cb206..b96592c39 100644 --- a/src/app/features/wallets/buy-num/buy-num.page.ts +++ b/src/app/features/wallets/buy-num/buy-num.page.ts @@ -2,6 +2,7 @@ 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 { combineLatest } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import { InAppStoreService } from '../../../shared/in-app-store/in-app-store.service'; @@ -13,11 +14,22 @@ import { InAppStoreService } from '../../../shared/in-app-store/in-app-store.ser export class BuyNumPage implements OnInit { readonly tmpIcon = '../../../../assets/images/num-icon.svg'; - readonly inAppProducts$ = this.store.inAppProductsWithNumpoints$.pipe( + readonly inAppProducts$ = combineLatest([ + this.store.inAppProductsWithNumpoints$, + this.store.numPointPricesById$, + ]).pipe( + map(([inAppProductsWithNumpoints, numPointPricesById]) => { + const totalProducts = inAppProductsWithNumpoints.length; + const totalPrices = Object.keys(numPointPricesById).length; + if (totalProducts === 0 || totalPrices === 0) { + return []; + } + return inAppProductsWithNumpoints; + }), tap(_ => this.ref.detectChanges()) ); - readonly totalProducts$ = this.store.inAppProductsWithNumpoints$.pipe( + readonly totalProducts$ = this.inAppProducts$.pipe( map(products => products.length), tap(_ => this.ref.detectChanges()) ); 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 aa266bcd2..df6259bc6 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 @@ -83,17 +83,21 @@ export class InAppStoreService implements OnDestroy { } async refreshNumPointsPricing() { - const result = await this.diaBackendNumService - .numPointsPriceList$() - .toPromise(); - const priceListFromRestApi = result.response.price_list; - - const numPointPricesById: NumPointPricesById = {}; - for (const item of priceListFromRestApi) { - numPointPricesById[item.inAppPurchaseId] = item; - } + try { + const result = await this.diaBackendNumService + .numPointsPriceList$() + .toPromise(); + const priceListFromRestApi = result.response.price_list; - this.numPointPricesById$.next(numPointPricesById); + const numPointPricesById: NumPointPricesById = {}; + for (const item of priceListFromRestApi) { + numPointPricesById[item.inAppPurchaseId] = item; + } + + this.numPointPricesById$.next(numPointPricesById); + } catch (_) { + this.numPointPricesById$.next({}); + } } purchase(product: IAPProduct) { From 0c675df94a3ad4d620e90cde1c3a5c3706bb685d Mon Sep 17 00:00:00 2001 From: sultanmyrza Date: Tue, 21 Jun 2022 13:11:08 +0800 Subject: [PATCH 16/17] fix: in-app purchase related translations --- src/assets/i18n/en-us.json | 4 ++-- src/assets/i18n/zh-tw.json | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/assets/i18n/en-us.json b/src/assets/i18n/en-us.json index 0c9f31252..6fd573d4c 100644 --- a/src/assets/i18n/en-us.json +++ b/src/assets/i18n/en-us.json @@ -290,9 +290,9 @@ "total": "Total", "calculateGasFee": "Calculate Gas", "buyNum": { - "buyNum": "Buy NUM", + "buyNum": "Buy NUM Points", "buyPoints": "Buy {{points}} Points", - "pointsAdded": "{{points}} points added", + "pointsAdded": "{{points}} NUM points added", "failedToAddPoints": "Failed to add points", "thisPackageIncludesXNumPoints": "This package include {{points}} NUM points.", "okIGotIt": "Ok, I got it", diff --git a/src/assets/i18n/zh-tw.json b/src/assets/i18n/zh-tw.json index 75ec09147..f9d03a331 100644 --- a/src/assets/i18n/zh-tw.json +++ b/src/assets/i18n/zh-tw.json @@ -290,13 +290,13 @@ "total": "總額", "calculateGasFee": "計算油費", "buyNum": { - "buyNum": "購買 NUM", - "buyPoints": "買{{points}}分", - "pointsAdded": "加了{{points}}分", - "failedToAddPoints": "加分失敗", - "thisPackageIncludesXNumPoints": "此套餐包括{{points}}個 NUM 分。", - "okIGotIt": "好,我知道了", - "inAppProductsNotAvailableYetPleaseTryAgainLater": "應用內產品尚不可用。 稍後再試。" + "buyNum": "購買 NUM 點數", + "buyPoints": "購買{{points}}點數", + "pointsAdded": "加值 {{points}} NUM 點數", + "failedToAddPoints": "加值失敗", + "thisPackageIncludesXNumPoints": "此方案包括{{points}}個 NUM 點數", + "okIGotIt": "好,我了解", + "inAppProductsNotAvailableYetPleaseTryAgainLater": "App 內購買目前無法使用,請稍後再試" } }, "invitation": { @@ -359,7 +359,7 @@ "updateNow": "現在更新" }, "inAppPurchase": { - "failedToInitInAppStore": "In-App Store 初始化期間發生錯誤。 請檢查您的互聯網連接質量。", - "inAppPurchaseErrorOcurred": "發生應用內購買錯誤。 請稍後再試。" + "failedToInitInAppStore": "App 購買功能發生錯誤,請檢查網路連線狀況", + "inAppPurchaseErrorOcurred": "發生 App 內購買錯誤,請稍後再試" } } From 9286ed908c94ca8acac8371215d7365e21851237 Mon Sep 17 00:00:00 2001 From: sultanmyrza Date: Tue, 21 Jun 2022 13:22:34 +0800 Subject: [PATCH 17/17] fix(set-secret.js): remove duplicate identifier BUBBLE_API_URL --- set-secret.js | 1 - 1 file changed, 1 deletion(-) diff --git a/set-secret.js b/set-secret.js index f46fbbca8..e16d5bf77 100644 --- a/set-secret.js +++ b/set-secret.js @@ -10,7 +10,6 @@ export const TRUSTED_CLIENT_KEY = '${process.env.NUMBERS_STORAGE_TRUSTED_CLIENT_ export const BUBBLE_DB_URL = '${process.env.NUMBERS_BUBBLE_DB_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 BUBBLE_API_URL = '${process.env.BUBBLE_API_URL}' `; fs.writeFile(targetPath, envConfigFile, err => { if (err) {