feat: resolve preview paywall by id instead of fetching all paywalls#489
feat: resolve preview paywall by id instead of fetching all paywalls#489Makisuo wants to merge 4 commits into
Conversation
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>
|
PR author is not in the allowed authors list. |
There was a problem hiding this comment.
ℹ️ 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 host —
API.swiftaddsEndpointHost.paywallsV2and aPaywallsV2config on the samebaseHostwith a/v2/path prefix. - Resolver endpoint —
Endpoint.swiftaddsresolvePaywall(byDatabaseId:retryCount:)→GET /v2/paywalls/resolve?id=<id>and removes the now-unusedpaywalls(). - Network layer —
Network.swiftaddsresolvePaywallIdentifier(forDatabaseId:retryCount:)authenticated with the publicapiKey(isForDebugging: false), replacinggetPaywalls()which used the debug key. - Response model —
Paywall.swiftdrops thePaywallslist model and addsPaywallIdentifierResolution(id,identifier,name). - Debug view controller —
finishLoadingPreview()resolves the identifier in one request;loadPreview()is simplified;pressedPreview()guards against the now-always-emptypaywallslist. - Test —
NetworkTests.resolvePaywall_endpointBuildsRequestasserts the builtGET .../v2/paywalls/resolve?id=123request carries anAuthorizationheader.
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.
Claude Opus | 𝕏
…ew-resolve-endpoint # Conflicts: # Sources/SuperwallKit/Network/API.swift
There was a problem hiding this comment.
ℹ️ 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— bothEndpointHost.paywallsV2(this PR) andEndpointHost.mmp(fromdevelop) are preserved in the enum, theApistruct fields, theinit, and thegetConfig(host:)switch (which remains exhaustive), with bothPaywallsV2andMMPconfig 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).
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>
There was a problem hiding this comment.
✅ 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-emptypaywallsproperty, dropped the tap targets from both the name chip andpreviewContainerView, and removed theSuperwallKit_down_arrowaffordance. - Made the chip display-only — renamed
previewPickerButton→previewNameButtonand setisUserInteractionEnabled = 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.
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>
There was a problem hiding this comment.
✅ 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
apiV2HostonNetworkEnvironment—SuperwallOptions.swiftaddsapiV2Hostreturningapi.superwall.dev(.developer),localhost:3001(.local), andapi.superwall.com(default), mirroring the existingenrichmentHostshape. - Repointed
PaywallsV2config —API.swift'sPaywallsV2.hostnow returnsapiV2Hostinstead ofbaseHost, so the resolver hitsapi.superwall.com/v2/paywalls/resolverather 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.
Claude Opus | 𝕏

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 numericpaywall_id(DB id) into the paywallidentifier(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):loadPreview()→network.getPaywalls()(fetch-all)finishLoadingPreview()→paywalls.first { $0.databaseId == paywallDatabaseId }?.identifierpaywallRequestManager.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.paywallsV2endpoint host (base host +/v2/prefix; reachable via thesuperwall.com/v2/*→ V2 Worker route).Endpoint.swift— addresolvePaywall(byDatabaseId:retryCount:)→GET /v2/paywalls/resolve?id=…; remove the now-unusedpaywalls()endpoint.Network.swift— addresolvePaywallIdentifier(forDatabaseId:), authenticated with the public key (isForDebugging: false→Authorization: Bearer <apiKey>); removegetPaywalls().DebugViewController.swift—finishLoadingPreview()resolves the identifier with one request instead of filtering a fetched list;loadPreview()simplified.Paywall.swift— remove the now-unusedPaywallslist model; add the smallPaywallIdentifierResolutionresponse model (kept in this existing file to avoid regeneratingproject.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 buildsGET …/v2/paywalls/resolve?id=123with an Authorization header.xcodebuild … build-for-testing→ TEST 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.