Open
Conversation
Introduces a new `.custom` product type that allows developers using an external PurchaseController to purchase products through their own payment system. Product data is fetched from the Superwall API and templated into paywalls. A custom transaction ID is generated before purchase and used as the original transaction identifier in transaction_complete. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift
Outdated
Show resolved
Hide resolved
Fixes a bug where cancelling and retrying a purchase would reuse the same transaction ID, causing potential duplicate-ID collisions in analytics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Avoids shifting .other's implicit Int raw value from 5 to 6, which could break external consumers persisting rawValue or using ObjC bridged constants. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sources/SuperwallKit/Paywall/Request/Operators/AddPaywallProducts.swift
Outdated
Show resolved
Hide resolved
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
.customproduct type enabling developers with an externalPurchaseControllerto purchase products through their own payment systemgetSuperwallProductsendpoint) and cached for paywall templating (price, period, trial info)originalTransactionIdentifierintransaction_completeNew files
CustomStoreProduct.swift— config model for custom products (decoded fromstore: "CUSTOM")CustomStoreTransaction.swift—StoreTransactionTypeimplementation with pre-generated transaction IDCustomProductTests.swift— unit tests covering decoding, attribute computation, trial eligibility, and product variablesModified files
Product.swift— added.custom(CustomStoreProduct)case toStoreProductTypeProductStore.swift— added.customcaseStoreProductAdapterObjc.swift— addedcustomProductfieldStoreProduct.swift— addedisCustomProductflag andcustomTransactionIdproperty, plusinit(customProduct:)StorePayment.swift— added init for custom productsPaywall.swift— addedcustomProductscomputed propertyPaywallLogic.swift— addedgetCustomProducts(from:)filterAddPaywallProducts.swift— fetches and caches custom products from API, merges intoproductsById, adds custom trial eligibility checkTransactionManager.swift— generates custom transaction ID before purchase, createsCustomStoreTransactionon purchase completionFactoryProtocols.swift/DependencyContainer.swift— addedmakeStoreTransaction(from: CustomStoreTransaction)V2ProductsResponse.swift— added.customtoSuperwallProductPlatformTest plan
CustomStoreProductdecoding and round-trip encodingProductwith.customtype decodingProductStore.customdecodingPaywallLogic.getCustomProductsfilteringStoreProduct(customProduct:)settingisCustomProductflagTestStoreProductattribute computation (price, period, trial)CustomStoreTransactionproperty valuesgetProductVariableswith custom productsCHANGELOG.mdfor any breaking changes, enhancements, or bug fixes.swiftlintin the main directory and fixed any issues.🤖 Generated with Claude Code
Greptile Summary
This PR introduces a new
.customproduct type for Superwall paywalls, allowing developers using an externalPurchaseControllerto display and purchase products from non-App Store payment systems (e.g. Stripe, web billing). Product metadata is fetched from thegetSuperwallProductsAPI endpoint and cached inStoreKitManager, aCustomStoreTransactioncarrying a pre-generated UUID is created at purchase completion, and trial eligibility is determined via entitlement history (mirroring the existing Stripe approach).TestStoreProductis also renamed toAPIStoreProductand gains real trial-price support, fixing a data gap for test mode products.Key changes:
CustomStoreProductmodel andCustomStoreTransactiontype with fullCodable/StoreTransactionTypeconformancefetchAndCacheCustomProductsinAddPaywallProductsfetches API data, handles duplicate product IDs with a warning, and invalidates the cache when entitlements changeprepareToPurchaseunconditionally regeneratescustomTransactionIdbefore each attempt, addressing the previous review comment about reuse across cancellationsProduct.swiftextends to four cascadingtry/catchblocks; any product with an unknown futurestorevalue will propagate aDecodingErrorrather than being silently skippedsubUnitis bound but never used inAPIStoreProduct.trialPeriodPricePerUnit, producing a compiler warningCustomStoreTransactionproperty values are all exercisedConfidence Score: 4/5
customTransactionIdis regenerated before each purchase attempt (fixing the prior review comment), the cache invalidation logic handles duplicates, and trial eligibility correctly falls back to entitlement history. The one forward-compatibility concern inProduct.swift(unknown future store types causing decode failure rather than graceful skip) is a pre-existing structural pattern now extended to four types, which is worth tracking but not a blocker. The unusedsubUnitbinding is a trivial compiler warning. Test coverage is thorough.Sources/SuperwallKit/Models/Product/Product.swift— the four-deep decode chain means unknown future store values will propagate errors upward rather than being skipped gracefully.Important Files Changed
store == "CUSTOM"during decoding, implementsisEqual/hashforNSObject, and isSendable.StoreTransactionTypethat carries a pre-generated UUID as the transaction ID; all StoreKit-specific fields are correctly stubbed tonil.TestStoreProduct; adds real trial price support. Minor issues: stale docstring still refers to "test store products", andsubUnitis bound but unused intrialPeriodPricePerUnitcausing a compiler warning.fetchAndCacheCustomProductsto pre-populate custom product data from the Superwall API, with duplicate-ID handling, cache invalidation on entitlement changes, and custom trial eligibility checking. Previously flagged issues (silent missing-product failure, duplicate crash) are now resolved.customTransactionIdis regenerated unconditionally at the start ofprepareToPurchase(before any early returns), andlatestTransactionuses it to build aCustomStoreTransaction. Transaction tracking and receipt-loading are correctly bypassed for custom products..customcase to the decoder chain as the final fallback; becauseCustomStoreProductvalidatesstore == "CUSTOM"and throws otherwise, any product with an unknown future store type will propagate an error upward instead of being gracefully skipped.formUnionoverride that delegates to the existing priority-mergingunionfunction, ensuring in-place set mutations respect entitlement priority logic.PaywallLogicfiltering,StoreProductflag setting, attribute computation,CustomStoreTransactionvalues, and all four trial eligibility paths..customtoSuperwallProductPlatformandtrialPeriodPricetoSuperwallProductSubscription, both with correctCodingKeys.Sequence Diagram
sequenceDiagram participant PVC as PaywallViewController participant TM as TransactionManager participant APR as AddPaywallProducts participant NW as Network participant SKM as StoreKitManager participant PC as PurchaseController Note over APR,SKM: Paywall Load Phase APR->>APR: fetchAndCacheCustomProducts(customProducts) APR->>NW: getSuperwallProducts() NW-->>APR: SuperwallProductsResponse APR->>SKM: setProduct(StoreProduct(customProduct:), forIdentifier:) APR->>APR: mergeCustomProducts into productsById APR->>APR: checkCustomTrialEligibility(productsById:) Note over PVC,PC: Purchase Phase PVC->>TM: prepareToPurchase(product:, source: .internal) TM->>TM: product.customTransactionId = UUID().uuidString TM->>TM: isFreeTrialAvailable → isCustomProductFreeTrialAvailable TM->>PC: purchase(product) PC-->>TM: .purchased TM->>TM: didPurchase() TM->>TM: latestTransaction(for: product, purchaseDate:) TM->>TM: CustomStoreTransaction(customTransactionId:, productIdentifier:, purchaseDate:) TM->>TM: factory.makeStoreTransaction(from: customTransaction) TM->>TM: trackTransactionDidSucceed(transaction) TM->>TM: finalizeInternalPurchase → dismiss paywallComments Outside Diff (3)
Sources/SuperwallKit/Models/Product/ProductStore.swift, line 267-270 (link)Int raw-value shift may break external consumers
ProductStoreis a publicInt-backed enum. Inserting.custombefore.othersilently shifts.other's implicit raw value from5to6. The SDK's own Codable path uses string keys (so it is not affected), but any consumer that:store.rawValueas anIntto disk/database, orProductStoreviaProductStore(rawValue: 5), or…will silently misinterpret
.otheras.custom(or fail to round-trip at all) after this update.Consider appending
.customafter.other(beforecase none/ end of enum), or explicitly assign stable raw values to all cases to make ordering irrelevant.Prompt To Fix With AI
Sources/SuperwallKit/StoreKit/Products/StoreProduct/StoreProduct.swift, line 522-528 (link)nonisolated(unsafe)on mutable properties of aSendableclassBoth
isCustomProductandcustomTransactionIdare declared asnonisolated(unsafe) varon afinal classthat conforms toSendable. These properties are mutated in async contexts (TransactionManager.prepareToPurchaseanddidPurchase), which means concurrent access is possible without any synchronization, making this technically a data race.The existing
introOfferTokenuses the same pattern, so this is consistent with the current codebase style — but each new mutablenonisolated(unsafe)property expands the unsynchronized mutation surface. IfStoreProductinstances can ever be accessed from multiple tasks concurrently (e.g., retry logic while a purchase is in-flight), consider protecting these with an actor oros_unfair_lock.Prompt To Fix With AI
Sources/SuperwallKit/StoreKit/Products/StoreProduct/StoreProduct.swift, line 697-703 (link)nonisolated(unsafe) varonSendableclass introduces data-race riskBoth
isCustomProductandcustomTransactionIdare declarednonisolated(unsafe) varon aSendablefinal class. Swift'sSendableconformance is satisfied here only because the developer promises no concurrent mutation, but the SDK's async actors make this easy to violate accidentally.customTransactionIdis written inprepareToPurchaseand read inlatestTransaction, both of which run in async contexts. While the current call order is sequential (prepare before purchase), any future code path that calls either function concurrently on the sameStoreProductinstance would produce an undetected data race.The existing
introOfferTokenproperty already uses the same pattern, so changing the overall approach is out of scope here, but it's worth considering actor-isolated storage or a dedicatedPurchaseStateactor for these custom-product fields to make the isolation contract explicit.Prompt To Fix With AI
Prompt To Fix All With AI
Last reviewed commit: "Update AddPaywallPro..."