Skip to content

4.15.1#466

Merged
yusuftor merged 42 commits intomasterfrom
develop
Apr 28, 2026
Merged

4.15.1#466
yusuftor merged 42 commits intomasterfrom
develop

Conversation

@yusuftor
Copy link
Copy Markdown
Collaborator

@yusuftor yusuftor commented Apr 28, 2026

4.15.1

Enhancements

  • Adds an onCustomCallback parameter to getPaywall.
  • SuperwallOptions.localResources now accepts UIImage's from xcasset files, e.g. UIImage(named: "my-image").
  • Exposes abandoned transaction product params in audience filters.

Fixes

  • Sanitizes email user attribute.

Greptile Summary

This PR bumps the SDK to 4.15.1 with four changes: adds an onCustomCallback parameter to getPaywall (wired through a CustomCallbackRegistry and PaywallViewControllerDelegateAdapter); extends SuperwallOptions.localResources to accept UIImage values from asset catalogs via the new AssetResource protocol; exposes abandoned-transaction product attributes in audience filters; and sanitises the "email" user attribute against a regex-validated Email type before sending it to the server.

Confidence Score: 5/5

Safe to merge — no P0 or P1 issues found; all four feature areas are well-tested and correctly implemented.

The PR is clean with no new critical or warning-level defects. The case-sensitive email key match (switch key vs switch key.lowercased()) was already flagged in a prior review cycle and remains; all other logic is sound. The custom callback registration lifecycle (init → didSet → deinit) is correct, localResources is properly excluded from Encodable, and the NSCache<UIImage, NSData> usage is thread-safe.

Sources/SuperwallKit/Identity/UserAttributes.swift — case-sensitive email key match noted in previous review remains unresolved.

Important Files Changed

Filename Overview
Sources/SuperwallKit/Identity/Email.swift New Email value type with regex-based validation; uses \z anchor correctly to reject trailing newlines.
Sources/SuperwallKit/Config/Options/AssetResource.swift New AssetResource protocol; URL and UIImage conformances are correctly guarded with #if canImport(UIKit).
Sources/SuperwallKit/Config/Options/SuperwallOptions.swift Changed localResources to [String: AssetResource] with a proper ObjC bridge; localResources is excluded from Encodable via a custom CodingKeys enum.
Sources/SuperwallKit/Paywall/View Controller/Web View/LocalFileSchemeHandler.swift Adds UIImage-to-PNG path with a static NSCache<UIImage, NSData> for repeated-request efficiency; thread-safe since NSCache is inherently thread-safe.
Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift Adds customCallbackRegistry dependency and syncCustomCallbackRegistration() called from init (where delegate is already set) and delegate.didSet; cleanup in deinit is correct.
Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift Adds abandoned-transaction product attribute expansion using camelCaseToSnakeCase(); "identifier" key correctly excluded to avoid collision with abandoned_product_id.
Sources/SuperwallKit/Dependencies/DependencyContainer.swift Moves storage.configure(apiKey:) into DependencyContainer.init so it runs synchronously; the StorageTests test validates this timing.
Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywall.swift Adds onCustomCallback parameter to both the callback-based and async getPaywall overloads; default nil preserves backwards compatibility.
Tests/SuperwallKitTests/Paywall/Presentation/CustomCallbackRegistryTests.swift New test suite covers registration on init, no-handler path, delegate reassignment (including nil clear), and deinit cleanup.
Tests/SuperwallKitTests/Identity/EmailTests.swift Comprehensive parametrized tests for valid/invalid email addresses including trailing-newline rejection.

Sequence Diagram

sequenceDiagram
    participant App
    participant Superwall
    participant PaywallViewController
    participant CustomCallbackRegistry
    participant WebView

    App->>Superwall: getPaywall(forPlacement:, delegate:, onCustomCallback:)
    Superwall->>Superwall: internallyGetPaywall(delegate: adapter with onCustomCallback)
    Superwall->>PaywallViewController: init(customCallbackRegistry:)
    PaywallViewController->>PaywallViewController: syncCustomCallbackRegistration()
    PaywallViewController->>CustomCallbackRegistry: register(paywallIdentifier:, handler:)

    WebView-->>CustomCallbackRegistry: custom callback triggered
    CustomCallbackRegistry-->>App: invoke onCustomCallback closure
    App-->>WebView: CustomCallbackResult (.success / .failure)

    PaywallViewController->>PaywallViewController: deinit
    PaywallViewController->>CustomCallbackRegistry: unregister(paywallIdentifier:)
Loading

Comments Outside Diff (2)

  1. Sources/SuperwallKit/Identity/UserAttributes.swift, line 376-391 (link)

    P2 Case-sensitive email key matching

    The switch key comparison is case-sensitive, so attribute keys like "Email" or "EMAIL" will bypass sanitization entirely and be forwarded to the server as-is. This is inconsistent with documented API examples that show lowercase "email" and could silently let invalid emails through for callers using different casing conventions (e.g. Objective-C or legacy integrations).

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: Sources/SuperwallKit/Identity/UserAttributes.swift
    Line: 376-391
    
    Comment:
    **Case-sensitive email key matching**
    
    The `switch key` comparison is case-sensitive, so attribute keys like `"Email"` or `"EMAIL"` will bypass sanitization entirely and be forwarded to the server as-is. This is inconsistent with documented API examples that show lowercase `"email"` and could silently let invalid emails through for callers using different casing conventions (e.g. Objective-C or legacy integrations).
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. Sources/SuperwallKit/Identity/UserAttributes.swift, line 378-383 (link)

    P2 security PII logged in warning message

    The warning log interpolates the caller-supplied stringValue directly into the message. For a genuine (but malformed) email like "user@example" or "john.doe@company", this emits the user's actual email address to the SDK's debug log stream. Depending on the host app's logging pipeline (crash reporters, analytics SDKs), this could constitute an unintended PII leak. Consider redacting or truncating the value, e.g. printing only its length or a masked form.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: Sources/SuperwallKit/Identity/UserAttributes.swift
    Line: 378-383
    
    Comment:
    **PII logged in warning message**
    
    The warning log interpolates the caller-supplied `stringValue` directly into the message. For a genuine (but malformed) email like `"user@example"` or `"john.doe@company"`, this emits the user's actual email address to the SDK's debug log stream. Depending on the host app's logging pipeline (crash reporters, analytics SDKs), this could constitute an unintended PII leak. Consider redacting or truncating the value, e.g. printing only its length or a masked form.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

Reviews (3): Last reviewed commit: "Always emit abandoned product attributes" | Re-trigger Greptile

anglinb and others added 30 commits March 26, 2026 09:40
The checkout API rejects `context.identity.email` unless it is a valid
email address or null. Apps that set a placeholder like `"none"` when the
user has no email silently break the Stripe checkout flow because the
server returns a validation error and no checkout session is created.

Introduce an `Email` domain primitive with a failable initializer that
validates against the same regex the API enforces. When merging user
attributes, the SDK now parses the `email` value through `Email` and
drops it (sends null) when invalid, with a warning log.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Configures storage with apiKey inside DependencyContainer.init rather
than via a separate Superwall.makeDependencyContainer factory. Prevents
code paths from reading storage.apiKey between init and configure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix redeem auth race during startup
Adds an optional `onCustomCallback` parameter to `getPaywall(...)`
so paywalls embedded by the developer can handle custom webview
callbacks. Previously this was only wired up through `register()`,
which presents in its own UIWindow.

The handler is stored on `PaywallViewControllerDelegateAdapter` and
registered with `CustomCallbackRegistry` from `PaywallViewController`
on init / when the delegate is reassigned, and unregistered on deinit.

Bumps version to 4.15.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Only unregister the custom callback handler when this PVC was the one
that registered it. Prevents a getPaywall-flow VC from clearing a
register-flow handler that happens to share the same paywall identifier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generalises SuperwallOptions.localResources from `[String: URL]` to
`[String: AssetResource]`. URL conforms to AssetResource so existing
call sites are unaffected. New `CatalogAsset(name:bundle:)` registers
a Data Set entry from an .xcassets, resolved at load time via
NSDataAsset — the iOS equivalent of Android's R.raw.* resource IDs.

ObjC keeps a URL-only shim under the same `localResources` name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`UTType.preferredMIMEType` is iOS 14+. On iOS 13, fall back to
`UTTypeCopyPreferredTagWithClass` from MobileCoreServices so catalog
assets are served with a real MIME type (an `<img>` or `<video>` is
refused by WKWebView when the MIME is `application/octet-stream`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SWLocalResourcesViewController: when a catalog asset exists but isn't
  image-decodable, show its UTI and byte count instead of a blank cell.
- AssetResource: remove the unused UIKit import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Satisfies the `trailing_closure` SwiftLint rule — the only remaining
project-level lint violation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- LocalFileSchemeHandler: remove unreferenced UIKit import.
- SuperwallOptions: ObjC setter now replaces only the URL subset of
  localResources, so CatalogAsset entries registered from Swift survive
  a subsequent ObjC assignment in mixed Swift/ObjC codebases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
localResources is documented as set once before configure(), so the
"mixed Swift/ObjC post-configure mutation" scenario the merge was
guarding against isn't part of the intended usage pattern. The setter
goes back to replacing the full dict.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NSDataAsset only resolves Data Sets, so a typical Image Set logo would
fail with "File not found". Try NSDataAsset first (lossless, any file
type), then fall back to UIImage(named:in:compatibleWith:)?.pngData()
so existing Image Sets work without restructuring the asset catalog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Calling NSDataAsset on an Image Set triggers a CoreUI log about a
wrong-typed lookup. Flip the order so UIImage(named:) runs first —
Image Sets resolve cleanly, and NSDataAsset is only reached when
there's no Image Set to mistype.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets customers register an in-memory image directly:
  "logo": UIImage(named: "Logo")!
Served to the webview as image/png via pngData(). Complements
CatalogAsset for cases where eager decoding is acceptable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Debug view was loading catalog assets via NSDataAsset only, so an
Image Set CatalogAsset previewed as "Asset not found" even though
the scheme handler resolved it successfully. Try UIImage(named:)
first, fall through to NSDataAsset for Data Sets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avoids re-encoding on every swlocal:// request, which runs on the main thread.
Also updates the ObjC bridge doc to reflect that UIImage is accepted there too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…alog

Accept asset catalog entries in localResources
yusuftor and others added 12 commits April 27, 2026 17:11
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d-duration

Expose abandoned transaction product params in filters
ICU treats $ as matching before a final \n, so user@example.com\n was
slipping through. Switch to \z and add a regression case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…attribute

Sanitize email user attribute before sending to checkout API
Removes SanitizeAttributeTests since the helper is no longer reachable;
EmailTests still covers the regex including the trailing-newline case.
Renames test funcs from backticked display names to plain identifiers
since newer Swift Testing rejects having both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avoids leaking PII from caller-supplied strings into the SDK log stream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the period guard so consumables and other non-subscription
products surface price, locale, and currency in audience filters,
matching PaywallInfo.placementParams behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yusuftor yusuftor merged commit 4850e1a into master Apr 28, 2026
4 checks passed
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.

4 participants