Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix] - Store Kit 1 race condition #2604

Open
wants to merge 60 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
d81c087
feat: create ThreadSafe class
arthurgeron Nov 8, 2023
be187d3
feat: make validproducts var thread safe
arthurgeron Nov 8, 2023
4da3557
feat: create LatestPromiseKeeper
arthurgeron Nov 8, 2023
aef6e7c
feat: avoid conflicts with racing promises
arthurgeron Nov 8, 2023
40f45be
fix: promise keeper import statements
arthurgeron Nov 8, 2023
b1e1513
fix: allow atomically value to be modified
arthurgeron Nov 8, 2023
2bf2f91
fix: move resolve if matches logic to class
arthurgeron Nov 8, 2023
5da30a3
fix: syntax when accessing validProducts dict
arthurgeron Nov 8, 2023
8286f5c
fix: lint errors
arthurgeron Nov 13, 2023
5a5c61f
chore: remove unecessary comments
arthurgeron Nov 13, 2023
7c02787
feat: add swift files to project target
arthurgeron Nov 13, 2023
fd6f73f
docs: improve documentation 17.11.2023 (#2613)
cervebar Nov 19, 2023
c67fe8f
refactor: migrate exmaple project to rn@0.72+ (#2633)
hyochan Dec 8, 2023
c4a8369
Update appleSk2.ts (#2608)
isnolan Dec 8, 2023
2c78c0f
feat: add renewal info for Storekit 2 (#2614)
cervebar Dec 8, 2023
5425b8e
feat: add currency regarding the new API (#2615)
cervebar Dec 8, 2023
bd28785
feat: add ios error reason user canceled (#2616)
cervebar Dec 8, 2023
b0f47a5
Update purchases.mdx (#2620)
isnolan Dec 8, 2023
61f314b
docs: update reamde
hyochan Nov 12, 2023
5f6f2bc
12.12.0
hyochan Dec 8, 2023
065620e
fix: add missing conn for SK2 intro offers (#2630)
Shaw-Signaturize Dec 15, 2023
2c99ff7
12.12.1
hyochan Dec 15, 2023
4bbf976
docs: update gold tier ⚜
hyochan Dec 25, 2023
5855e16
chore(deps): remove @types/react-native
hyochan Dec 25, 2023
56ce551
fix: docs
hyochan Dec 25, 2023
48b5693
chore: update lock files
arthurgeron Jan 7, 2024
61b9bba
refactor: migrate exmaple project to rn@0.72+ (#2633)
hyochan Dec 8, 2023
55a3ee8
build(deps): bump follow-redirects from 1.15.3 to 1.15.4 in /docs (#2…
dependabot[bot] Jan 15, 2024
c3d6d32
feat: Added export finishTransaction() from StoreKit2. (#2664)
khrulev Jan 15, 2024
4f3ebbc
fix: avoid app crash due to multiple reject invocation (#2669)
khrulev Jan 15, 2024
eef0559
12.12.2
hyochan Jan 15, 2024
8dee627
docs: typos (#2673)
pvinis Jan 18, 2024
01349fa
Update troubleshooting.mdx (#2681)
Vittor-Javidan Feb 3, 2024
e3727d4
docs: typos (#2684)
burivuhster Feb 9, 2024
aebe920
build(deps): bump undici from 5.28.2 to 5.28.3 (#2688)
dependabot[bot] Mar 1, 2024
4e24b96
build(deps): bump ip from 1.1.8 to 1.1.9 (#2690)
dependabot[bot] Mar 1, 2024
3b71574
build(deps): bump ip from 1.1.8 to 1.1.9 in /IapExample (#2691)
dependabot[bot] Mar 1, 2024
335a26e
Update hooks.md documentation (#2687)
emilcieslar Mar 1, 2024
20a0e2a
docs: Improving IapExample Readme.md (#2692)
dmitryou Mar 1, 2024
bc8dad6
chore: Update Expo Config Plugins (#2695)
jimhunty Mar 1, 2024
2bc1152
12.13.0
hyochan Mar 1, 2024
3fc7b11
Update get-started.mdx to avoid crashing when app launching on iOS 12…
zyestin Mar 31, 2024
92b59be
build(deps): bump follow-redirects from 1.15.4 to 1.15.6 in /docs (#2…
dependabot[bot] Mar 31, 2024
86dd4fb
build(deps): bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /docs…
dependabot[bot] Mar 31, 2024
e132722
build(deps): bump express from 4.18.2 to 4.19.2 in /docs (#2714)
dependabot[bot] Mar 31, 2024
8009369
fix: lint errors
arthurgeron Nov 13, 2023
ffc5db8
refactor: migrate exmaple project to rn@0.72+ (#2633)
hyochan Dec 8, 2023
bbf311a
chore: update lock files
arthurgeron Jan 7, 2024
76166cb
Merge branch 'main' of github.com:dooboolab-community/react-native-ia…
arthurgeron Apr 2, 2024
ea8a0cb
fix: rejects previous promises
arthurgeron Apr 2, 2024
20a803b
feat: fetch minimum ios version from project and apply it as default …
arthurgeron Apr 3, 2024
f026dc9
feat: add NO_FLIPPER to Troubleshooting section in Example's README
arthurgeron Apr 3, 2024
ceed01f
fix: wrong path for bridging header in xcode proj file
arthurgeron Apr 3, 2024
1308aa7
feat: use cache key for buildcache
arthurgeron Apr 3, 2024
d9f97be
chore: add yarn yml & configs to gitignore
arthurgeron Apr 5, 2024
463d40b
feat: add podfile verification to cache
arthurgeron Apr 5, 2024
1bc05a5
feat: replace xcodebuild with xcodebuild action
arthurgeron Apr 5, 2024
7e5cfe4
fix: destination device ios
arthurgeron Apr 5, 2024
cfec6fe
feat: disable flipper in ci example ios
arthurgeron Apr 5, 2024
9911894
feat: change runner for intel macos
arthurgeron Apr 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 38 additions & 0 deletions ios/LatestPromiseKeeper.swift
@@ -0,0 +1,38 @@
import StoreKit

// Only keeps latest promise, assumes older promises are not needed
// Avoids racing conditions by storing latestPromise in a thread safe var
// Cancels previous promises when new ones are added
// Should not be used when all promises are relevant (e.g. Purchases)
class LatestPromiseKeeper {
private var latestPromise: ThreadSafe<(RCTPromiseResolveBlock, RCTPromiseRejectBlock)?> = ThreadSafe(nil)
private var latestRequest: ThreadSafe<SKProductsRequest?> = ThreadSafe(nil)

func setLatestPromise(request: SKProductsRequest, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
cancelOngoingRequest()

latestRequest.atomically { $0 = request }
latestPromise.atomically { $0 = (resolve, reject) }
}

func cancelOngoingRequest() {
latestRequest.atomically { ongoingRequest in
ongoingRequest?.cancel()
ongoingRequest = nil
}

latestPromise.atomically { $0 = nil }
}

func resolveIfRequestMatches(matchingRequest: SKProductsRequest, items: [[String: Any?]], operation: (RCTPromiseResolveBlock, [[String: Any?]]) -> Void) {
latestPromise.atomically { promiseResolvers in
guard let (resolve, _) = promiseResolvers else { return }

latestRequest.atomically { ongoingRequest in
guard ongoingRequest === matchingRequest else { return }

operation(resolve, items)
}
}
}
}
27 changes: 17 additions & 10 deletions ios/RNIapIos.swift
Expand Up @@ -8,18 +8,19 @@ class RNIapIos: RCTEventEmitter, SKRequestDelegate, SKPaymentTransactionObserver
private var hasListeners = false
private var pendingTransactionWithAutoFinish = false
private var receiptBlock: ((Data?, Error?) -> Void)? // Block to handle request the receipt async from delegate
private var validProducts: [String: SKProduct]
private var validProducts: ThreadSafe<[String: SKProduct]>
private var promotedPayment: SKPayment?
private var promotedProduct: SKProduct?
private var productsRequest: SKProductsRequest?
private let latestPromiseKeeper = LatestPromiseKeeper()
private var countPendingTransaction: Int = 0
private var hasTransactionObserver = false

override init() {
promisesByKey = [String: [RNIapIosPromise]]()
pendingTransactionWithAutoFinish = false
myQueue = DispatchQueue(label: "reject")
validProducts = [String: SKProduct]()
validProducts = ThreadSafe<[String: SKProduct]>([:])
super.init()
addTransactionObserver()
}
Expand Down Expand Up @@ -148,7 +149,7 @@ class RNIapIos: RCTEventEmitter, SKRequestDelegate, SKPaymentTransactionObserver
stopObserving()
rejectAllPendingPromises()
receiptBlock = nil
validProducts.removeAll()
validProducts.atomically { $0.removeAll() }
promotedPayment = nil
promotedProduct = nil
productsRequest = nil
Expand All @@ -162,10 +163,13 @@ class RNIapIos: RCTEventEmitter, SKRequestDelegate, SKPaymentTransactionObserver
) {
let productIdentifiers = Set<String>(skus)
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)

if let productsRequest = productsRequest {
productsRequest.delegate = self
let key: String = productsRequest.key
addPromise(forKey: key, resolve: resolve, reject: reject)

// Update latestPromiseKeeper with the new request and promise
self.latestPromiseKeeper.setLatestPromise(request: productsRequest, resolve: resolve, reject: reject)

productsRequest.start()
}
}
Expand All @@ -189,7 +193,7 @@ class RNIapIos: RCTEventEmitter, SKRequestDelegate, SKPaymentTransactionObserver
reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
) {
pendingTransactionWithAutoFinish = andDangerouslyFinishTransactionAutomatically
if let product = validProducts[sku] {
if let product = validProducts.value[sku] {
addPromise(forKey: product.productIdentifier, resolve: resolve, reject: reject)

let payment = SKMutablePayment(product: product)
Expand Down Expand Up @@ -254,7 +258,7 @@ class RNIapIos: RCTEventEmitter, SKRequestDelegate, SKPaymentTransactionObserver
reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
) {
debugMessage("clear valid products")
validProducts.removeAll()
validProducts.atomically { $0.removeAll() }
resolve(nil)
}

Expand Down Expand Up @@ -348,23 +352,26 @@ class RNIapIos: RCTEventEmitter, SKRequestDelegate, SKPaymentTransactionObserver

// StoreKitDelegate
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
// Add received products
for prod in response.products {
add(prod)
}

var items: [[String: Any?]] = [[:]]
for product in validProducts.values {
for product in validProducts.value.values {
items.append(getProductObject(product))
}

resolvePromises(forKey: request.key, value: items)
self.latestPromiseKeeper.resolveIfRequestMatches(matchingRequest: request, items: items) { (resolve, items) in
resolve(items)
}
}

// Add to valid products from Apple server response. Allowing getProducts, getSubscriptions call several times.
// Doesn't allow duplication. Replace new product.
func add(_ aProd: SKProduct) {
debugMessage("Add new object: \(aProd.productIdentifier)")
validProducts[aProd.productIdentifier] = aProd
validProducts.atomically { $0[aProd.productIdentifier] = aProd }
}

func request(_ request: SKRequest, didFailWithError error: Error) {
Expand Down
18 changes: 18 additions & 0 deletions ios/ThreadSafe.swift
@@ -0,0 +1,18 @@
class ThreadSafe<A> {
private var _value: A
private let queue = DispatchQueue(label: "ThreadSafe")

init(_ value: A) {
self._value = value
}

var value: A {
return queue.sync { _value }
}

func atomically(_ transform: (inout A) -> ()) {
queue.sync {
transform(&self._value)
}
}
}