Skip to content

feat: resolve preview paywall by id instead of fetching all paywalls#489

Open
Makisuo wants to merge 4 commits into
developfrom
feat/paywall-preview-resolve-endpoint
Open

feat: resolve preview paywall by id instead of fetching all paywalls#489
Makisuo wants to merge 4 commits into
developfrom
feat/paywall-preview-resolve-endpoint

Conversation

@Makisuo

@Makisuo Makisuo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Problem

The debug/preview flow (open one paywall via a dashboard QR / deep link) fetches ALL paywalls for the app (GET /api/v1/paywalls) just to translate the deep-link numeric paywall_id (DB id) into the paywall identifier (slug), then fetches that one paywall by identifier. The fetch-all is O(#paywalls) and breaks for customers with many paywalls (slow / times out).

Flow today (DebugViewController):

  1. loadPreview()network.getPaywalls() (fetch-all)
  2. finishLoadingPreview()paywalls.first { $0.databaseId == paywallDatabaseId }?.identifier
  3. paywallRequestManager.getPaywall(from:) (full paywall by identifier)

Change

Replace the fetch-all + in-memory filter (steps 1–2) with a single resolver lookup, then keep the existing full-paywall fetch (step 3) untouched.

New V2 endpoint (backend: superwall/paywall-next#3456): GET /v2/paywalls/resolve?id=<id>{ id, identifier, name }, scoped to the app that owns the public key.

  • API.swift — add a .paywallsV2 endpoint host (base host + /v2/ prefix; reachable via the superwall.com/v2/* → V2 Worker route).
  • Endpoint.swift — add resolvePaywall(byDatabaseId:retryCount:)GET /v2/paywalls/resolve?id=…; remove the now-unused paywalls() endpoint.
  • Network.swift — add resolvePaywallIdentifier(forDatabaseId:), authenticated with the public key (isForDebugging: falseAuthorization: Bearer <apiKey>); remove getPaywalls().
  • DebugViewController.swiftfinishLoadingPreview() resolves the identifier with one request instead of filtering a fetched list; loadPreview() simplified.
  • Paywall.swift — remove the now-unused Paywalls list model; add the small PaywallIdentifierResolution response model (kept in this existing file to avoid regenerating project.pbxproj).

Behavior change

The in-DebugViewController "Your Paywalls" multi-paywall picker is removed — it was populated from the fetch-all. Opening a specific paywall by id (the primary preview path) is unaffected. pressedPreview() now guards against the empty list.

Testing

  • NetworkTests.resolvePaywall_endpointBuildsRequest — asserts the endpoint builds GET …/v2/paywalls/resolve?id=123 with an Authorization header.
  • Verified locally: xcodebuild … build-for-testingTEST BUILD SUCCEEDED (framework + test target compile; only pre-existing deprecation warnings). Full test run happens in CI (local simulator was unavailable).

Rollout

Backend is additive and should ship first (superwall/paywall-next#3456). Preview is debug-only, so an SDK build pointed at an environment without the endpoint would only 404 on preview.

The debug/preview flow fetched ALL paywalls for the app just to translate
the deep-link numeric `paywall_id` into the paywall `identifier`, which is
slow and breaks for apps with many paywalls.

Add a single-lookup resolver: `GET /v2/paywalls/resolve?id=<id>` on the V2
API returns `{ id, identifier, name }` for a paywall id, scoped to the
app's public key. The preview flow now resolves the identifier with one
request, then fetches that one paywall exactly as before.

- Add `.paywallsV2` endpoint host (base host, `/v2/` prefix)
- Add `Endpoint.resolvePaywall(byDatabaseId:)` + `Network.resolvePaywallIdentifier`
  (authenticated with the public key via `isForDebugging: false`)
- Rewrite `DebugViewController` preview to use the resolver; drop the fetch-all
- Remove the now-unused `Paywalls` list model and `getPaywalls()`
- The "Your Paywalls" multi-paywall picker is removed (it depended on the
  fetch-all); previewing one paywall by id is unaffected

Requires the backend resolver endpoint (superwall/paywall-next#3456).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown

PR author is not in the allowed authors list.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one rough edge and a nit worth a look.

Reviewed changes — the debug/preview flow now resolves a deep-link numeric paywall DB id to its identifier (slug) via a single V2 resolver lookup instead of fetching every paywall for the app and filtering in memory.

  • New V2 endpoint hostAPI.swift adds EndpointHost.paywallsV2 and a PaywallsV2 config on the same baseHost with a /v2/ path prefix.
  • Resolver endpointEndpoint.swift adds resolvePaywall(byDatabaseId:retryCount:)GET /v2/paywalls/resolve?id=<id> and removes the now-unused paywalls().
  • Network layerNetwork.swift adds resolvePaywallIdentifier(forDatabaseId:retryCount:) authenticated with the public apiKey (isForDebugging: false), replacing getPaywalls() which used the debug key.
  • Response modelPaywall.swift drops the Paywalls list model and adds PaywallIdentifierResolution (id, identifier, name).
  • Debug view controllerfinishLoadingPreview() resolves the identifier in one request; loadPreview() is simplified; pressedPreview() guards against the now-always-empty paywalls list.
  • TestNetworkTests.resolvePaywall_endpointBuildsRequest asserts the built GET .../v2/paywalls/resolve?id=123 request carries an Authorization header.

I verified the URL builds correctly (/v2/ + paywalls/resolve), that a 404 fails fast (checked in getRequestId before decode, outside the retry closure — no 6× retry against a missing endpoint), and that the auth switch from debug key to public key is intentional and consistent with the resolver being app-scoped. No correctness issues.

ℹ️ CHANGELOG not updated

CHANGELOG.md has no entry for this change. This is debug/preview-only and touches no public SDK API, so it is arguably exempt from the customer-facing-changes convention — flagging only so the omission is a deliberate call rather than an oversight.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread Sources/SuperwallKit/Debug/DebugViewController.swift Outdated
…ew-resolve-endpoint

# Conflicts:
#	Sources/SuperwallKit/Network/API.swift

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the incremental changes. The only change since the prior review is a clean merge of develop; a prior informational rough-edge thread remains open for the author's call.

Reviewed changes — the only change since the prior review is the merge of develop (03c8d9e) into the branch, which required resolving a conflict in API.swift where develop added an mmp endpoint host alongside this PR's new paywallsV2 host.

  • Merge conflict resolution in API.swift — both EndpointHost.paywallsV2 (this PR) and EndpointHost.mmp (from develop) are preserved in the enum, the Api struct fields, the init, and the getConfig(host:) switch (which remains exhaustive), with both PaywallsV2 and MMP config structs fully defined.

I verified the conflict resolution is complete: mmpHost resolves on NetworkEnvironment (SuperwallOptions.swift:271) so MMP compiles, and none of this PR's own additions (Endpoint.swift, Network.swift, Paywall.swift, DebugViewController.swift) were altered by the merge — the develop-side deltas in those files are the incoming MMP feature, not edits to this PR's resolver logic. The previously-flagged dead preview-picker path in DebugViewController.pressedPreview() is unchanged by this merge and remains open for the author to decide (permanent removal vs. follow-up).

Pullfrog  | Fix it ➔View workflow run | Using Claude Opus𝕏

With the fetch-all gone, the "Your Paywalls" multi-paywall picker can no
longer be populated, so `pressedPreview()` was unreachable and the picker
chip still advertised a dropdown that did nothing.

- Remove `pressedPreview()` and the always-empty `paywalls` property
- Drop the picker tap target from the name chip and the preview container
- Remove the down-arrow affordance; the chip is now a display-only name
  label (renamed `previewPickerButton` -> `previewNameButton`)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — the only change since the prior review is c6cde4b, which removes the dead multi-paywall picker path that was flagged inline on the previous review. With the fetch-all gone, the picker could never be populated, so the author retired it permanently rather than leaving an inert dropdown affordance.

  • Removed the dead picker path — deleted pressedPreview() and the always-empty paywalls property, dropped the tap targets from both the name chip and previewContainerView, and removed the SuperwallKit_down_arrow affordance.
  • Made the chip display-only — renamed previewPickerButtonpreviewNameButton and set isUserInteractionEnabled = false, with a comment explaining why it no longer responds to taps.

I confirmed no dangling references to pressedPreview, previewPickerButton, or the paywalls property remain in DebugViewController.swift, and previewNameButton is consistently defined, constrained, and titled in finishLoadingPreview(). This cleanly resolves the prior inline thread with no new issues.

Pullfrog  | View workflow run | Using Claude Opus𝕏

The V2 API is served from the `superwall.com` domain (api.superwall.com in
production, api.superwall.dev in developer), which is a DIFFERENT apex
domain than `baseHost` (the legacy v1 API on api.superwall.me). Using
baseHost would have sent the resolver to api.superwall.me/v2/... in
production and 404'd.

Add a dedicated `NetworkEnvironment.apiV2Host` (mirroring `enrichmentHost`,
another superwall.com-domain service) and point the `PaywallsV2` host
config at it. Local uses localhost:3001 (the apps/api wrangler dev port).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — the only change since the prior review is b073dda, which corrects the host the V2 paywall resolver points at: it was reusing the legacy v1 baseHost (api.superwall.me), but the V2 API is served from a different domain.

  • New apiV2Host on NetworkEnvironmentSuperwallOptions.swift adds apiV2Host returning api.superwall.dev (.developer), localhost:3001 (.local), and api.superwall.com (default), mirroring the existing enrichmentHost shape.
  • Repointed PaywallsV2 configAPI.swift's PaywallsV2.host now returns apiV2Host instead of baseHost, so the resolver hits api.superwall.com/v2/paywalls/resolve rather than the v1 host.

I confirmed the new apiV2Host switch is consistent with the closest existing analog (enrichmentHost), where .releaseCandidate and .custom likewise fall to the .com default, and that the doc comments accurately describe the v1-vs-v2 host split. This is a correct fix with no new issues, and the sole open thread from the initial review was already resolved.

Pullfrog  | View workflow run | Using Claude Opus𝕏

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