Skip to content

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

Open
Makisuo wants to merge 11 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 11 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 before this PR (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.

GET /v2/paywalls/resolve?id=<id>{ id, identifier, name }, scoped to the application in the caller's token.

  • 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:); remove the now-unused paywalls() endpoint.
  • Network.swift — add resolvePaywallIdentifier(forDatabaseId:); 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).

Authentication — updated since this PR was opened

The resolver is authenticated with the debugger's signed preview token (sat_) — the token already carried by the debug deep link (superwall_debug=true&paywall_id=…&token=…) — sent as Authorization: Bearer <debugKey> via isForDebugging: true.

Not the app's public key, as this description originally said. The debugger's QR preview flow never carries the public key, so the endpoint rejected it; the backend now verifies the sat_ token instead (superwall/paywall-next#3583).

The paywall picker — no longer removed

Earlier revisions of this PR removed the in-DebugViewController "Your Paywalls" picker, because it was populated from the fetch-all. It is now restored, so this PR costs no user-visible capability.

superwall/paywall-next#3657 adds GET /v2/paywalls/preview-list, returning id / identifier / name for the non-archived paywalls of the application in the same sat_ token — no presentable paywall JSON. Enough to render the picker at a fraction of the old payload, fetched on demand instead of on every debugger launch.

  • Paywall.swiftPaywallPreviewList / PaywallPreviewListItem decodables
  • Endpoint.swiftlistPreviewPaywalls(retryCount:) on the .paywallsV2 host
  • Network.swiftlistPreviewPaywalls(), same isForDebugging: true auth, with a lower retry count since it only feeds an optional picker
  • DebugViewController.swift — restores previewPickerButton (name, down arrow, tap target) and pressedPreview, keyed off previewPaywalls

Two deliberate behaviours:

  • The list is fetched from viewDidLoad alongside the preview load, not after it. The picker is most useful precisely when the requested paywall fails to render, so it must not sit behind the preview's success path. A failure logs at .warn and leaves the picker empty rather than surfacing an error.
  • pressedPreview requires more than one entry before opening — an action sheet offering only the paywall already on screen is noise.

PaywallPreviewList decodes only data. The endpoint also returns object, has_more and application_id; none are declared, so a future change to those fields can't throw keyNotFound and empty the picker.

Testing

  • NetworkTests.resolvePaywall_endpointBuildsRequest — asserts the endpoint builds GET …/v2/paywalls/resolve?id=123 with an Authorization header.
  • Verified on device: the debugger resolves a deep-link paywall_id and renders the paywall against production.

Rollout

Backend ships first. All four backend PRs are merged to paywall-next main:

PR What
#3456 the resolver endpoint
#3583 switches it to sat_ token auth
#3653 registers the handler in the production wiring
#3657 the preview-list endpoint behind the picker

#3653 matters for rollout. The resolver was registered only in the test-layer wiring, not server.ts, so /v2/paywalls/resolve returned 401 on every host from 2026-07-03 until it was fixed — with a green test suite throughout. Confirm both endpoints return 404 (not 401) for an unauthenticated request before merging:

curl -s -o /dev/null -w "%{http_code}\n" "https://api.superwall.com/v2/paywalls/resolve?id=1"
curl -s -o /dev/null -w "%{http_code}\n" "https://api.superwall.com/v2/paywalls/preview-list"

404 = registered and reachable. 401 = not deployed.

Preview is debug-only, so an SDK build pointed at an environment without the endpoints degrades to a failed preview (and an empty picker), not a broken app.

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𝕏

@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 fb00fc48, which switches the V2 paywall resolver's authentication from the app's public API key back to the debugger's signed preview token, matching what the backend actually authenticates the resolve request with.

  • Auth switched to the debugger tokenNetwork.resolvePaywallIdentifier now sends the request with isForDebugging: true, so makeHeaders uses storage.debugKey (the sat_ token from the deeplink) as the bearer token instead of the app's public apiKey.
  • Docs realigned — the doc comments on Network.resolvePaywallIdentifier, Endpoint.resolvePaywall, and SuperwallOptions were updated to describe the debugger-token auth.
  • CHANGELOG entry added — an UnreleasedFixes note now describes the auth correction, addressing the prior informational nit.
  • Test strengthenedresolvePaywall_endpointBuildsRequest seeds storage.debugKey = "sat_test", builds with isForDebugging: true, and now asserts the exact Authorization: Bearer sat_test header rather than mere presence.

I traced the ordering: DebugManager.handle(deepLinkUrl:) sets storage.debugKey from the deeplink token param before launchDebuggerloadPreviewresolvePaywallIdentifier runs, so the debug key is always populated by request time. The auth change, docs, CHANGELOG, and test are consistent, and all prior review threads are resolved.

Pullfrog  | View workflow run | Using Claude Opus𝕏

yusuftor and others added 3 commits July 28, 2026 14:23
…endpoint

Brings back the multi-paywall picker this branch removed in c6cde4b,
without reinstating the fetch-all that made it removable.

The picker let you switch previews from inside the debugger. It was backed
by the SDK fetching every paywall in full; when that went away the array
could never be populated, `pressedPreview()` was unreachable behind a
`guard !paywalls.isEmpty`, and the chip advertised a dropdown that did
nothing — so it was deleted. Without it, previewing a different paywall
means going back to the dashboard for a fresh QR code.

paywall-next#3657 adds `GET /v2/paywalls/preview-list`, which returns
id/identifier/name for the non-archived paywalls of the application in the
debugger's `sat_` preview token — no presentable paywall JSON. That is
enough to render the picker at a fraction of the old payload, fetched on
demand rather than on every debugger launch.

- `PaywallPreviewList` / `PaywallPreviewListItem` decodables
- `Endpoint.listPreviewPaywalls` on the `.paywallsV2` host
- `Network.listPreviewPaywalls`, same `isForDebugging: true` auth as the
  resolver, with a lower retry count since it only feeds an optional picker
- Restores `previewPickerButton` (name, down arrow, tap target) and
  `pressedPreview`, now keyed off `previewPaywalls`

The list loads after the previewed paywall is on screen, so the picker
never delays what the user asked for, and a failure degrades to an empty
picker rather than an error. `pressedPreview` needs more than one entry
before opening — an action sheet offering only the paywall already on
screen is noise.

Co-Authored-By: Claude Opus 5 <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 critical issues — two rough edges inline, plus a coverage gap and a stale PR description.

Reviewed changes — since the prior review, d2aa389 reverses course on the picker: the "Your Paywalls" multi-paywall picker removed in c6cde4b is restored, now backed by a second new V2 endpoint that returns lightweight rows instead of the fetch-all it replaced.

  • New GET /v2/paywalls/preview-list endpointEndpoint.listPreviewPaywalls(retryCount:) and Network.listPreviewPaywalls(retryCount: = 2) on the existing .paywallsV2 host, authenticated with the same sat_ debugger token as the resolver.
  • New response modelsPaywallPreviewList (data, hasMore) and PaywallPreviewListItem (id, identifier, name) in Paywall.swift.
  • Picker restored in DebugViewControllerpreviewPickerButton regains its down-arrow affordance and pressedPreview target, previewContainerView's tap target is back, and the previewNameButton rename plus isUserInteractionEnabled = false are reverted.
  • Best-effort list load — a new loadPreviewPaywalls() populates previewPaywalls after the previewed paywall renders, logging at .warn and leaving the list empty on failure; pressedPreview() now bails unless previewPaywalls.count > 1.

I traced the deep-link → resolve → render → list-load → switch sequence: @MainActor isolation holds, storage.debugKey is non-optional and always set before either request, paywallDatabaseId is always populated by DebugManager.presentDebugger before the load task, and the synthesized Decodable conformances tolerate the extra object / application_id keys the doc comments describe. Two pre-existing rough edges are restored verbatim rather than introduced here (the activity indicator is never stopped on failure paths, and rapid picker switches can race untracked loadPreview() tasks) — both existed on develop before this PR, so I'm not flagging them as regressions.

ℹ️ The new listPreviewPaywalls endpoint has no test

The resolver got resolvePaywall_endpointBuildsRequest when it landed, but the second endpoint added here ships with no equivalent. It's the same class of thing worth pinning down — host, /v2/ path concatenation, and the debugger-token auth header — and CLAUDE.md asks for a unit test alongside new SDK functionality.

Technical details
# Missing endpoint test for `listPreviewPaywalls`

## Affected sites
- `Tests/SuperwallKitTests/Network/NetworkTests.swift:182-206` — only `resolvePaywall_endpointBuildsRequest` exists; nothing covers `Endpoint<EndpointKinds.Superwall, PaywallPreviewList>.listPreviewPaywalls`.
- `Sources/SuperwallKit/Network/Endpoint.swift:251-259` — the untested endpoint.

## Required outcome
- A test asserting the built request for `listPreviewPaywalls` is a `GET` against `…/v2/paywalls/preview-list` carrying `Authorization: Bearer sat_test` when built with `isForDebugging: true`.
- Optionally a decode test for `PaywallPreviewList` against a representative payload, which would also pin down whether `has_more` is genuinely always present (see the inline comment on `Paywall.swift:62`).

## Suggested approach
Mirror `resolvePaywall_endpointBuildsRequest` verbatim — seed `dependencyContainer.storage.debugKey`, call `makeRequest(with:factory:)`, assert method, URL substring, and the exact `Authorization` header.

ℹ️ The PR description now contradicts the code

The Behavior change section still states the "Your Paywalls" picker "is removed", and neither the description nor the Rollout section mentions /v2/paywalls/preview-list — so a reader would conclude only one backend endpoint gates this SDK change. The resolved review thread also carries the author's earlier statement that the picker was "permanently retired, not a follow-up", which is worth correcting for anyone reading back through the history.

Technical details
# PR description out of date after the picker restore

## Affected sites
- PR #489 body, "## Change" — lists five files and one endpoint; `d2aa389` adds a second endpoint (`GET /v2/paywalls/preview-list`), two new models (`PaywallPreviewList`, `PaywallPreviewListItem`), and `Network.listPreviewPaywalls`.
- PR #489 body, "## Behavior change" — claims the picker is removed and that `pressedPreview()` "now guards against the empty list"; the picker is restored and the guard is now `previewPaywalls.count > 1`.
- PR #489 body, "## Rollout" — describes a single backend dependency; two endpoints must now exist server-side.

## Required outcome
- Description reflects both endpoints, the restored picker, and the degraded-but-safe behaviour when `preview-list` is unavailable (warn log, empty picker, inert affordance).

## Open questions for the human
- Was the picker restore prompted by feedback, or did the backend `preview-list` endpoint land and make it cheap again? A sentence on that would explain the reversal for future readers.

ℹ️ Nitpicks

  • Sources/SuperwallKit/Debug/DebugViewController.swift:123 — the doc comment says the list is "Empty when … the app has a single paywall", but a single-paywall app returns one row; the picker declines because of the count > 1 guard, not emptiness.
  • Sources/SuperwallKit/Models/Paywall/Paywall.swift:38PaywallPreviewListItem duplicates PaywallIdentifierResolution field-for-field, justified only by the possibility that the two endpoints diverge later. Using the resolution type as data's element would compile unchanged and drop the duplicate.

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

Comment thread Sources/SuperwallKit/Debug/DebugViewController.swift Outdated
Comment thread Sources/SuperwallKit/Models/Paywall/Paywall.swift Outdated
Two review findings on the picker restoration.

`hasMore` was a non-optional `Bool` that nothing read. The decoder only
applies `convertFromSnakeCase`, so if `preview-list` ever stopped sending
`has_more` the whole list decode would throw `keyNotFound` and the picker
would silently empty behind a `.warn`. The SDK does not paginate, so the
field is dropped rather than made optional — no reason to carry a decode
dependency on something unused.

`loadPreviewPaywalls()` sat at the end of the render success path, and both
the resolve `catch` and the fetch `catch` return before reaching it. The
picker therefore only populated when the paywall loaded, leaving the
down-arrow inert exactly when switching away is most useful — the case the
picker exists for. It now runs from `viewDidLoad` alongside the preview
load, concurrently and independently, so a failed render still offers
alternatives.

Calling it only from `viewDidLoad` also means switching paywalls via the
picker no longer refetches a list that cannot have changed.

Co-Authored-By: Claude Opus 5 <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 critical issues — both prior threads are cleanly addressed; one leftover dead-end inline.

Reviewed changes — since the prior review, b708cda addresses both inline threads from d2aa389: the paywall picker's list load no longer hangs off the preview's success path, and the unused-but-required has_more field is gone.

  • Picker load hoisted to viewDidLoadloadPreviewPaywalls() now runs in its own Task alongside Task { await loadPreview() } instead of being awaited at the end of finishLoadingPreview()'s do block, so the picker populates even when the requested paywall fails to resolve or render. Running it only there also means switching previews via the picker no longer refetches a list that cannot have changed.
  • hasMore dropped from PaywallPreviewList — the model now decodes data only, and the doc comment records that object, has_more and application_id are deliberately left undecoded so a response-shape change can't turn into a keyNotFound that empties the picker.

I traced the new task's ordering and isolation: storage.debugKey is written synchronously in DebugManager.handle(deepLinkUrl:) before the launchDebugger task, and the view controller is only constructed downstream of it, so the picker request can never fire with an empty bearer token. DebugViewController is @MainActor, so both viewDidLoad tasks inherit that isolation and interleave only at await points — no data race on previewPaywalls. The one behavioural gap left is that pressedPreview still refuses to open when paywallDatabaseId is nil, which b708cda newly makes reachable with a populated list.

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

Comment thread Sources/SuperwallKit/Debug/DebugViewController.swift Outdated
Fills the gap left by the picker restoration: `listPreviewPaywalls` had no
test, while `resolvePaywall` did.

- `listPreviewPaywalls_endpointBuildsRequest` mirrors the resolver's test —
  GET, correct path, and the `sat_` debug key as the bearer. Also asserts
  the URL carries no query string: the application is taken from the token's
  scope server-side, so a client-supplied `application_id` would be a way
  around that scoping.
- Three decoding tests around `PaywallPreviewList`, which declares only
  `data` while the endpoint also returns `object`, `has_more` and
  `application_id`. They pin both directions — undeclared fields present and
  absent — plus the empty-list case, so the response shape can change
  without a `keyNotFound` silently emptying the picker.

Co-Authored-By: Claude Opus 5 <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 — the coverage gap flagged on the prior review is closed. One nit inline on how tightly the endpoint tests pin the URL.

Reviewed changes — since the prior review, 9260802 is test-only: it adds the listPreviewPaywalls coverage the last review asked for, plus decode tests that pin the deliberately-undeclared fields on the picker's response model. No source file changed.

  • Endpoint-build test for the picker requestlistPreviewPaywalls_endpointBuildsRequest seeds storage.debugKey, builds with isForDebugging: true, and asserts the GET, the /v2/paywalls/preview-list path, the absence of any query string (the application comes from the token's scope), and the exact Authorization: Bearer sat_test header.
  • Three decode tests for PaywallPreviewList — undeclared fields present, undeclared fields absent, and an empty data array, each documenting why the model declares only data.
  • PR description brought current — it now describes the restored picker, both V2 endpoints, the sat_ token auth, and a rollout section that distinguishes 404 (registered) from 401 (not deployed), including the production-wiring fix that made the resolver unreachable for three weeks.

I checked the assertions against the code they exercise rather than taking them at face value: Endpoint.makeRequest leaves queryItems nil for this endpoint so the no-? expectation is meaningful, SuperwallRequestData's memberwise init tolerates the omitted requestId, and the decode tests use JSONDecoder.fromSnakeCase — which is exactly EndpointKinds.Superwall.jsonDecoder (EndpointKind.swift:42), so they cover the production path rather than a parallel one. I could not compile or run the suite here (Linux runner, no Xcode), so the tests are reviewed statically.

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

Comment thread Tests/SuperwallKitTests/Network/NetworkTests.swift Outdated
…point tests

Two more review findings.

`pressedPreview` still bailed on `guard let id = paywallDatabaseId`, which
was the last thing keeping the picker inert in the case b708cda set out to
fix: a debug deep link without a `paywall_id` leaves it nil, so nothing
renders, but the now-independent list load populates `previewPaywalls` and
the picker refused to open anyway. The id was only used to mark the current
row with a checkmark, so it never needed to be non-nil.

Replaces both that guard and the `count > 1` check with a single condition —
open when the list contains something other than what is already on screen.
That still declines on an empty list and on a single entry matching the
current paywall, while opening when nothing rendered.

The endpoint tests matched only the path, so they passed regardless of which
host resolved — exactly how the V2 resolver shipped pointing at the v1
`baseHost` (b073dda). Both now assert host and path together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

3 participants