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

integrate ios storekit 2 #1882

Merged
merged 21 commits into from
Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/publish-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Publish main package

on:
release:
types: [created]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
cache: 'yarn'
registry-url: 'https://registry.npmjs.org'

- name: Remove example code
run: rm -rf IapExample

- name: Install dependencies
run: yarn install --immutable

- name: Run lint scripts
run: yarn lint:ci

- name: Verify no files have changed after auto-fix
run: git diff -- ":(exclude)IapExample/*" --exit-code HEAD

- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 0 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ opt_in_rules:
- operator_usage_whitespace
- redundant_type_annotation

indentation: 2
vertical_whitespace_closing_braces: true
vertical_whitespace_opening_braces: true
2 changes: 1 addition & 1 deletion IapExample/ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'

platform :ios, '12.4'
platform :ios, '15.0'
install! 'cocoapods', :deterministic_uuids => false

target 'IapExample' do
Expand Down
6 changes: 3 additions & 3 deletions IapExample/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ PODS:
- React-Core
- RNGestureHandler (2.5.0):
- React-Core
- RNIap (8.6.5):
- RNIap (9.0.0):
- React-Core
- RNScreens (3.15.0):
- React-Core
Expand Down Expand Up @@ -575,12 +575,12 @@ SPEC CHECKSUMS:
ReactCommon: e30ec17dfb1d4c4f3419eac254350d6abca6d5a2
RNCMaskedView: cb9670ea9239998340eaab21df13fa12a1f9de15
RNGestureHandler: bad495418bcbd3ab47017a38d93d290ebd406f50
RNIap: e9f648d00e693913f80bbe5b661a5257fb72fe93
RNIap: 95084640290f4d68559dc37f1c8424b3f7dbf5d8
RNScreens: 4a1af06327774490d97342c00aee0c2bafb497b7
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
Yoga: 7ab6e3ee4ce47d7b789d1cb520163833e515f452
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

PODFILE CHECKSUM: cfed50b11ea421296640e72457dcf62114e957f6
PODFILE CHECKSUM: 47f330ba4aa0808e88cd2a085debf346af3dc659

COCOAPODS: 1.11.3
2 changes: 1 addition & 1 deletion RNIap.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Pod::Spec.new do |s|
s.license = package["license"]
s.authors = package["author"]

s.platforms = { :ios => "10.0" , :tvos => "10.0"}
s.platforms = { :ios => "15.0" , :tvos => "15.0"}
s.source = { :git => "https://github.com/dooboolab/react-native-iap.git", :tag => "#{s.version}" }

s.source_files = "ios/*.{h,m,mm,swift}"
Expand Down
22 changes: 0 additions & 22 deletions docs/docs/usage_instructions/receipt_validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,6 @@ flow) or unstable internet connections.
For these cases we have a convenience method `getReceiptIOS()` which gets
the latest receipt for the app at any given time. The response is base64 encoded.

### iOS Purchasing process right way.

Issue regarding `valid products`

- In iOS, generally you are fetching valid products at App launching process.

If you fetch again, or fetch valid subscription, the products are added to
the array object in iOS side (Objective-C `NSMutableArray`).

This makes unexpected behavior when you fetch with a part of product lists.

For example, if you have products of `[A, B, C]`, and you call fetch function
with only `[A]`, this module returns `[A, B, C]`).

This is weird, but it works.

- But, weird result is weird, so we made a new method which remove all valid products.

If you need to clear all products, subscriptions in that array, just call
`clearProductsIOS()`, and do the fetching job again, and you will receive what
you expected.

### Example backend (Node.js)

[Here](https://github.com/mifi/in-app-subscription-example) you can find an example backend for idempotent validating of receipts on both iOS/Android and storing and serving subscription state to the client.
112 changes: 112 additions & 0 deletions ios/IapSerializationUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// IapSerializationUtils.swift
// RNIap
//
// Created by Aguilar Andres on 8/18/22.
//

import Foundation
import StoreKit

func serialize(_ p: Product) -> [String: Any?] {
return ["displayName": p.displayName,
"description": p.description,
"id": p.id,
"displayPrice": p.displayPrice,
"price": p.price,
"isFamilyShareable": p.isFamilyShareable,
"subscription": p.subscription?.subscriptionGroupID,
"jsonRepresentation": p.jsonRepresentation,
"debugDescription": p.debugDescription,
"subscription": serialize(p.subscription),
"type": serialize(p.type)
]
}

func serialize(_ e: Error?) -> [String: Any?]? {
guard let e = e else {return nil}
return ["localizedDescription": e.localizedDescription]
}

func serialize(_ si: Product.SubscriptionInfo?) -> [String: Any?]? {
guard let si = si else {return nil}
return [
"subscriptionGroupID": si.subscriptionGroupID,
"promotionalOffers": si.promotionalOffers.map {(offer: Product.SubscriptionOffer) in serialize(offer)},
"introductoryOffer": serialize(si.introductoryOffer),
"subscriptionPeriod": si.subscriptionPeriod
]
}

func serialize(_ so: Product.SubscriptionOffer?) -> [String: Any?]? {
guard let so = so else {return nil}
return [
"id": so.id,
"price": so.price,
"displayPrice": so.displayPrice,
"type": so.type,
"paymentMode": so.paymentMode,
"period": so.period,
"periodCount": so.periodCount
]
}

// Transaction
func serialize(_ t: Transaction) -> [String: Any?] {
return ["id": t.id,
"appBundleID": t.appBundleID,
"offerID": t.offerID,
"subscriptionGroupID": t.subscriptionGroupID,
"appAccountToken": t.appAccountToken,
"debugDescription": t.debugDescription,
"deviceVerification": t.deviceVerification,
"deviceVerificationNonce": t.deviceVerificationNonce,
"expirationDate": t.expirationDate,
"isUpgraded": t.isUpgraded,
"jsonRepresentation": t.jsonRepresentation,
"offerType": serialize(t.offerType),
"expirationDate": t.expirationDate,
"originalID": t.originalID,
"originalPurchaseDate": t.originalPurchaseDate,
"ownershipType": serialize(t.ownershipType),
"productType": serialize(t.productType),
"productID": t.productID,
"purchasedQuantity": t.purchasedQuantity,
"revocationDate": t.revocationDate,
"revocationReason": t.revocationReason,
"purchaseDate": t.purchaseDate,
"signedDate": t.signedDate,
"webOrderLineItemID": t.webOrderLineItemID
]
}

func serialize(_ ot: Transaction.OfferType?) -> String? {
guard let ot = ot else {return nil}
switch ot {
case .promotional: return "promotional"
case .introductory: return "introductory"
case .code: return "code"
default:
return nil
}
}
func serialize(_ ot: Transaction.OwnershipType?) -> String? {
guard let ot = ot else {return nil}
switch ot {
case .purchased: return "purchased"
case .familyShared: return "familyShared"
default:
return nil
}
}
func serialize(_ pt: Product.ProductType?) -> String? {
guard let pt = pt else {return nil}
switch pt {
case .autoRenewable: return "autoRenewable"
case .consumable: return "consumable"
case .nonConsumable: return "nonConsumable"
case .nonRenewable: return "nonRenewable"
default:
return nil
}
}
39 changes: 39 additions & 0 deletions ios/IapTypes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// IapTypes.swift
// RNIap
//
// Created by Aguilar Andres on 8/18/22.
//

import Foundation
import StoreKit

typealias RNIapIosPromise = (RCTPromiseResolveBlock, RCTPromiseRejectBlock)

struct ProductOrError {
let product: Product?
let error: Error?
}

public enum StoreError: Error {
case failedVerification
}

enum IapErrors: String, CaseIterable {
case E_UNKNOWN = "E_UNKNOWN"
case E_SERVICE_ERROR = "E_SERVICE_ERROR"
case E_USER_CANCELLED = "E_USER_CANCELLED"
case E_USER_ERROR = "E_USER_ERROR"
case E_ITEM_UNAVAILABLE = "E_ITEM_UNAVAILABLE"
case E_REMOTE_ERROR = "E_REMOTE_ERROR"
case E_NETWORK_ERROR = "E_NETWORK_ERROR"
case E_RECEIPT_FAILED = "E_RECEIPT_FAILED"
case E_RECEIPT_FINISHED_FAILED = "E_RECEIPT_FINISHED_FAILED"
case E_DEVELOPER_ERROR = "E_DEVELOPER_ERROR"
case E_PURCHASE_ERROR = "E_PURCHASE_ERROR"
case E_SYNC_ERROR = "E_SYNC_ERROR"
case E_DEFERRED_PAYMENT = "E_DEFERRED_PAYMENT"
func asInt() -> Int {
return IapErrors.allCases.firstIndex(of: self)!
}
}
30 changes: 30 additions & 0 deletions ios/IapUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// IapUtils.swift
// RNIap
//
// Created by Aguilar Andres on 8/15/22.
//

import Foundation
import StoreKit

public func debugMessage(_ object: Any...) {
#if DEBUG
for item in object {
print("[react-native-iap] \(item)")
}
#endif
}

func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
// Check whether the JWS passes StoreKit verification.
switch result {
case .unverified:
// StoreKit parses the JWS, but it fails verification.
throw StoreError.failedVerification

case .verified(let safe):
// The result is verified. Return the unwrapped value.
return safe
}
}
47 changes: 19 additions & 28 deletions ios/RNIapIos.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,58 +12,49 @@ @interface RCT_EXTERN_MODULE (RNIapIos, NSObject)
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(getItems:
RCT_EXTERN_METHOD(products:
(NSArray*)skus
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(getAvailableItems:
RCT_EXTERN_METHOD(currentEntitlements:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(buyProduct:
RCT_EXTERN_METHOD(purchase:
(NSString*)sku
andDangerouslyFinishTransactionAutomatically:(BOOL)andDangerouslyFinishTransactionAutomatically
applicationUsername:(NSString*)applicationUsername
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(buyProductWithOffer:
(NSString*)sku
forUser:(NSString*)usernameHash
withOffer:(NSDictionary*)discountOffer
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(buyProductWithQuantityIOS:
(NSString*)sku
appAccountToken:(NSString*)appAccountToken
quantity:(NSInteger)quantity
withOffer:(NSDictionary*)discountOffer
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(clearTransaction:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(clearProducts:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(promotedProduct:
(RCTPromiseResolveBlock)resolve
RCT_EXTERN_METHOD(isEligibleForIntroOffer:
(NSString*)groupID
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(buyPromotedProduct:(RCTPromiseResolveBlock)resolve
RCT_EXTERN_METHOD(currentEntitlement:
(NSString*)sku
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(requestReceipt:
(BOOL)refresh
RCT_EXTERN_METHOD(latestTransaction:
(NSString*)sku
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(finishTransaction:
(NSString*)transactionIdentifier
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(getPendingTransactions:
RCT_EXTERN_METHOD(pendingTransactions:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(sync:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

Expand Down
Loading