feat: prepare the bridge for native 6.1.0 (anonymous user id, proxy, Web2App redemption) - #293
Draft
kherembourg wants to merge 12 commits into
Draft
feat: prepare the bridge for native 6.1.0 (anonymous user id, proxy, Web2App redemption)#293kherembourg wants to merge 12 commits into
kherembourg wants to merge 12 commits into
Conversation
…mption payload types
…lder modifiers and the web redemption listener
…e web redemption listener
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.
Prepares the React Native SDK for Purchasely 6.1.0: the anonymous user id, the API proxy, the Web2App redemption listener and the two new analytics events.
Blocked on the native release
Native 6.1.0 is not published. Verified today, 2026-09-03:
The new native APIs exist only on the
developbranch of each SDK repo. This PR is written against those sources. CI has run once. Seven of the nine checks fail, and every one of them fails for the same reason: the 6.1.0 artifact does not exist.linttestbuild-androidCould not find io.purchasely:core:6.1.0.build-rn-0-86-androidCould not find io.purchasely:core:6.1.0.E2E Tests (Android T1-T30)Could not find io.purchasely:google-play:6.1.0.andio.purchasely:core:6.1.0build-iosCocoaPods could not find compatible versions for pod "Purchasely"build-rn-0-86-iosiOS Unit Tests (bridge)E2E Tests (iOS T1-T30)Android, from the
build-androidlog:iOS, from the
iOS Unit Tests (bridge)log:Both failures happen at dependency resolution, before compilation. That proves the artifact is missing, and nothing about the bridge code. To show that the bridge code itself is correct apart from the missing symbols, the Android module was compiled locally against the old pin
io.purchasely:core:6.0.1. Every error is a 6.1.0 symbol, and there are no others:The iOS equivalent was not exercised locally: it needs a full
pod installagainst a pod that does not exist yet. Stated as unverified, not as passing. Three Objective-C selectors are therefore derived from the Swift signatures rather than from a compiled header:appAnonymousUserId:override:,webRedemptionDelegate:appHandlesRedemptionAlert:andwebRedemptionCompletedWithResult:. They follow the standard Swift-to-ObjC naming rules and match theonUserAttributeSetWithKey:type:value:source:processingLegalBasis:precedent already in this bridge, but a compiler has not confirmed them.One more step is needed after the native release
example/ios/Podfile.lockis checked in and pinsPurchasely (6.0.0). The iOS log above shows CocoaPods refusing the snapshot against the new podspec requirement. That lockfile cannot be regenerated until the pod is published, and itsSPEC CHECKSUMmust not be hand-written. So once iOS 6.1.0 reaches the CocoaPods trunk, someone must run:and commit the updated
Podfile.lock. Android needs no equivalent step: this repo keeps no Gradle lockfile, so the four Android checks go green on the artifact alone.No workaround was added. There is no reflection, no
try/catcharound a missing symbol, no version-conditional compilation, no vendored native source. When the native 6.1.0 artifacts are published, both checks turn green with no code change. The pin bump is isolated inchore(deps): pin the native iOS and Android SDKs to 6.1.0, so it reverts on its own if the native release slips.Naming, ruled
The two native platforms named this feature differently:
appAnonymousUserId(_ value: UUID?, override: Bool)anonymousUserId(anonymousUserId: UUID?, override: Boolean)React Native needs one name. This PR uses
anonymousUserId.Reason: the RN builder already exposes
appUserId(id)for the logged-in identity.appAnonymousUserIdsits one word away from it and readers would confuse the two constantly.anonymousUserIdalso matches Android, and it matches the existingPurchasely.getAnonymousUserId()getter already in the bridge.Ruled on 2026-09-04: keep
anonymousUserId. The RN builder stays on the Android spelling. No rename is needed, and this point is closed.What this PR adds
builder(key).anonymousUserId(id: string, override = false)appAnonymousUserId(_:override:)anonymousUserId(_, override)builder(key).proxy(api: string)automaticDeeplinkHandlingproxy(api)builder(key).appHandlesRedemptionAlert(handles: boolean)webRedemptionDelegate(_:appHandlesRedemptionAlert:)webRedemptionListener(appHandlesRedemptionAlert, listener)Purchasely.addWebRedemptionListener(cb)/removeWebRedemptionListener()PLYWebRedemptionDelegatePLYWebRedemptionListenerPLYEventNamegains'REDEMPTION_CONSUMED'and'REDEMPTION_FAILED'PLYEventProperties.redemptionproxyis the only asymmetric one. The JS method exists on both platforms so cross-platform code compiles, and the iOS bridge documents that it has no iOS equivalent.The anonymous user id is validated at the bridge
JavaScript has no UUID type, so the id crosses as a
string. Both native builders take aUUID, which is where the guarantee used to live. A string-typed bridge is the only place left to catch a bad value, so both bridges parse it and refuse a bad one loudly:[[NSUUID alloc] initWithUUIDString:], andRCTLogErroronnil.UUID.fromString, andLog.eonIllegalArgumentException.start()never throws for this. The bridge logs an error and skips the modifier, which matches how the native SDKs treat an unusableproxyurl.One difference the bridge closes:
UUID.fromStringis lenient and accepts a short form such as"1-2-3-4-5"thatNSUUIDrefuses. The Android bridge adds a round-trip check, so one id string is accepted, or refused, on both platforms.parseCanonicalUuidis a pure function and is unit-tested.The redemption listener is an event, not a builder callback
A JS function cannot cross the bridge into a native builder, so this follows the
USER_ATTRIBUTE_SET_LISTENERpattern already in the repo: a nativeWEB_REDEMPTION_LISTENERevent plus a JS subscription.appHandlesRedemptionAlertstays on the builder, because it changes what the native SDK presents.Both bridges register the native delegate/listener unconditionally at
start(). The native SDKs have no runtime setter on purpose: a redemption can settle duringstart(), from a cold start that the link itself triggered, or from a token a previous launch left pending. Registering always is behaviour-neutral when JS added no listener. The TSDoc tells integrators to add the listener beforestart()for the same reason.The result type keeps both nullable levels honest:
contextis null when the server's 200 carried nothing to describe. A presentcontextcan still hold a nullsubscription. Neither was flattened.Three behaviours are documented in the TSDoc, all verified in the native sources:
replayistruewhen the server reports the token was already redeemed. The SDK keeps no cache and calls the server every time. It is a verdict about the token, not an observation of the user.allowDeeplink. The native SDK interceptsply/redeemout of band, before the routing branch the gate sits behind.errorMessagefor an expired link can contain a masked email address, so the app can tell the user where the fresh link went. TheREDEMPTION_FAILEDevent drops it. The TSDoc warns integrators to show it to the user and not to forward it to an analytics stack or a crash reporter.The two new analytics events
PLYEventNameis a closed string union, so typed integrator code could not switch on these even though the native SDK emits them. Both are added, withPLYEventProperties.redemptionand its payload types.The payload shape was read from iOS
RedemptionOutcome.swiftand Android'sRedemptionProperties, which the AndroidPLYEventPropertiesRedemptionJsonRegressionTestpins byte for byte:REDEMPTION_CONSUMED:redemptionwithtoken,receipt(id,validation_status),subscriptions(active and non-consumable only), andpurchase_contextwhen the backend returned one.REDEMPTION_FAILED:redemptionwithtokenanderror_code, plus the top-levelerror_message. Known codes:EXPIRED_REDEMPTION_TOKEN,INVALID_REDEMPTION_TOKEN. A transport or parsing failure carries no code.The masked email hint is not in the event type. It reaches the listener only.
What needs no bridge surface
Checked, and deliberately left alone:
PLYRedemptionPropertiesandPLYRedemptionPurchaseContextbehind empty sealed interfaces.PurchaselyModule.ktalready reads the payload withevent.properties.toMap(), which is the supported contract and is unchanged byte for byte. Confirmed, nothing to do.iOS privacy manifest, for RN app developers
The iOS 6.1.0 pod ships three new entries in its
PrivacyInfo.xcprivacy, from the OpenTelemetry and MetricKit work:NSPrivacyCollectedDataTypePerformanceDataAppFunctionalityNSPrivacyCollectedDataTypeOtherDiagnosticDataAppFunctionalityNSPrivacyCollectedDataTypeCrashDataAppFunctionalityAn app that ships this pod inherits them and may need to update its App Store privacy answers. None is linked to the user and none is used for tracking.
Android Kotlin floor
Checked, and it does not move. Native Android pins
kotlin = "2.3.21"in its ownlibs.versions.tomlon both the6.0.2tag anddevelop, so 6.1.0 raises no floor over 6.0.x.The floor that already applies still applies: a host app must override
kotlinVersionto 2.3.21 or later.packages/purchasely/android/gradle.propertiesalready carriesPurchasely_kotlinVersion=2.3.21as the fallback, andexample/android/build.gradlesetskotlinVersion = "2.3.21". The RN template's default is lower, so this stays an integrator step. Flagged here rather than changed: bumping the host template is out of scope for this PR.Verification
Unicode scan for U+2010 to U+2015 over the added lines: 0 hits.
Out of scope
sdk_public_doc.mdand theDocumentation/repo are not updated. That is a doc follow-up once the naming ruling lands.test-projects/is untouched.example/ios/Podfile.lockis not regenerated. It cannot be, until the pod is published. See the step above..anonymousUserId(...)and.proxy(...)as commented call sites, not live ones: a hardcoded UUID would pin every install of the demo app to one anonymous user, which the E2E suite asserts on, and a live proxy would repoint the demo app away from production..appHandlesRedemptionAlert(false)and the listener are live, both behaviour-neutral.