Skip to content

Sanitize email user attribute before sending to checkout API#462

Open
MathisDetourbet wants to merge 1 commit intosuperwall:developfrom
MathisDetourbet:fix/sanitize-email-user-attribute
Open

Sanitize email user attribute before sending to checkout API#462
MathisDetourbet wants to merge 1 commit intosuperwall:developfrom
MathisDetourbet:fix/sanitize-email-user-attribute

Conversation

@MathisDetourbet
Copy link
Copy Markdown

@MathisDetourbet MathisDetourbet commented Apr 16, 2026

Summary

  • Introduce an Email type with a failable initializer that validates against the regex enforced by the checkout API (^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)
  • Sanitize the email user attribute in mergeAttributes — invalid values are replaced with nil so the server receives null instead of a malformed string
  • Log a warning when an invalid email is dropped so app developers get feedback

Context

When an app sets the email user attribute to a placeholder like "none" (e.g. when the user hasn't provided an email yet), the checkout API rejects the request with an HttpApiDecodeError because "none" is neither a valid email nor null. This silently breaks the Stripe checkout — the payment sheet never opens and the user sees no feedback.

The fix parses the email value at the SDK boundary so the server always receives either a valid email or null, regardless of what the app sends.

Test plan

  • Unit tests on Email type (valid addresses, parameterized invalid inputs including "none", "", "null", "N/A")
  • Unit tests on sanitizeAttribute (valid passthrough, invalid → nil, non-email keys untouched, non-string values untouched)
  • Manual: set email attribute to "none" → verify Stripe checkout opens (previously broken)
  • Manual: set email attribute to a real email → verify it's forwarded correctly

🤖 Generated with Claude Code

Greptile Summary

Introduces an Email value type with a regex-backed failable initialiser and wires it into mergeAttributes via a new sanitizeAttribute helper, so the checkout API always receives either a well-formed email or null instead of placeholder strings like "none". The approach is clean and well-tested; the only notable edge case is that ICU's $ anchor (used by NSRegularExpression) matches just before a trailing , which \z would close.

Confidence Score: 5/5

Safe to merge; all findings are minor style/edge-case suggestions with no blocking issues.

Both inline comments are P2: the $ vs \z anchor edge case is unlikely in practice (trailing newlines in email strings are rare), and the try! concern is cosmetic. No logic bugs, data-loss risk, or missing test coverage for the primary fix path.

Email.swift — the $ anchor and try! points are both in the regex initialisation block.

Important Files Changed

Filename Overview
Sources/SuperwallKit/Identity/Email.swift New validated Email value type; ICU $ anchor may accept trailing-newline emails — \z would be safer.
Sources/SuperwallKit/Identity/UserAttributes.swift Adds sanitizeAttribute to drop invalid email strings before they reach the checkout API; nil return correctly propagates removal through IdentityLogic.mergeAttributes.
Tests/SuperwallKitTests/Identity/EmailTests.swift Good parameterised coverage of valid/invalid cases and sanitizeAttribute pass-through; a trailing-newline case is absent but would reveal the $ anchor issue.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: Sources/SuperwallKit/Identity/Email.swift
Line: 18

Comment:
**`$` anchor may match before a trailing newline**

ICU regular expressions (used by `NSRegularExpression`) treat `$` as matching at the end of the string *or* just before a final `\n` character, even without `.anchorsMatchLines`. This means `"user@example.com\n"` would pass validation and be stored with the trailing newline, potentially causing a server rejection. Use `\z` to strictly anchor at the end of the string in all cases.

```suggestion
    pattern: #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z"#
```

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

---

This is a comment left during a code review.
Path: Sources/SuperwallKit/Identity/Email.swift
Line: 17-19

Comment:
**Consider adding a comment or explicit assertion for `try!`**

The pattern is a compile-time literal and will never throw, so the force-try is safe. Adding a short comment (or a `preconditionFailure` wrapper) makes that intent explicit and prevents future maintainers from worrying about it.

```suggestion
  // Pattern is a validated literal — initialization is guaranteed to succeed.
  private static let regex = try! NSRegularExpression( // swiftlint:disable:this force_try
    pattern: #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"#
  )
```

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

Reviews (1): Last reviewed commit: "Sanitize email user attribute before sen..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

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>
let rawValue: String

private static let regex = try! NSRegularExpression(
pattern: #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"#
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 $ anchor may match before a trailing newline

ICU regular expressions (used by NSRegularExpression) treat $ as matching at the end of the string or just before a final \n character, even without .anchorsMatchLines. This means "user@example.com\n" would pass validation and be stored with the trailing newline, potentially causing a server rejection. Use \z to strictly anchor at the end of the string in all cases.

Suggested change
pattern: #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"#
pattern: #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z"#
Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/SuperwallKit/Identity/Email.swift
Line: 18

Comment:
**`$` anchor may match before a trailing newline**

ICU regular expressions (used by `NSRegularExpression`) treat `$` as matching at the end of the string *or* just before a final `\n` character, even without `.anchorsMatchLines`. This means `"user@example.com\n"` would pass validation and be stored with the trailing newline, potentially causing a server rejection. Use `\z` to strictly anchor at the end of the string in all cases.

```suggestion
    pattern: #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z"#
```

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

Comment on lines +17 to +19
private static let regex = try! NSRegularExpression(
pattern: #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"#
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Consider adding a comment or explicit assertion for try!

The pattern is a compile-time literal and will never throw, so the force-try is safe. Adding a short comment (or a preconditionFailure wrapper) makes that intent explicit and prevents future maintainers from worrying about it.

Suggested change
private static let regex = try! NSRegularExpression(
pattern: #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"#
)
// Pattern is a validated literal — initialization is guaranteed to succeed.
private static let regex = try! NSRegularExpression( // swiftlint:disable:this force_try
pattern: #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"#
)
Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/SuperwallKit/Identity/Email.swift
Line: 17-19

Comment:
**Consider adding a comment or explicit assertion for `try!`**

The pattern is a compile-time literal and will never throw, so the force-try is safe. Adding a short comment (or a `preconditionFailure` wrapper) makes that intent explicit and prevents future maintainers from worrying about it.

```suggestion
  // Pattern is a validated literal — initialization is guaranteed to succeed.
  private static let regex = try! NSRegularExpression( // swiftlint:disable:this force_try
    pattern: #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"#
  )
```

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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