Skip to content

feat: Purchasely 6.1.0 (anonymous user id, API proxy, Web2App redemption) - #293

Merged
kherembourg merged 26 commits into
mainfrom
feat/6.1.0-native-apis
Sep 7, 2026
Merged

feat: Purchasely 6.1.0 (anonymous user id, API proxy, Web2App redemption)#293
kherembourg merged 26 commits into
mainfrom
feat/6.1.0-native-apis

Conversation

@kherembourg

@kherembourg kherembourg commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Brings the React Native SDK to Purchasely 6.1.0: the anonymous user id, the API proxy, the Web2App redemption listener and the two redemption analytics events.

Native pins: iOS pod 6.1.0, Android io.purchasely:*:6.1.0. Package version 6.1.0 on all five npm packages.

Changelog 6.1.0

Added

builder(key).anonymousUserId(id: string, override = false)

Sets the anonymous user id that the SDK reports for this device. id must be a canonical UUID string. The SDK stores it uppercase, and applies it only when the device holds no anonymous id yet unless override is true.

await Purchasely.builder('API_KEY')
    .anonymousUserId('3f2504e0-4f89-11d3-9a0c-0305e82c3301')
    .start()

JavaScript has no UUID type, so the id crosses as a string and each bridge parses it. A value that is not a canonical UUID is refused with a log and the modifier is skipped. start() still succeeds. Android's UUID.fromString accepts a short form that iOS NSUUID refuses, so the Android bridge adds a round-trip check to make both platforms agree on what canonical means.

builder(key).proxy(api: string | null)

Routes Purchasely API traffic through a proxy instead of api.purchasely.io, for a region where that host is unreachable, such as mainland China. Only the API host changes; the paywall host and the tracking host stay on production.

await Purchasely.builder('API_KEY').proxy('https://svc.purchasely.io').start()

Three states, and they are not interchangeable:

Call Effect
.proxy('https://…') Routes the API host through the proxy
.proxy(null) Clears the proxy, back to api.purchasely.io
never called Leaves the SDK's current setting untouched

api must be an https base URL with a host, and carry no query, fragment or credentials. Start-time only on both platforms; neither native SDK has a runtime setter.

builder(key).webRedemptionListener(cb, appHandlesRedemptionAlert?)

Reports the outcome of a Web2App redemption ({scheme}://ply/redeem/{token}), on the start chain, matching the native shape (webRedemptionDelegate on iOS, webRedemptionListener on Android).

await Purchasely.builder('API_KEY')
    .webRedemptionListener((result) => {
        if (result.isSuccess) unlock(result.context?.subscription)
        else showError(result.errorCode, result.errorMessage)
    }, true) // optional: the app shows the result screen
    .start()

The callback never crosses the bridge. The native side registers itself as the delegate and forwards each outcome as an event, so the modifier only subscribes the JS callback.

It is on the chain for a reason. A redemption can settle during start(), from a cold start that the link itself triggered or a token a previous launch left pending. The modifier subscribes immediately before the native start() call, so that ordering cannot be got wrong. A later call replaces the earlier chain listener rather than stacking a second one, and a builder that is never started subscribes nothing.

Purchasely.addWebRedemptionListener(cb) and removeWebRedemptionListener() remain for an app that must add or replace the listener while the SDK already runs. A redemption settling during start() is then missed.

PLYWebRedemptionResult is { isSuccess, context, replay, errorCode, errorMessage }. context and context.subscription are separately nullable: a success can describe nothing, and a present context can carry no subscription. A failure still reports replay: false and context: null, so the shape never changes.

Three behaviours integrators need:

  • replay is true when the server reports the token was already redeemed. It is a verdict about the token, not about the user.
  • A redemption deeplink is not subject to allowDeeplink.
  • On both platforms, errorMessage for an expired link can carry a masked email address. That hint is personal data. Show it to the user, do not send it to an analytics stack or a crash reporter, and do not make the rule platform-specific. The REDEMPTION_FAILED event drops the hint on iOS and on Android alike.

builder(key).appHandlesRedemptionAlert(handles: boolean)

Decides who shows the outcome. false (the default) keeps the SDK popin and calls the listener after the user acknowledges it. true shows nothing and calls the listener as soon as the redemption settles.

PLYEventName gains 'REDEMPTION_CONSUMED' and 'REDEMPTION_FAILED'

Both are new on the two native platforms in 6.1.0, verified absent in iOS 6.0.1 and Android 6.0.2. PLYEventProperties.redemption carries the payload: token, receipt (id, validation_status), subscriptions limited to active and non-consumable, and purchase_context on success; token and error_code plus a top-level error_message on failure.

SubscriptionSource.WEB_CHECKOUT_STRIPE

Both natives expose a web-checkout source (Android StoreType.WEB_CHECKOUT_STRIPE, iOS PLYSubscriptionSource.stripe) and the JS enum had no member for it. Web2App redemption grants subscriptions from that source, so context.subscription.subscriptionSource is the payload most likely to carry it.

Changed

sdk_public_doc.md documents the four new builder modifiers, the listener, the redemption result, the nullable subscription fields and the subscriptionSource values.

CLAUDE.md: the claim that native tests cannot run in CI was wrong. Android JUnit has run in the build-android job and iOS XCTest in its own job since 2026-07-23. The CI job list was also missing four jobs.

Fixed

A web checkout subscription reported no source. The Android store-type mapping listed four values and StoreType.WEB_CHECKOUT_STRIPE fell into else -> null. iOS passed its raw value through to an enum with no member for it. Fixed on both bridges plus the enum and the constants type.

purchaseToken, nextRenewalDate and cancelledDate are now ?: string | null. The native iOS PLYSubscription has no purchase token property, so the iOS bridge never emitted the key and omits each date when the native value is nil. Android assigns all three unconditionally from nullable fields, so it sends an explicit null. Optional alone was not enough, since it admits undefined but not null. This also affects userSubscriptions() and userSubscriptionsHistory(), which share the mapper.

Action for integrators: typed code that reads purchaseToken without a guard now fails to compile. The fix exposes runtime behaviour that already shipped; it does not change it.

Platform availability

API iOS Android
anonymousUserId yes yes
proxy, including null to clear yes yes
appHandlesRedemptionAlert yes yes
webRedemptionListener on the chain yes yes
REDEMPTION_CONSUMED / REDEMPTION_FAILED yes yes
SubscriptionSource.WEB_CHECKOUT_STRIPE yes yes

iOS privacy manifest

The iOS 6.1.0 pod adds three entries to its PrivacyInfo.xcprivacy: PerformanceData, OtherDiagnosticData and CrashData, all AppFunctionality, none linked to the user and none used for tracking. An app that ships this pod inherits them and may need to update its App Store privacy answers.

Test plan

  • yarn all:prepare clean, yarn test 283 passed, yarn lint 0 errors, yarn typecheck clean
  • Android bridge compiles against io.purchasely:core:6.1.0; 60 JUnit tests pass
  • iOS bridge builds against pod 6.1.0; 83 XCTest tests pass
  • Both example apps launch and render a Purchasely paywall, on an Android device and an iOS simulator
  • Every new test was verified to fail against the behaviour it replaces

Review findings addressed

Two independent reviews ran against this branch. Every finding was verified against the released 6.1.0 sources before acting, and all are fixed:

  • The masked email hint was documented as iOS only. It is on both platforms, and the wording would have led integrators to forward personal data to analytics on Android.
  • webRedemptionListener subscribed on the spot and dropped the removal handle, so two calls left two live subscriptions and an unstarted builder leaked one.
  • removeWebRedemptionListener() left a stale handle, whose later release drove the iOS listener count one too low. RCTEventEmitter calls stopObserving at zero, which clears the flag gating every event the module sends, so analytics and the paywall lifecycle could go silent with their listeners still registered.
  • proxy could be set but never cleared.
  • proxy was wrongly documented as Android-only; MOB-308 did land in iOS 6.1.0.
  • The subscription fields needed | null, not just optional.
  • An RCTLogError red-boxed the host app in debug for a skipped option, while Android only logged. Both now warn.

Notes

  • The Android PLYRedemptionProperties sealed-interface change needs no bridge work: PurchaselyModule.kt already reads the payload with event.properties.toMap().
  • The Console screen preview change and the purchase-context restore need no bridge surface.
  • The Android Kotlin floor does not move. The published core-6.1.0 POM pins kotlin-stdlib 2.3.21, as 6.0.x did. A host app still needs a kotlinVersion override.

@greptile-apps

greptile-apps Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR upgrades all five React Native packages and their native dependencies to Purchasely 6.1.0, adding anonymous-user configuration, Android API proxying, Web2App redemption callbacks, and redemption event types.

  • Adds matching Android and iOS redemption bridges and start-time options.
  • Exposes the new listener, builder APIs, event payloads, and corrected subscription optionality in TypeScript.
  • Adds cross-platform tests, example usage, public documentation, version metadata, and safer per-ref E2E concurrency.
  • One explicit repository formatting requirement remains unmet in newly added TypeScript code.

Confidence Score: 4/5

The functional changes appear sound, but the explicit repository indentation requirement must be satisfied before merging.

The earlier dependency, subscription-payload, and documentation findings are no longer outstanding: all three threads were manually resolved, and the current code or supplied resolution evidence addresses each issue. The only remaining finding is that newly added TypeScript uses two-space indentation despite both applicable repository guides requiring four spaces.

Files Needing Attention: packages/purchasely/src/index.ts, packages/purchasely/src/types.ts

Important Files Changed

Filename Overview
packages/purchasely/android/src/main/java/com/reactnativepurchasely/PurchaselyModule.kt Adds Android 6.1.0 builder options, redemption event mapping, canonical UUID parsing, and shared subscription serialization.
packages/purchasely/ios/PurchaselyRN.m Adds iOS anonymous-user and redemption configuration plus the cross-platform redemption result event.
packages/purchasely/src/startBuilder.ts Adds the anonymous-user, Android proxy, and redemption-alert builder APIs and forwards them atomically at startup.
packages/purchasely/src/index.ts Exposes the Web2App redemption listener API, with new code requiring formatting to meet repository rules.
packages/purchasely/src/types.ts Adds redemption event/result types and corrects subscription field optionality, with some new indentation conflicting with repository rules.
sdk_public_doc.md Documents the new 6.1.0 APIs, lifecycle ordering, platform limitations, and redemption privacy considerations.
.github/workflows/e2e-android.yml Scopes Android E2E concurrency by ref and limits cancellation to superseded pull-request runs.
.github/workflows/e2e-ios.yml Scopes iOS E2E concurrency by ref and limits cancellation to superseded pull-request runs.

Sequence Diagram

sequenceDiagram
    participant App as React Native app
    participant JS as Purchasely TypeScript API
    participant Bridge as Native bridge
    participant SDK as Purchasely 6.1.0 SDK

    App->>JS: addWebRedemptionListener(callback)
    App->>JS: builder(apiKey).options().start()
    JS->>Bridge: start(..., startOptions)
    Bridge->>SDK: configure UUID/proxy/redemption delegate
    SDK-->>Bridge: settled redemption result
    Bridge-->>JS: WEB_REDEMPTION_LISTENER
    JS-->>App: callback(PLYWebRedemptionResult)
Loading

Fix all with Greploop Fix All in Claude Code Fix All in Cursor Fix All in Codex

Prompt To Fix All With AI
### Issue 1
packages/purchasely/src/index.ts:130-193
**Inconsistent TypeScript Indentation**

The new listener declarations and functions use two-space indentation, but the repository guides require four-space indentation. The same pattern appears in the added object properties around lines 611–612 and in `packages/purchasely/src/types.ts` around lines 125–133. This explicit repository requirement must be satisfied before merging.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (2): Last reviewed commit: "docs: document anonymousUserId, proxy an..." | Re-trigger Greptile

Comment thread packages/purchasely/android/build.gradle
Comment thread packages/purchasely/src/types.ts
Comment thread packages/purchasely/src/index.ts Outdated
kherembourg added a commit that referenced this pull request Sep 7, 2026
The native iOS PLYSubscription has no purchase token property, so
PLYSubscription+Hybrid.m asDictionary never emitted the key. The type
promised a required string on every platform. nextRenewalDate and
cancelledDate have the same gap: the iOS bridge omits each key when the
native date is nil.

Reported by Greptile on #293. The gap predates 6.1.0 and also affects
userSubscriptions(), but the new web redemption context surfaces the same
shape on a new API, so the contract is corrected here.
@kherembourg

Copy link
Copy Markdown
Contributor Author

All 3 Greptile findings addressed. Details in the inline threads.

# Finding Outcome
1 P1 Unavailable native dependencies Obsolete. Native 6.1.0 is published (Maven <latest>6.1.0</latest>, CocoaPods 6.1.0). Podfile.lock is genuine pod update Purchasely output (7dd20fd), not hand-written. All 6 build checks pass. No code change.
2 P1 Incomplete iOS subscription payload Valid. Fixed in 122898b. Root cause is broader: native iOS PLYSubscription has no purchase token property, so the mapper could never emit it. nextRenewalDate and cancelledDate had the same defect and were also fixed. All three are now optional and documented.
3 P2 Public guide omits APIs Valid. Fixed in 44faad9. sdk_public_doc.md documents anonymousUserId, proxy (Android only) and the redemption listener, including the "add the listener before start()" rule, the appHandlesRedemptionAlert table and the masked-email warning.

Verification after the fixes: yarn test 269 passed, yarn lint 0 errors, yarn typecheck clean. Android bridge compiles against 6.1.0 with 60 unit tests passing; iOS bridge builds with 77 unit tests passing. Both example apps launch and render a Purchasely paywall (Android device and iOS simulator).

Finding 2 is worth a reviewer's attention beyond this PR: making purchaseToken optional means typed client code that reads it without a guard now fails to compile. That exposes a pre-existing iOS runtime behaviour rather than changing it.

kherembourg added a commit that referenced this pull request Sep 7, 2026
#297)

## The warning

`packages/purchasely/ios/PurchaselyRN.m` line 581 gives the native SDK
the
bridge module as the user attribute delegate:

```objc
[Purchasely setUserAttributeDelegate: self];
```

`packages/purchasely/ios/PurchaselyRN.h` line 12 does not declare
`PLYUserAttributeDelegate` on the class. The compiler reports:

```
packages/purchasely/ios/PurchaselyRN.m:581:43: warning: sending 'PurchaselyRN *const __strong' to parameter of incompatible type 'id<PLYUserAttributeDelegate> _Nonnull'
  581 |     [Purchasely setUserAttributeDelegate: self];
      |                                           ^~~~
note: passing argument to parameter 'userAttributeDelegate' here
 1419 | + (void)setUserAttributeDelegate:(id <PLYUserAttributeDelegate> _Nonnull)userAttributeDelegate;
```

## Why it matters

The warning is a type safety hole, not only untidy output.

The compiler cannot verify the delegate contract on a class that does
not
declare the protocol. `PurchaselyRN` implements two delegate methods:
`onUserAttributeSetWithKey:type:value:source:processingLegalBasis:` and
`onUserAttributeRemovedWithKey:source:`. Both bridge to the
`USER_ATTRIBUTE_SET_LISTENER` and `USER_ATTRIBUTE_REMOVED_LISTENER`
events in
JavaScript.

Without the declaration the compiler gives no check at all on this
contract.
The call site accepts any object, and a change in the protocol stays
invisible until a runtime test exercises the callback. The JavaScript
listener then does not receive events any more, and nothing in the build
reports it.

The declaration restores three compile-time checks:

1. The call site type-checks. `-Wprotocol` and the argument type check
both
   apply, so the warning disappears for the right reason.
2. A signature drift on a selector that the bridge implements becomes a
   warning. The compiler compares each implemented method against the
   protocol declaration.
3. A required method that a later SDK version adds to the protocol
becomes a
   `-Wprotocol` warning on this class.

A pure rename in the native SDK stays silent, because all current
methods
are optional. The declaration still makes the class role explicit, and
it
gives the compiler the contract to check the two points above.

## Why the fix is safe

All three methods of `PLYUserAttributeDelegate` are `@objc optional`.
The
generated Objective-C header in the pinned pod (`Purchasely 6.0.0`,
`Purchasely.framework/Headers/Purchasely-Swift.h` line 1345) shows:

```objc
SWIFT_PROTOCOL("_TtP10Purchasely24PLYUserAttributeDelegate_")
@protocol PLYUserAttributeDelegate
@optional
- (void)onUserAttributeSetWithKey:(NSString * _Nonnull)key type:(enum PLYUserAttributeType)type value:(id _Nullable)value source:(enum PLYUserAttributeSource)source;
- (void)onUserAttributeSetWithKey:(NSString * _Nonnull)key type:(enum PLYUserAttributeType)type value:(id _Nullable)value source:(enum PLYUserAttributeSource)source processingLegalBasis:(enum PLYDataProcessingLegalBasis)processingLegalBasis;
- (void)onUserAttributeRemovedWithKey:(NSString * _Nonnull)key source:(enum PLYUserAttributeSource)source;
@EnD
```

The declaration adds no required method. The two selectors that the
bridge
implements match the protocol exactly. The behaviour does not change.

## Build evidence

Command, on the CocoaPods workspace:

```
xcodebuild -workspace example/ios/example.xcworkspace -scheme react-native-purchasely \
  -configuration Debug -destination 'generic/platform=iOS' \
  -derivedDataPath example/ios/DerivedData CODE_SIGNING_ALLOWED=NO build
```

Before the fix:

```
PurchaselyRN.m:581:43: warning: sending 'PurchaselyRN *const __strong' to parameter of incompatible type 'id<PLYUserAttributeDelegate> _Nonnull'
** BUILD SUCCEEDED **
```

After the fix:

```
(no PLYUserAttributeDelegate warning)
** BUILD SUCCEEDED **
```

The XCTest bundle `react-native-purchasely-Unit-Tests` also passes on a
booted simulator.

## Scope

The change is one line in one header file. The pull request touches no
other
file. It changes no behaviour on iOS, and it touches nothing on Android.

The header has never declared this protocol. The delegate call site
arrived
in 5.0.4 (#173). The warning is confirmed against the pinned pod of
6.0.0,
so it ships in 6.0.0. Earlier releases are not checked against their own
pod
pin. The warning is not related to the 6.1.0 work in #293.

## Out of scope

The same build prints unrelated `-Wdeprecated-declarations` warnings,
for
example on `setThemeMode:`. This pull request leaves them unchanged.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lder modifiers and the web redemption listener
The native iOS PLYSubscription has no purchase token property, so
PLYSubscription+Hybrid.m asDictionary never emitted the key. The type
promised a required string on every platform. nextRenewalDate and
cancelledDate have the same gap: the iOS bridge omits each key when the
native date is nil.

Reported by Greptile on #293. The gap predates 6.1.0 and also affects
userSubscriptions(), but the new web redemption context surfaces the same
shape on a new API, so the contract is corrected here.
@kherembourg
kherembourg force-pushed the feat/6.1.0-native-apis branch from 44faad9 to baa95aa Compare September 7, 2026 09:06
Comment thread packages/purchasely/src/index.ts Outdated
MOB-308 landed in the iOS 6.1.0 release. The builder exposes
proxy() and proxy(api:), bridged as proxyWithApi:. The bridge and the
docs wrongly described the modifier as Android-only, based on a check
against an earlier develop commit.

The native parameter is an NSURL?, where nil means turn the proxy off
rather than ignore the value. A string that NSURL cannot parse therefore
skips the modifier and logs an error, instead of passing nil and
silently disabling a proxy the app asked for.
@kherembourg kherembourg changed the title feat: prepare the bridge for native 6.1.0 (anonymous user id, proxy, Web2App redemption) feat: Purchasely 6.1.0 (anonymous user id, API proxy, Web2App redemption) Sep 7, 2026
proxy(api) rejected null and the forwarding dropped it, so a proxy could
be set but never cleared. Both native SDKs document null as the way back
to api.purchasely.io.

The three states are now distinct end to end: an absent key leaves each
SDK's current setting untouched, null clears the proxy, a string sets it.
The iOS bridge maps NSNull to proxyWithApi:nil and Android calls
proxy(null) only when the key is present.

Four tests cover the three states and the never-called/cleared
distinction. Verified they fail against the old forwarding.
The listener belongs on the chain, the same shape as the native SDKs
(webRedemptionDelegate on iOS, webRedemptionListener on Android). The
callback never crosses the bridge: the native side registers itself as
the delegate and forwards each outcome as an event, so the builder only
has to subscribe the JS callback.

Chaining it also removes the footgun the standalone form left behind. A
redemption can settle during start(), so a listener added afterwards
misses exactly the case it is needed for. The modifier subscribes at
chain time, which makes that ordering impossible to get wrong. The
optional second argument sets appHandlesRedemptionAlert, mirroring
Android's two-arity form.

The listener moves to its own module so the builder can subscribe
without importing index.ts, which would be a cycle. Its emitter is
built on first use, not at import time, so importing the builder does
not construct one.

addWebRedemptionListener and removeWebRedemptionListener stay for the
runtime case.

Also drops the commented-out anonymousUserId and proxy call sites from
the example app, and the appHandlesRedemptionAlert(false) line that only
restated the default.
…eaks

webRedemptionListener() subscribed on the spot and dropped the removal
handle. Two consequences, both reported on #293: calling the modifier
twice left two live subscriptions, so one redemption invoked both
callbacks and could show two result screens; and a builder that was
never started kept a live subscription forever, retaining a stale
callback.

The callback is now held in builder state and subscribed in start(),
immediately before the native call. That still guarantees the listener
is in place for a redemption settling during start(), which is the
reason the modifier is on the chain at all.

redemption.ts keeps the chain-owned subscription so a later chain
replaces an earlier one. A listener added with addWebRedemptionListener
is app-owned and is left untouched.

Three tests reproduce the reported behaviour and were verified to fail
against it.
The iOS bridge had three tests on the redemption feature and all three
only checked that a symbol exists: the event name, the protocol
conformance and the selector. Nothing checked the payload the delegate
builds, which is where the null policy and the context nesting live.

PLYWebRedemptionResult has no public initializer, so a test cannot build
one. The body construction is split into a class method that takes the
parts, which makes the policy testable: which key holds NSNull, that a
present context with no subscription stays distinct from no context at
all, and that the same five keys appear on success and on failure.

Six tests, verified to fail when the two null cases are collapsed.
Android already covered the equivalent mapping in
PurchaselyModuleTest.kt.
yarn prepare generates declarations with a stricter tsconfig than
yarn typecheck uses, so an unguarded emitterSpy.subscriptions[0] passed
typecheck and then failed the declaration build with TS18048. Both E2E
jobs stop at that step, which is how it surfaced.

docs: correct the CLAUDE.md claim that native tests cannot run in CI.
Both suites have run in CI since 2026-07-23: Android JUnit in the
build-android job and iOS XCTest in its own iOS Unit Tests (bridge) job,
both driven through the example project because they need the React
Native dependencies. The CI job list was also two years stale and
missing four jobs. Both documented commands were run before committing.
removeWebRedemptionListener() left builderSubscription pointing at a
subscription it had already torn down. React Native's removeAllListeners
goes straight to RCTDeviceEventEmitter and settles the native count
itself, while the per-subscription remove() closure still believes it
owns a listener, so the next chain registration sent a second
removeListeners(1) for a listener already accounted for.

On iOS that is not a harmless miscount. RCTEventEmitter.m does
_listenerCount = MAX(_listenerCount - count, 0) and calls stopObserving
the moment it reaches zero, and stopObserving clears shouldEmit, which
gates every event the module sends. One extra decrement could silence
analytics and the presentation lifecycle while their listeners were
still registered.

Reported on #293. The reporter's mechanism was a double remove, which
React Native already guards by nulling the closure; the real path is
removeAllListeners bypassing that closure entirely.
A subscription bought through web checkout reported no source at all on
Android: the storeType mapping listed four values and
StoreType.WEB_CHECKOUT_STRIPE fell into else -> null. iOS passed its raw
value through, but the JS enum had no member for it, so neither platform
gave a caller a usable answer.

This matters for 6.1.0 specifically. Web2App redemption grants
subscriptions from that source, so PLYWebRedemptionResult
context.subscription is the payload most likely to carry it.

Adds the Android when branch, the sourceStripe constant on both bridges,
the Constants field, and SubscriptionSource.WEB_CHECKOUT_STRIPE.

Also widens purchaseToken, nextRenewalDate and cancelledDate to
string | null. Making them optional was not enough: iOS omits the keys,
but Android's PLYSubscription.toMap() assigns each one unconditionally
from a nullable field, so a caller receives an explicit null. Verified
against the 6.1.0 tag.
The docs said the expired-link email hint was iOS only. It is not.
Android 6.1.0 RedemptionOutcome.Expired.toResult() builds errorMessage
as "<message> A new link was sent to <emailHint>.", the same string the
docs showed as the iOS example, and its own source comments it as
"masked email = PII".

The wording was actively harmful: an integrator would gate the
do-not-log rule on Platform.OS === 'ios' and forward personal data to
analytics on Android. Corrected in the type doc, the listener doc, the
public guide and the example app, with an explicit instruction not to
make the rule platform-specific.

The other half of the claim holds on both platforms: Android's toEvent
drops the hint, so REDEMPTION_FAILED never carries it.

Found by an independent review of the pull request.
RCTLogError shows a full-screen RedBox in a debug build, so an invalid
anonymousUserId or an unparsable proxy string looked like a crash even
though start() continues and only the modifier is skipped. Android logs
with Log.e and shows no UI, so the two platforms disagreed on how loud
a skippable misconfiguration is.

RCTLogWarn matches the rest of this file, matches Android, and still
surfaces the mistake at integration time.

Also moves a docblock in redemption.ts onto the function it describes,
and replaces an empty-string cancelledDate in the redemption fixture
with null, which is what Android actually reports.
…source

The Retrieve User Subscriptions section printed subscriptionSource,
nextRenewalDate and cancelledDate without saying any of them can be
absent, and the enum's new WEB_CHECKOUT_STRIPE member was undocumented.

Adds the platform asymmetry (iOS omits the key, Android sends an
explicit null), the Android-only nature of purchaseToken, and a table of
subscriptionSource values noting that a Web2App redemption reports the
web checkout source.
@kherembourg
kherembourg merged commit 6a092e5 into main Sep 7, 2026
10 checks passed
@kherembourg
kherembourg deleted the feat/6.1.0-native-apis branch September 7, 2026 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants