Skip to content
This repository was archived by the owner on Oct 16, 2025. It is now read-only.
Merged
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
9 changes: 6 additions & 3 deletions Sources/Helpers/StoreKitTypesBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,10 @@ private extension StoreKitTypesBridge {
let string: String
let uppercased: String
}

struct JSONTransactionReason: Codable {
let transactionReason: String
}
Comment on lines +337 to +339
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Rename to align with coding guidelines.

According to the coding guidelines, acronyms in Swift should use Pascal case (first letter caps, rest lowercase) when at the beginning or middle of an identifier. JSONTransactionReason should be JsonTransactionReason.

As per coding guidelines.

Apply this diff to fix the naming:

-    struct JSONTransactionReason: Codable {
+    struct JsonTransactionReason: Codable {
         let transactionReason: String
     }

Also update the usage at line 365:

-        if let decodedReason = try? JSONDecoder().decode(JSONTransactionReason.self, from: transaction.jsonRepresentation),
+        if let decodedReason = try? JSONDecoder().decode(JsonTransactionReason.self, from: transaction.jsonRepresentation),
             decodedReason.transactionReason == "RENEWAL" {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
struct JSONTransactionReason: Codable {
let transactionReason: String
}
struct JsonTransactionReason: Codable {
let transactionReason: String
}
Suggested change
struct JSONTransactionReason: Codable {
let transactionReason: String
}
if let decodedReason = try? JSONDecoder().decode(JsonTransactionReason.self, from: transaction.jsonRepresentation),
decodedReason.transactionReason == "RENEWAL" {
🤖 Prompt for AI Agents
In Sources/Helpers/StoreKitTypesBridge.swift around lines 337–339, rename the
struct JSONTransactionReason to JsonTransactionReason to follow Pascal-case for
acronyms; update its declaration and all references (including the usage at line
365) to the new name so the type matches everywhere.


static func transactionReasonDetails(from transaction: StoreKit.Transaction) -> TransactionReason {
if let revocation = transaction.revocationReason {
Expand All @@ -358,9 +362,8 @@ private extension StoreKitTypesBridge {
}

// Try to infer renewal for iOS <17
if transaction.productType == .autoRenewable,
let expirationDate = transaction.expirationDate,
expirationDate > transaction.purchaseDate {
if let decodedReason = try? JSONDecoder().decode(JSONTransactionReason.self, from: transaction.jsonRepresentation),
decodedReason.transactionReason == "RENEWAL" {
Comment on lines +365 to +366
Copy link
Member

@hyochan hyochan Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current JSON decoding approach is good, but there's a more reliable way to detect renewals using originalID:

// More reliable - compares originalID with current transaction ID
if transaction.productType == .autoRenewable,
   transaction.originalID != transaction.id {
    return TransactionReason(lowercased: "renewal", string: "renewal", uppercased: "RENEWAL")
}

Why this is better:

  • No JSON parsing overhead
  • More reliable: originalID != id definitively means renewal
  • First purchase: originalID == id
  • Any renewal: originalID != id (points to original purchase)

Evidence from issue hyochan/react-native-iap#3054:
Both monthly renewal and yearly upgrade showed originalTransactionIdentifierIOS: "2000001030743975" (different from their own transaction IDs), proving they are renewals/upgrades.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I’ve tested your implementation, but the case where a previously cancelled subscription is purchased again is still marked as a renewal, and its originalId remains the originalId of the first-ever purchase.

I believe jsonRepresentation is more reliable here, as it reflects the actual state of transactionReason.

Here is the part of purchase object that was tested.

countryCodeIOS: "UKR"
environmentIOS: "Sandbox"
expirationDateIOS: 1760169564000
id: "2000001031849543"
isAutoRenewing: true
isUpgradedIOS: falseofferIOS: null
originalTransactionDateIOS: 1747052007000
originalTransactionIdentifierIOS: "2000000917067139"
ownershipTypeIOS: "purchased"
platform: "ios"
productId: "uapro_month"
purchaseState: "purchased"
reasonIOS: "renewal"
reasonStringRepresentationIOS: "renewal"
transactionDate: 1760169384000
transactionReasonIOS: "RENEWAL"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right! Thanks for testing with actual data.

When a user re-purchases a cancelled subscription:

  • The originalID still points to the very first purchase
  • So originalID != id would wrongly mark it as "RENEWAL" instead of "PURCHASE"

Apple's transactionReason in JSON correctly distinguishes:

  • Re-purchase after cancel = "PURCHASE"
  • Actual renewal = "RENEWAL"

Your implementation is correct. My suggestion was flawed - thanks for catching this! 🙏

return TransactionReason(lowercased: "renewal", string: "renewal", uppercased: "RENEWAL")
}

Expand Down