feat: iap (#1996)
This commit is contained in:
@@ -349,6 +349,8 @@ PODS:
|
||||
- React-Core
|
||||
- RNFS (2.20.0):
|
||||
- React-Core
|
||||
- RNIap (12.4.4):
|
||||
- React-Core
|
||||
- RNKeychain (8.0.0):
|
||||
- React-Core
|
||||
- RNPrivacySnapshot (1.0.0):
|
||||
@@ -422,6 +424,7 @@ DEPENDENCIES:
|
||||
- "RNCAsyncStorage (from `../node_modules/@react-native-community/async-storage`)"
|
||||
- RNFileViewer (from `../node_modules/react-native-file-viewer`)
|
||||
- RNFS (from `../node_modules/react-native-fs`)
|
||||
- RNIap (from `../node_modules/react-native-iap`)
|
||||
- RNKeychain (from `../node_modules/react-native-keychain`)
|
||||
- RNPrivacySnapshot (from `../node_modules/react-native-privacy-snapshot`)
|
||||
- RNShare (from `../node_modules/react-native-share`)
|
||||
@@ -518,6 +521,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-file-viewer"
|
||||
RNFS:
|
||||
:path: "../node_modules/react-native-fs"
|
||||
RNIap:
|
||||
:path: "../node_modules/react-native-iap"
|
||||
RNKeychain:
|
||||
:path: "../node_modules/react-native-keychain"
|
||||
RNPrivacySnapshot:
|
||||
@@ -578,6 +583,7 @@ SPEC CHECKSUMS:
|
||||
RNCAsyncStorage: b03032fdbdb725bea0bd9e5ec5a7272865ae7398
|
||||
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
|
||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||
RNIap: 3bcd6982cf99503339cf9243e4ba70a45ea2cf72
|
||||
RNKeychain: 4f63aada75ebafd26f4bc2c670199461eab85d94
|
||||
RNPrivacySnapshot: 8eaf571478a353f2e5184f5c803164f22428b023
|
||||
RNShare: a5dc3b9c53ddc73e155b8cd9a94c70c91913c43c
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
1C2EEB3B45F4EB07AC795C77 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
|
||||
33BB1B14071EBE5978EBF3A8 /* libPods-StandardNotes-StandardNotesTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 04FCB5A3A3387CA3CFC82AA3 /* libPods-StandardNotes-StandardNotesTests.a */; };
|
||||
BC8DEA834BF198E8511F04FF /* libPods-StandardNotesDev.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 51F2D747BE02C2A1BCFEEFD1 /* libPods-StandardNotesDev.a */; };
|
||||
CD6592A9291EEFCC00C09DC6 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD6592A8291EEFCC00C09DC6 /* StoreKit.framework */; };
|
||||
CD7D5ECA27800609005FE1BF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CD7D5EC927800608005FE1BF /* LaunchScreen.storyboard */; };
|
||||
CD7D5ECF278015D2005FE1BF /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
|
||||
CD7D5ED0278015D2005FE1BF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
@@ -59,6 +60,7 @@
|
||||
66417CEB7622E77D89928FCA /* Pods-StandardNotes.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StandardNotes.debug.xcconfig"; path = "Target Support Files/Pods-StandardNotes/Pods-StandardNotes.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
948EE90E15EA48C27577820B /* Pods-StandardNotes.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StandardNotes.release.xcconfig"; path = "Target Support Files/Pods-StandardNotes/Pods-StandardNotes.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A09B7794259DBFABFC4D05CE /* Pods-StandardNotesDev.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StandardNotesDev.debug.xcconfig"; path = "Target Support Files/Pods-StandardNotesDev/Pods-StandardNotesDev.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
CD6592A8291EEFCC00C09DC6 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||
CD7D5EC8278005B6005FE1BF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = StandardNotes/Info.plist; sourceTree = "<group>"; };
|
||||
CD7D5EC927800608005FE1BF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
CD7D5EDF278015D2005FE1BF /* StandardNotesDev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StandardNotesDev.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -86,6 +88,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CD6592A9291EEFCC00C09DC6 /* StoreKit.framework in Frameworks */,
|
||||
1C2EEB3B45F4EB07AC795C77 /* (null) in Frameworks */,
|
||||
DD3D1CE428EC1C8BA0C49211 /* libPods-StandardNotes.a in Frameworks */,
|
||||
);
|
||||
@@ -153,6 +156,7 @@
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CD6592A8291EEFCC00C09DC6 /* StoreKit.framework */,
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
04FCB5A3A3387CA3CFC82AA3 /* libPods-StandardNotes-StandardNotesTests.a */,
|
||||
51F2D747BE02C2A1BCFEEFD1 /* libPods-StandardNotesDev.a */,
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"react-native-fingerprint-scanner": "standardnotes/react-native-fingerprint-scanner#b55d1c0ca627a87a130f758603f12911fbac200f",
|
||||
"react-native-flag-secure-android": "standardnotes/react-native-flag-secure-android#cb08e74583c22a5d912842459b35ebbbb4bcd852",
|
||||
"react-native-fs": "^2.19.0",
|
||||
"react-native-iap": "^12.4.4",
|
||||
"react-native-keychain": "standardnotes/react-native-keychain#d277d360494cbd02be4accb4a360772a8e0e97b6",
|
||||
"react-native-privacy-snapshot": "standardnotes/react-native-privacy-snapshot#653e904c90fc6f2b578da59138f2bfe5d7f942fe",
|
||||
"react-native-share": "^7.9.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AsyncStorage from '@react-native-community/async-storage'
|
||||
import SNReactNative from '@standardnotes/react-native-utils'
|
||||
import { AppleIAPReceipt } from '@standardnotes/services/dist/Domain/Subscription/AppleIAPReceipt'
|
||||
import {
|
||||
ApplicationIdentifier,
|
||||
Environment,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
RawKeychainValue,
|
||||
removeFromArray,
|
||||
TransferPayload,
|
||||
AppleIAPProductId,
|
||||
UuidString,
|
||||
} from '@standardnotes/snjs'
|
||||
import { ColorSchemeObserverService } from 'ColorSchemeObserverService'
|
||||
@@ -41,6 +43,7 @@ import Share from 'react-native-share'
|
||||
import { AndroidBackHandlerService } from '../AndroidBackHandlerService'
|
||||
import { AppStateObserverService } from './../AppStateObserverService'
|
||||
import Keychain from './Keychain'
|
||||
import { PurchaseManager } from '../PurchaseManager'
|
||||
|
||||
export type BiometricsType = 'Fingerprint' | 'Face ID' | 'Biometrics' | 'Touch ID'
|
||||
|
||||
@@ -99,6 +102,10 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
private colorSchemeService?: ColorSchemeObserverService,
|
||||
) {}
|
||||
|
||||
purchaseSubscriptionIAP(plan: AppleIAPProductId): Promise<AppleIAPReceipt | undefined> {
|
||||
return PurchaseManager.getInstance().purchase(plan)
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.stateObserverService?.deinit()
|
||||
;(this.stateObserverService as unknown) = undefined
|
||||
@@ -108,7 +115,7 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
;(this.colorSchemeService as unknown) = undefined
|
||||
}
|
||||
|
||||
consoleLog(...args: any[]): void {
|
||||
consoleLog(...args: unknown[]): void {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(args)
|
||||
}
|
||||
|
||||
18
packages/mobile/src/Lib/Logging.ts
Normal file
18
packages/mobile/src/Lib/Logging.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { log as utilsLog } from '@standardnotes/snjs'
|
||||
|
||||
export enum LoggingDomain {
|
||||
AppleIAP,
|
||||
}
|
||||
|
||||
const LoggingStatus: Record<LoggingDomain, boolean> = {
|
||||
[LoggingDomain.AppleIAP]: true,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function log(domain: LoggingDomain, ...args: any[]): void {
|
||||
if (!LoggingStatus[domain]) {
|
||||
return
|
||||
}
|
||||
|
||||
utilsLog(LoggingDomain[domain], ...args)
|
||||
}
|
||||
80
packages/mobile/src/PurchaseManager.ts
Normal file
80
packages/mobile/src/PurchaseManager.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { LoggingDomain, log } from './Lib/Logging'
|
||||
import { EmitterSubscription } from 'react-native'
|
||||
import {
|
||||
initConnection,
|
||||
endConnection,
|
||||
purchaseErrorListener,
|
||||
purchaseUpdatedListener,
|
||||
type ProductPurchase,
|
||||
type PurchaseError,
|
||||
type SubscriptionPurchase,
|
||||
finishTransaction,
|
||||
requestSubscription,
|
||||
getSubscriptions,
|
||||
} from 'react-native-iap'
|
||||
import { AppleIAPReceipt, AppleIAPProductId } from '@standardnotes/snjs'
|
||||
|
||||
export class PurchaseManager {
|
||||
private static instance: PurchaseManager
|
||||
private listenerDisposer: EmitterSubscription
|
||||
private errorDisposer: EmitterSubscription
|
||||
|
||||
private constructor() {
|
||||
this.listenerDisposer = purchaseUpdatedListener((purchase: SubscriptionPurchase | ProductPurchase) => {
|
||||
log(LoggingDomain.AppleIAP, 'purchaseUpdatedListener', purchase)
|
||||
const receipt = purchase.transactionReceipt
|
||||
if (receipt) {
|
||||
void finishTransaction({ purchase, isConsumable: false })
|
||||
}
|
||||
})
|
||||
|
||||
this.errorDisposer = purchaseErrorListener((error: PurchaseError) => {
|
||||
log(LoggingDomain.AppleIAP, 'purchaseErrorListener', error)
|
||||
})
|
||||
}
|
||||
|
||||
public static getInstance(): PurchaseManager {
|
||||
if (!PurchaseManager.instance) {
|
||||
PurchaseManager.instance = new PurchaseManager()
|
||||
}
|
||||
|
||||
return PurchaseManager.instance
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.listenerDisposer.remove()
|
||||
this.errorDisposer.remove()
|
||||
void endConnection()
|
||||
}
|
||||
|
||||
async purchase(sku: AppleIAPProductId): Promise<AppleIAPReceipt | undefined> {
|
||||
await initConnection()
|
||||
|
||||
const subscriptions = await getSubscriptions({
|
||||
skus: [AppleIAPProductId.PlusPlanYearly, AppleIAPProductId.ProPlanYearly],
|
||||
})
|
||||
|
||||
log(LoggingDomain.AppleIAP, 'Retrieved subscriptions', subscriptions)
|
||||
|
||||
try {
|
||||
const result = await requestSubscription({ sku, andDangerouslyFinishTransactionAutomaticallyIOS: true })
|
||||
|
||||
log(LoggingDomain.AppleIAP, 'Purchase result', result)
|
||||
|
||||
if (result && result.transactionId && result.transactionDate) {
|
||||
return {
|
||||
transactionId: result.transactionId,
|
||||
productId: result.productId as AppleIAPProductId,
|
||||
transactionDate: String(result.transactionDate),
|
||||
transactionReceipt: result.transactionReceipt,
|
||||
}
|
||||
} else {
|
||||
log(LoggingDomain.AppleIAP, 'Purchase method returning undefined even though successful')
|
||||
return undefined
|
||||
}
|
||||
} catch (error) {
|
||||
log(LoggingDomain.AppleIAP, error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user