Conversation
Adds the ability to programmatically restore purchases from JavaScript, enabling use cases like a "Restore Purchases" button in app settings outside of the paywall context. Changes: - iOS: Add toJson() to RestorationResult and AsyncFunction bridge - Android: Add restorationResultToJson() and AsyncFunction bridge - TypeScript: Add RestorationResult type, module declaration, store method - Compat: Add restorePurchases() to legacy Superwall class Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
commit: |
src/SuperwallExpoModule.types.ts
Outdated
| /** | ||
| * Represents the result of a purchase restoration attempt. | ||
| * - `restored`: The restoration completed successfully. | ||
| * - `failed`: The restoration failed, with an accompanying error message. | ||
| */ | ||
| export type RestorationResult = | ||
| | { result: "restored" } | ||
| | { result: "failed"; errorMessage: string } |
There was a problem hiding this comment.
Naming collision with existing
RestorationResult class
There is already an exported RestorationResult abstract class in src/compat/lib/RestorationResult.ts (re-exported from src/compat/index.ts). This new discriminated union type has the same name but a structurally different shape — notably the compat class's Failed.toJson() returns errorMessage: null when no error is present, whereas this new type declares errorMessage: string (never null).
Because src/index.ts uses export type * from "./SuperwallExpoModule.types", this discriminated union will now be publicly exported from the main package entry point under the same name as the compat class. Consumers of the compat SDK who call Superwall.restorePurchases() will want to annotate its Promise<RestorationResultJson> return, but the most naturally discoverable RestorationResult in scope (from the compat path) is the abstract class — leading to a type mismatch. Consider naming this something distinct like RestorationResultRaw or RestorationResultData to avoid the collision.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/SuperwallExpoModule.types.ts
Line: 2336-2343
Comment:
**Naming collision with existing `RestorationResult` class**
There is already an exported `RestorationResult` abstract class in `src/compat/lib/RestorationResult.ts` (re-exported from `src/compat/index.ts`). This new discriminated union type has the same name but a structurally different shape — notably the compat class's `Failed.toJson()` returns `errorMessage: null` when no error is present, whereas this new type declares `errorMessage: string` (never null).
Because `src/index.ts` uses `export type * from "./SuperwallExpoModule.types"`, this discriminated union will now be publicly exported from the main package entry point under the same name as the compat class. Consumers of the compat SDK who call `Superwall.restorePurchases()` will want to annotate its `Promise<RestorationResultJson>` return, but the most naturally discoverable `RestorationResult` in scope (from the compat path) is the abstract class — leading to a type mismatch. Consider naming this something distinct like `RestorationResultRaw` or `RestorationResultData` to avoid the collision.
How can I resolve this? If you propose a fix, please make it concise.| AsyncFunction("restorePurchases") { promise: Promise -> | ||
| ioScope.launch { | ||
| Superwall.instance.restorePurchases().fold({ result -> | ||
| scope.launch { | ||
| promise.resolve(restorationResultToJson(result)) | ||
| } | ||
| }, { error -> | ||
| scope.launch { | ||
| promise.reject(CodedException(error)) | ||
| } | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Platform inconsistency: Android can reject while iOS always resolves
On iOS, restorePurchases() returns a RestorationResult value directly; the promise always resolves (with { result: "restored" } or { result: "failed", ... }). On Android, the .fold() failure branch calls promise.reject(CodedException(error)), meaning if the native SDK wraps an unexpected exception in Result.failure(...), the JS promise rejects rather than resolves.
The TypeScript return type Promise<RestorationResult> implies the promise always resolves on both platforms. A caller who writes:
const result = await superwall.restorePurchases()
// assumes result.result === "restored" | "failed"…will see an unhandled exception on Android in unexpected failure scenarios rather than a { result: "failed" } object.
Consider aligning platforms so that any Android-side exception is converted into a RestorationResult.Failed shape (resolved, not rejected), mirroring iOS's behavior.
Prompt To Fix With AI
This is a comment left during a code review.
Path: android/src/main/java/expo/modules/superwallexpo/SuperwallExpoModule.kt
Line: 421-433
Comment:
**Platform inconsistency: Android can reject while iOS always resolves**
On iOS, `restorePurchases()` returns a `RestorationResult` value directly; the promise always resolves (with `{ result: "restored" }` or `{ result: "failed", ... }`). On Android, the `.fold()` failure branch calls `promise.reject(CodedException(error))`, meaning if the native SDK wraps an unexpected exception in `Result.failure(...)`, the JS promise rejects rather than resolves.
The TypeScript return type `Promise<RestorationResult>` implies the promise always resolves on both platforms. A caller who writes:
```kotlin
const result = await superwall.restorePurchases()
// assumes result.result === "restored" | "failed"
```
…will see an unhandled exception on Android in unexpected failure scenarios rather than a `{ result: "failed" }` object.
Consider aligning platforms so that any Android-side exception is converted into a `RestorationResult.Failed` shape (resolved, not rejected), mirroring iOS's behavior.
How can I resolve this? If you propose a fix, please make it concise.
src/compat/index.ts
Outdated
| import { EventEmitter } from "expo" | ||
| import { version } from "../../package.json" | ||
| import SuperwallExpoModule from "../SuperwallExpoModule" | ||
| import type { RestorationResult as RestorationResultJson } from "../SuperwallExpoModule.types" |
There was a problem hiding this comment.
RestorationResultJson alias not re-exported from compat
RestorationResultJson is used as the return type of the compat restorePurchases() method but is only an internal import alias — it is not exported from the compat entry point. A consumer of Superwall.restorePurchases() who needs to annotate the resolved type cannot conveniently import it from the compat path.
Since the same RestorationResult name is already taken in this module by the abstract class, either:
- Export it under a distinct name (e.g.,
export type { RestorationResult as RestorationResultResponse } from "../SuperwallExpoModule.types"), or - Resolve the naming collision described in the
SuperwallExpoModule.types.tscomment and re-export the type here.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/compat/index.ts
Line: 25
Comment:
**`RestorationResultJson` alias not re-exported from compat**
`RestorationResultJson` is used as the return type of the compat `restorePurchases()` method but is only an internal import alias — it is not exported from the compat entry point. A consumer of `Superwall.restorePurchases()` who needs to annotate the resolved type cannot conveniently import it from the compat path.
Since the same `RestorationResult` name is already taken in this module by the abstract class, either:
1. Export it under a distinct name (e.g., `export type { RestorationResult as RestorationResultResponse } from "../SuperwallExpoModule.types"`), or
2. Resolve the naming collision described in the `SuperwallExpoModule.types.ts` comment and re-export the type here.
How can I resolve this? If you propose a fix, please make it concise.- Rename RestorationResult → RestorationResultResponse in types to avoid naming collision with the compat RestorationResult class - Allow errorMessage to be string | null to match compat Failed.toJson() - Android: resolve with RestorationResult.Failed instead of rejecting the promise, aligning behavior with iOS (promise always resolves) - Re-export RestorationResultResponse from compat entry point so consumers can import the type directly Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Addressed all three review comments in a12eb03:
|
Summary
restorePurchases()across iOS, Android, and TypeScript layers, allowing programmatic purchase restoration from JavaScript (e.g., a "Restore Purchases" button in app settings)RestorationResulttype as a discriminated union (restored|failed) to the TypeScript type systemuseSuperwall) and the compat SDK (Superwallclass)Test plan
AsyncFunctionandtoJson()extensionAsyncFunctionandrestorationResultToJson()restorePurchases()from JS and confirm it returns{ result: "restored" }on successrestorePurchases()from JS and confirm it returns{ result: "failed", errorMessage: "..." }on failureSuperwall.restorePurchases()works withawaitConfig()guard🤖 Generated with Claude Code
Greptile Summary
This PR exposes
restorePurchases()across iOS, Android, and TypeScript, adding aRestorationResultdiscriminated union type and wiring it into both the hooks SDK (useSuperwall) and the compat SDK (Superwallclass). The native implementations are straightforward and follow established patterns in the module.Key concerns found during review:
RestorationResultnaming collision (P1):src/SuperwallExpoModule.types.tsintroduces a newRestorationResultdiscriminated union type that is now publicly exported fromsrc/index.tsviaexport type *. However,src/compat/lib/RestorationResult.tsalready exports an abstractRestorationResultclass (withRestored/Failedsubclasses) fromsrc/compat/index.ts. These two types have the same name but different structures — notably,Failed.toJson()in the compat class returnserrorMessage: null, while the new type declareserrorMessage: string. This risks confusing consumers who try to type-annotateSuperwall.restorePurchases()results using the compat-exported class.restorePurchases()returnsResult.failure(exception), whereas iOS always resolves. The TypeScript signaturePromise<RestorationResult>implies the promise always resolves on both platforms, making this silent divergence a potential source of unhandled rejections on Android.RestorationResultJsonalias used as the compatrestorePurchases()return type is not re-exported from the compat entry point, making it difficult for compat SDK consumers to type-annotate the resolved value.Confidence Score: 3/5
Result.failurepath is an edge case, and the type naming collision is a DX/discoverability issue rather than a runtime bug). Resolving these before wider release is recommended.src/SuperwallExpoModule.types.ts(naming collision) andandroid/src/main/java/expo/modules/superwallexpo/SuperwallExpoModule.kt(promise rejection vs resolution inconsistency with iOS).Important Files Changed
RestorationResultdiscriminated union type — creates a naming collision with the existing abstractRestorationResultclass exported fromsrc/compat/lib/RestorationResult.ts, and the two types have structurally different shapes for thefailedcase.restorePurchasesAsyncFunctionusing.fold()on the SDK result; the failure branch rejects the promise rather than resolving with afailedshape, creating a platform behavioral inconsistency with iOS.restorationResultToJsonhelper — correct and symmetric withrestorationResultFromJson.toJson()extension onRestorationResult— clean and consistent with the existingfromJsonstatic method.restorePurchasesAsyncFunctionwrappingSuperwall.shared.restorePurchases()in a SwiftTask; always resolves the promise, matching the TypeScript contract.restorePurchases(): Promise<RestorationResult>declaration to the native module interface — straightforward and correctly typed.restorePurchases()to the compatSuperwallclass withawaitConfig()guard; howeverRestorationResultJsonis an unexported alias making the return type undiscoverable for compat SDK consumers.restorePurchasestoSuperwallStoreinterface and implementation — consistent with other store methods in pattern (noawaitConfig(), directly delegating to the native module).Sequence Diagram
sequenceDiagram participant JS as JavaScript (useSuperwall / Superwall class) participant Bridge as Expo Native Bridge participant iOS as iOS (SuperwallKit) participant Android as Android (Superwall SDK) JS->>Bridge: restorePurchases() alt iOS Bridge->>iOS: Superwall.shared.restorePurchases() iOS-->>Bridge: RestorationResult (.restored | .failed) Bridge->>Bridge: result.toJson() Bridge-->>JS: Promise.resolve({ result: "restored" | "failed", errorMessage? }) else Android Bridge->>Android: Superwall.instance.restorePurchases() Android-->>Bridge: Result<RestorationResult> alt Result.success Bridge->>Bridge: restorationResultToJson(result) Bridge-->>JS: Promise.resolve({ result: "restored" | "failed", errorMessage? }) else Result.failure (unexpected exception) Bridge-->>JS: Promise.reject(CodedException) end endPrompt To Fix All With AI
Last reviewed commit: "Expose restorePurcha..."