Skip to content

expo-refresh - #189

Open
Makisuo wants to merge 4 commits into
mainfrom
expo-fresh
Open

Makisuo wants to merge 4 commits into
mainfrom
expo-fresh

Conversation

@Makisuo

@Makisuo Makisuo commented Apr 10, 2026

Copy link
Copy Markdown
Collaborator

Greptile Summary

This PR adds a refreshConfiguration native function (iOS + Android) and wires it up in SuperwallProvider so that, in development mode, the Superwall SDK configuration is automatically refreshed after Metro/Fast Refresh cycles — avoiding the need for a full app restart to pick up dashboard config updates.

Confidence Score: 5/5

Safe to merge — all findings are P2 style suggestions with no correctness impact on the Metro-refresh path.

The core feature is logically correct and well-guarded by the DEV flag and the initial-observe skip. All remaining comments are P2: the iOS promise never-reject is benign for the current fire-and-forget caller, and the stale-ref edge case requires two separate non-batched state updates in a Metro refresh scenario (unlikely in practice). The test suite is thorough and covers the key behavioural contracts.

src/SuperwallProvider.tsx (stale-ref guard), ios/SuperwallExpoModule.swift (missing reject path)

Important Files Changed

Filename Overview
src/SuperwallProvider.tsx Adds Metro-refresh detection via a hasObservedConfiguredRef / refreshStateRef snapshot pair; reads isLoading and configurationError from a render-snapshot ref inside an [isConfigured]-only effect, which can silently skip a refresh if those fields are updated in a later render.
ios/SuperwallExpoModule.swift Adds refreshConfiguration async function; resolves promise inside the completion closure but has no error path — the promise always resolves nil regardless of whether the SDK encountered an error.
android/src/main/java/expo/modules/superwallexpo/SuperwallExpoModule.kt Adds refreshConfiguration async function that calls the Android SDK synchronously and resolves the promise immediately, with proper error rejection on Throwable.
src/SuperwallExpoModule.ts Adds refreshConfiguration(): Promise<void> to the native module declaration; minor reformatting of didHandleCustomCallback signature.
src/tests/sdk.behavior.test.tsx Good coverage of the new behaviour: initial-configure skip, dev-rerun trigger, production guard, error catch — all with proper mocking of refreshConfiguration.

Sequence Diagram

sequenceDiagram
    participant Metro as Metro (Fast Refresh)
    participant SW as SuperwallProvider
    participant Ref as refreshStateRef / hasObservedConfiguredRef
    participant Native as SuperwallExpoModule (native)
    participant SDK as SuperwallKit / Android SDK

    Note over SW: App starts — initial configure()
    SW->>Ref: hasObservedConfiguredRef = false
    SDK-->>SW: isConfigured → true
    SW->>Ref: hasObservedConfiguredRef = true (skip refresh)

    Note over Metro: Developer triggers Fast Refresh
    Metro->>SDK: SDK re-initialises
    SDK-->>SW: isConfigured → false
    SDK-->>SW: isConfigured → true
    SW->>Ref: Read refreshStateRef (isLoading=false, configError=null)
    SW->>Ref: hasObservedConfiguredRef already true → proceed
    SW->>Native: refreshConfiguration()
    Native->>SDK: Superwall.shared.refreshConfiguration (iOS callback) / Superwall.instance.refreshConfiguration() (Android sync)
    SDK-->>Native: done
    Native-->>SW: Promise resolves
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: ios/SuperwallExpoModule.swift
Line: 138-145

Comment:
**Promise never rejects on iOS**

The completion closure only ever calls `promise.resolve(nil)`, so any internal failure in `Superwall.shared.refreshConfiguration` is silently swallowed on iOS. The Android counterpart wraps the call in `try/catch` and calls `promise.reject(CodedException(error))` on failure. While the TypeScript caller currently uses `.catch()` for Metro-refresh, future callers awaiting `refreshConfiguration()` would receive a resolved promise even when the SDK failed on iOS. Consider adding error propagation — if the SuperwallKit closure provides an `Error?` parameter, reject there; otherwise wrap the call in a `do/catch`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/SuperwallProvider.tsx
Line: 137-152

Comment:
**Refresh can be silently skipped when `isLoading` lags behind `isConfigured`**

`isLoading` and `configurationError` are read from `refreshStateRef.current` inside an effect that only depends on `[isConfigured]`. If the store emits `isConfigured = true` in one render and then clears `isLoading` in a subsequent render (i.e. the two state updates are not batched), the effect runs while `isLoading` is still `true` and exits early — the refresh is never attempted, with no log or retry. This is an edge case, but Metro-refresh timing makes it more likely than typical usage.

A safer alternative is to also depend on `isLoading` and `configurationError` directly:

```ts
useEffect(() => {
  if (!__DEV__ || !isConfigured || isLoading || configurationError) {
    return
  }

  if (!hasObservedConfiguredRef.current) {
    hasObservedConfiguredRef.current = true
    return
  }

  SuperwallExpoModule.refreshConfiguration().catch((error) => {
    console.error("[Superwall] Failed to refresh configuration after Metro refresh", error)
  })
}, [isConfigured, isLoading, configurationError])
```

The `hasObservedConfiguredRef` already prevents spurious calls, so the extra dependencies don't add refresh invocations — they only ensure the effect re-evaluates when `isLoading` or the error clears after `isConfigured` is set.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "expo-refresh" | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

@pkg-pr-new

pkg-pr-new Bot commented Apr 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/expo-superwall@189

commit: ec2f429

Comment thread ios/SuperwallExpoModule.swift
Comment thread src/SuperwallProvider.tsx Outdated
Makisuo and others added 3 commits June 1, 2026 13:09
# Conflicts:
#	src/SuperwallExpoModule.ts
Address review feedback: the dev-only refresh effect read isLoading and
configurationError from a ref while depending only on [isConfigured]. If
isConfigured flips true in one render before isLoading clears in a later
(unbatched) render, the effect ran while isLoading was still true and bailed
out, silently skipping the refresh. Read both values reactively and add them
to the dependency array so the effect re-evaluates once they settle. The
hasObservedConfiguredRef guard still prevents spurious calls, so no extra
refreshes are introduced. Removes the now-unused refreshStateRef.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	src/__tests__/sdk.behavior.test.tsx
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.

1 participant