Repository: stripe/stripe-ios
Pull request #4937: Add default opt-in for inline Link signup
Author: login: tillh-stripe
Labels: name: ship without translations
name: skip dead code check
Created: 2025-05-28T15:11:02Z | Merged: 2025-05-30T14:14:00Z

## Summary
<!-- Simple summary of what was changed. -->

This pull request adds default opt-in as a form of Link inline signup for US consumers.

The functionality is currently behind a local feature flag and will also be gated by a backend flag (`link_mobile_disable_default_opt_in`).

## Motivation
<!-- Why are you making this change? If it's for fixing a bug, if possible, please include a code snippet or example project that demonstrates the issue. -->

## Testing
<!-- How was the code tested? Be as specific as possible. -->

Unit and snapshot tests.

## Changelog
<!-- Is this a notable change that affects users? If so, add a line to `CHANGELOG.md` and prefix the line with one of the following:
    - [Added] for new features.
    - [Changed] for changes in existing functionality.
    - [Deprecated] for soon-to-be removed features.
    - [Removed] for now removed features.
    - [Fixed] for any bug fixes.
    - [Security] in case of vulnerabilities.
-->



Diff:
diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift
index b3723d31b34a..320b2f287f3a 100644
--- a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift	
+++ b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift	
@@ -59,6 +59,7 @@ struct PaymentSheetTestPlayground: View {
         SettingView(setting: $playgroundController.settings.fcLiteEnabled)
         SettingView(setting: $playgroundController.settings.linkInSPMs)
         SettingView(setting: $playgroundController.settings.linkFlowControllerChanges)
+        SettingView(setting: $playgroundController.settings.linkDefaultOptIn)
     }
 
     var body: some View {
diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift
index 9074cfbf291e..068a98c82daf 100644
--- a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift	
+++ b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift	
@@ -604,6 +604,12 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable {
         case off
     }
 
+    enum LinkDefaultOptIn: String, PickerEnum {
+        static let enumName: String = "Link DOI"
+        case on
+        case off
+    }
+
     enum ConfigurationStyle: String, PickerEnum {
         static let enumName: String = "Style"
         case automatic
@@ -666,6 +672,7 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable {
     var cardBrandAcceptance: CardBrandAcceptance
     var linkInSPMs: LinkInSPMs
     var linkFlowControllerChanges: LinkFlowControllerChanges
+    var linkDefaultOptIn: LinkDefaultOptIn
 
     static func defaultValues() -> PaymentSheetTestPlaygroundSettings {
         return PaymentSheetTestPlaygroundSettings(
@@ -720,7 +727,8 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable {
             rowSelectionBehavior: .default,
             cardBrandAcceptance: .all,
             linkInSPMs: .off,
-            linkFlowControllerChanges: .off
+            linkFlowControllerChanges: .off,
+            linkDefaultOptIn: .off
         )
     }
 
diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift b/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift
index 379966cfdf11..445100984ccd 100644
--- a/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift	
+++ b/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift	
@@ -504,6 +504,7 @@ class PlaygroundController: ObservableObject {
 
             PaymentSheet.LinkFeatureFlags.enableLinkInSPM = newValue.linkInSPMs == .on
             PaymentSheet.LinkFeatureFlags.enableLinkFlowControllerChanges = newValue.linkFlowControllerChanges == .on
+            PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = newValue.linkDefaultOptIn == .on
         }.store(in: &subscribers)
 
         // Listen for analytics
diff --git a/Stripe/StripeiOSTests/LinkInlineSignupElementSnapshotTests.swift b/Stripe/StripeiOSTests/LinkInlineSignupElementSnapshotTests.swift
index fc82954e7a41..7af90b98cb21 100644
--- a/Stripe/StripeiOSTests/LinkInlineSignupElementSnapshotTests.swift
+++ b/Stripe/StripeiOSTests/LinkInlineSignupElementSnapshotTests.swift
@@ -50,6 +50,43 @@ class LinkInlineSignupElementSnapshotTests: STPSnapshotTestCase {
         verify(sut)
     }
 
+    // MARK: Default opt-in
+
+    func testDefaultOptIn_fully_preFilled() {
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = true
+        let sut = makeSUT(
+            saveCheckboxChecked: true,
+            linkAccountEmailAddress: "user@example.com",
+            preFillPhone: "+13105551234",
+            allowsDefaultOptIn: true
+        )
+        verify(sut)
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = false
+    }
+
+    func testDefaultOptIn_fully_preFilled_longEmail() {
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = true
+        let sut = makeSUT(
+            saveCheckboxChecked: true,
+            linkAccountEmailAddress: "user_with_a_super_long_email_address@example.com",
+            preFillPhone: "+13105551234",
+            allowsDefaultOptIn: true
+        )
+        verify(sut)
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = false
+    }
+
+    func testDefaultOptIn_partially_preFilled() {
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = true
+        let sut = makeSUT(
+            saveCheckboxChecked: true,
+            preFillPhone: "+13105551234",
+            allowsDefaultOptIn: true
+        )
+        verify(sut)
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = false
+    }
+
     // MARK: Textfield only mode
 
     func testDefaultState_textFieldsOnly() {
@@ -134,7 +171,8 @@ extension LinkInlineSignupElementSnapshotTests {
         country: String = "US",
         preFillName: String? = nil,
         preFillPhone: String? = nil,
-        showCheckbox: Bool = true
+        showCheckbox: Bool = true,
+        allowsDefaultOptIn: Bool = false
     ) -> LinkInlineSignupElement {
         var configuration = PaymentSheet.Configuration()
         configuration.merchantDisplayName = "[Merchant]"
@@ -151,6 +189,7 @@ extension LinkInlineSignupElementSnapshotTests {
             configuration: configuration,
             showCheckbox: showCheckbox,
             accountService: MockAccountService(),
+            allowsDefaultOptIn: allowsDefaultOptIn,
             linkAccount: linkAccount,
             country: country
         )
diff --git a/Stripe/StripeiOSTests/LinkSignupViewModelTests.swift b/Stripe/StripeiOSTests/LinkSignupViewModelTests.swift
index 2b5b6b7f0825..9ac7385b9a17 100644
--- a/Stripe/StripeiOSTests/LinkSignupViewModelTests.swift
+++ b/Stripe/StripeiOSTests/LinkSignupViewModelTests.swift
@@ -161,6 +161,77 @@ class LinkInlineSignupViewModelTests: STPNetworkStubbingTestCase {
         let sut = makeSUT(country: "US", showCheckbox: false, hasAccountObject: true)
         XCTAssertEqual(sut.consentAction, .implied_v0_0)
     }
+
+    func test_defaultOptIn_allowed_for_eligible_merchant() {
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = true
+        let sut = makeSUT(country: "US", showCheckbox: true, allowsDefaultOptIn: true)
+        XCTAssertEqual(sut.mode, .checkboxWithDefaultOptIn)
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = false
+    }
+
+    func test_defaultOptIn_not_allowed_if_feature_flag_off() {
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = false
+        let sut = makeSUT(country: "US", showCheckbox: true, allowsDefaultOptIn: true)
+        XCTAssertNotEqual(sut.mode, .checkboxWithDefaultOptIn)
+    }
+
+    func test_defaultOptIn_not_allowed_for_ineligible_merchant() {
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = true
+        let sut = makeSUT(country: "US", showCheckbox: true, allowsDefaultOptIn: false)
+        XCTAssertNotEqual(sut.mode, .checkboxWithDefaultOptIn)
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = false
+    }
+
+    func test_defaultOptIn_not_allowed_outside_US() {
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = true
+        let sut = makeSUT(country: "CA", showCheckbox: true, allowsDefaultOptIn: true)
+        XCTAssertNotEqual(sut.mode, .checkboxWithDefaultOptIn)
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = false
+    }
+
+    func test_defaultOptIn_not_allowed_if_showing_checkbox() {
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = true
+        let sut = makeSUT(country: "US", showCheckbox: false, allowsDefaultOptIn: true)
+        XCTAssertNotEqual(sut.mode, .checkboxWithDefaultOptIn)
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = false
+    }
+
+    func test_defaultOptIn_shows_readonly_view_if_completely_prefilled() {
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = true
+        let sut = makeSUT(country: "US", showCheckbox: true, allowsDefaultOptIn: true)
+        sut.emailWasPrefilled = true
+        sut.phoneNumberWasPrefilled = true
+        XCTAssertTrue(sut.shouldShowDefaultOptInView)
+        XCTAssertFalse(sut.shouldShowEmailField)
+        XCTAssertFalse(sut.shouldShowPhoneField)
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = false
+    }
+
+    func test_defaultOptIn_shows_fields_if_user_asked_to_change_signup_data() {
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = true
+        let sut = makeSUT(country: "US", showCheckbox: true, allowsDefaultOptIn: true)
+        sut.emailWasPrefilled = true
+        sut.phoneNumberWasPrefilled = true
+        XCTAssertTrue(sut.shouldShowDefaultOptInView)
+        XCTAssertFalse(sut.shouldShowEmailField)
+        XCTAssertFalse(sut.shouldShowPhoneField)
+        sut.didAskToChangeSignupData = true
+        XCTAssertFalse(sut.shouldShowDefaultOptInView)
+        XCTAssertTrue(sut.shouldShowEmailField)
+        XCTAssertTrue(sut.shouldShowPhoneField)
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = false
+    }
+
+    func test_defaultOptIn_shows_fields_if_not_completely_prefilled() {
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = true
+        let sut = makeSUT(country: "US", showCheckbox: true, allowsDefaultOptIn: true)
+        sut.emailWasPrefilled = true
+        sut.phoneNumberWasPrefilled = false
+        XCTAssertFalse(sut.shouldShowDefaultOptInView)
+        XCTAssertTrue(sut.shouldShowEmailField)
+        XCTAssertTrue(sut.shouldShowPhoneField)
+        PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn = false
+    }
 }
 
 extension LinkInlineSignupViewModelTests {
@@ -200,7 +271,8 @@ extension LinkInlineSignupViewModelTests {
         country: String,
         showCheckbox: Bool,
         hasAccountObject: Bool = false,
-        shouldFailLookup: Bool = false
+        shouldFailLookup: Bool = false,
+        allowsDefaultOptIn: Bool = false
     ) -> LinkInlineSignupViewModel {
         let linkAccount: PaymentSheetLinkAccount? = hasAccountObject
             ? PaymentSheetLinkAccount(email: "user@example.com", session: nil, publishableKey: nil, useMobileEndpoints: false)
@@ -210,6 +282,7 @@ extension LinkInlineSignupViewModelTests {
             configuration: PaymentSheet.Configuration(),
             showCheckbox: showCheckbox,
             accountService: MockAccountService(shouldFailLookup: shouldFailLookup),
+            allowsDefaultOptIn: allowsDefaultOptIn,
             linkAccount: linkAccount,
             country: country
         )
diff --git a/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj b/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj
index b8fcfc6cf525..1cbaf50c32a9 100644
--- a/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj
+++ b/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj
@@ -356,6 +356,7 @@
 		CB46EF492CED1A2E00E9A7F2 /* PaymentMethodIncentive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB46EF482CED1A2E00E9A7F2 /* PaymentMethodIncentive.swift */; };
 		CB46EF4B2CED1BDA00E9A7F2 /* PromoBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB46EF4A2CED1BDA00E9A7F2 /* PromoBadgeView.swift */; };
 		CBC98E222DD53F9D00458788 /* LinkFlowControllerHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC98E212DD53F9D00458788 /* LinkFlowControllerHelpers.swift */; };
+		CB76C0792DE4B57D0009E086 /* LinkDefaultOptInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB76C0782DE4B57D0009E086 /* LinkDefaultOptInView.swift */; };
 		CBE09F4D2D8DA62A0083D0B8 /* LinkConfirmationExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE09F4C2D8DA62A0083D0B8 /* LinkConfirmationExtras.swift */; };
 		CBECCB382DB1BA4100EF0875 /* PaymentMethodWithLinkDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBECCB372DB1BA4100EF0875 /* PaymentMethodWithLinkDetails.swift */; };
 		CBF7BE542D11BF5300A4C172 /* BankAccountInfoViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF7BE532D11BF5300A4C172 /* BankAccountInfoViewSnapshotTests.swift */; };
@@ -851,6 +852,7 @@
 		CB46EF482CED1A2E00E9A7F2 /* PaymentMethodIncentive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodIncentive.swift; sourceTree = "<group>"; };
 		CB46EF4A2CED1BDA00E9A7F2 /* PromoBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromoBadgeView.swift; sourceTree = "<group>"; };
 		CBC98E212DD53F9D00458788 /* LinkFlowControllerHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkFlowControllerHelpers.swift; sourceTree = "<group>"; };
+		CB76C0782DE4B57D0009E086 /* LinkDefaultOptInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkDefaultOptInView.swift; sourceTree = "<group>"; };
 		CBCFE3D39D670C3C77C59722 /* cs-CZ */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "cs-CZ"; path = "cs-CZ.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		CBE09F4C2D8DA62A0083D0B8 /* LinkConfirmationExtras.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkConfirmationExtras.swift; sourceTree = "<group>"; };
 		CBECCB372DB1BA4100EF0875 /* PaymentMethodWithLinkDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodWithLinkDetails.swift; sourceTree = "<group>"; };
@@ -1665,6 +1667,7 @@
 		C71284B45BF3D3B487C1B99D /* InlineSignup */ = {
 			isa = PBXGroup;
 			children = (
+				CB76C0782DE4B57D0009E086 /* LinkDefaultOptInView.swift */,
 				A5E8DD8761B4C52B143038C4 /* LinkInlineSignupElement.swift */,
 				70F6CF58AECE26DA8F1FFED6 /* LinkInlineSignupView-CheckboxElement.swift */,
 				539BC5317DB9A13B247DC576 /* LinkInlineSignupView.swift */,
@@ -2293,6 +2296,7 @@
 				B6859A882BE54CD30018E06C /* PaymentSheetVerticalViewController.swift in Sources */,
 				73F3E8DCF2314972A162B2A3 /* StripePaymentSheetBundleLocator.swift in Sources */,
 				73EE441CF71707651109CE19 /* ConsumerSession+LookupResponse.swift in Sources */,
+				CB76C0792DE4B57D0009E086 /* LinkDefaultOptInView.swift in Sources */,
 				311AC53D6C76953E9B70148A /* ConsumerSession+PublishableKey.swift in Sources */,
 				6198AA6E2BED1C5A00F39D3E /* SavedPaymentMethodRowButton.swift in Sources */,
 				2BEA2A103AD3EE94D60A06D4 /* ConsumerSession.swift in Sources */,
diff --git a/StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en.lproj/Localizable.strings b/StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en.lproj/Localizable.strings
index c514fda8c499..a457f4e2d9b7 100644
--- a/StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en.lproj/Localizable.strings
+++ b/StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en.lproj/Localizable.strings
@@ -98,6 +98,9 @@
 /* Legal text shown when creating a Link account. */
 "By joining Link, you agree to the <terms>Terms</terms> and <privacy>Privacy Policy</privacy>." = "By joining Link, you agree to the <terms>Terms</terms> and <privacy>Privacy Policy</privacy>.";
 
+/* Legal text shown when creating a Link account. */
+"By providing phone number and email, you agree to create a Link account subject to the Link <terms>Terms</terms> and <privacy>Privacy Policy</privacy>." = "By providing phone number and email, you agree to create a Link account subject to the Link <terms>Terms</terms> and <privacy>Privacy Policy</privacy>.";
+
 /* Legal text shown when using AUBECS. */
 "By providing your bank account details and confirming this payment, you agree to this Direct Debit Request and the <terms>Direct Debit Request service agreement</terms>, and authorise Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) to debit your account through the Bulk Electronic Clearing System (BECS) on behalf of %@ (the \"Merchant\") for any amounts separately communicated to you by the Merchant. You certify that you are either an account holder or an authorised signatory on the account listed above." = "By providing your bank account details and confirming this payment, you agree to this Direct Debit Request and the <terms>Direct Debit Request service agreement</terms>, and authorise Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) to debit your account through the Bulk Electronic Clearing System (BECS) on behalf of %@ (the \"Merchant\") for any amounts separately communicated to you by the Merchant. You certify that you are either an account holder or an authorised signatory on the account listed above.";
 
@@ -343,6 +346,10 @@ e.g, 'Pay faster at Example, Inc. and thousands of businesses.' */
 /* The label of a switch indicating whether to save the payment method for future payments. */
 "Save for future payments" = "Save for future payments";
 
+/* Label for a checkbox that when checked allows the payment information
+to be saved and used in future checkout sessions. */
+"Save my info for faster checkout with Link" = "Save my info for faster checkout with Link";
+
 /* The label of a switch indicating whether to save the user's payment details for future payment */
 "Save payment details to %@ for future purchases" = "Save payment details to %@ for future purchases";
 
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Helpers/PaymentSheetLinkAccount.swift b/StripePaymentSheet/StripePaymentSheet/Source/Helpers/PaymentSheetLinkAccount.swift
index dca72903b418..958a3774818d 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/Helpers/PaymentSheetLinkAccount.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/Helpers/PaymentSheetLinkAccount.swift
@@ -48,6 +48,15 @@ class PaymentSheetLinkAccount: PaymentSheetLinkAccountInfoProtocol {
 
         // Clicked button in separate Link sheet
         case clicked_button_mobile_v1 = "clicked_button_mobile_v1"
+
+        // Checkbox pre-checked, w/ email & phone prefilled
+        case prechecked_opt_in_box_prefilled_all = "prechecked_opt_in_box_prefilled_all"
+
+        // Checkbox pre-checked, some fields prefilled
+        case prechecked_opt_in_box_prefilled_some = "prechecked_opt_in_box_prefilled_some"
+
+        // Checkbox pre-checked, no fields prefilled
+        case prechecked_opt_in_box_prefilled_none = "prechecked_opt_in_box_prefilled_none"
     }
 
     // Dependencies
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/v1-elements-sessions/STPElementsSession.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/v1-elements-sessions/STPElementsSession.swift
index 665af4b3ff26..a2984742befc 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/v1-elements-sessions/STPElementsSession.swift	
+++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/v1-elements-sessions/STPElementsSession.swift	
@@ -309,6 +309,10 @@ extension STPElementsSession {
     var incentive: PaymentMethodIncentive? {
         linkSettings?.linkConsumerIncentive.flatMap(PaymentMethodIncentive.init)
     }
+
+    var allowsLinkDefaultOptIn: Bool {
+        linkFlags["link_mobile_disable_default_opt_in"] != true
+    }
 }
 
 extension STPElementsSession {
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkDefaultOptInView.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkDefaultOptInView.swift
new file mode 100644
index 000000000000..36fe3567f190
--- /dev/null
+++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkDefaultOptInView.swift
@@ -0,0 +1,74 @@
+//
+//  LinkDefaultOptInView.swift
+//  StripePaymentSheet
+//
+//  Created by Till Hellmund on 5/26/25.
+//
+
+@_spi(STP) import StripeCore
+@_spi(STP) import StripeUICore
+import UIKit
+
+protocol LinkDefaultOptInViewDelegate: AnyObject {
+    func linkDefaultOptInViewDidSelectChange(_ view: LinkDefaultOptInView)
+}
+
+class LinkDefaultOptInView: UIView {
+    weak var delegate: LinkDefaultOptInViewDelegate?
+
+    init(
+        email: String,
+        phoneNumber: PhoneNumber,
+        theme: ElementsAppearance
+    ) {
+        super.init(frame: .zero)
+
+        let emailLabel = UILabel()
+        emailLabel.text = email
+        emailLabel.font = theme.fonts.footnote
+        emailLabel.textColor = theme.colors.bodyText
+
+        let phoneLabel = UILabel()
+        phoneLabel.text = phoneNumber.string(as: .national)
+        phoneLabel.font = theme.fonts.smallFootnote
+        phoneLabel.textColor = theme.colors.secondaryText
+
+        let contentStackView = UIStackView(arrangedSubviews: [emailLabel, phoneLabel])
+        contentStackView.axis = .vertical
+        contentStackView.isLayoutMarginsRelativeArrangement = true
+        contentStackView.translatesAutoresizingMaskIntoConstraints = false
+        contentStackView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+        contentStackView.setContentHuggingPriority(.defaultLow, for: .horizontal)
+
+        let changeButton = UIButton(type: .system)
+        changeButton.translatesAutoresizingMaskIntoConstraints = false
+        changeButton.setTitle(.Localized.change, for: .normal)
+        changeButton.titleLabel?.font = theme.fonts.footnote
+        changeButton.setTitleColor(theme.colors.primary, for: .normal)
+        changeButton.addTarget(self, action: #selector(onChangeClick), for: .touchUpInside)
+        changeButton.setContentHuggingPriority(.required, for: .horizontal)
+        changeButton.setContentCompressionResistancePriority(.required, for: .horizontal)
+
+        addSubview(contentStackView)
+        addSubview(changeButton)
+
+        NSLayoutConstraint.activate([
+            contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
+            contentStackView.topAnchor.constraint(equalTo: topAnchor, constant: 12),
+            contentStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
+
+            changeButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
+            changeButton.centerYAnchor.constraint(equalTo: centerYAnchor),
+
+            contentStackView.trailingAnchor.constraint(equalTo: changeButton.leadingAnchor, constant: -12),
+        ])
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    @objc func onChangeClick() {
+        delegate?.linkDefaultOptInViewDidSelectChange(self)
+    }
+}
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupElement.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupElement.swift
index 0a7919a8f61d..a7aabcbbf2ae 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupElement.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupElement.swift
@@ -40,12 +40,14 @@ final class LinkInlineSignupElement: Element {
         linkAccount: PaymentSheetLinkAccount?,
         country: String?,
         showCheckbox: Bool,
-        accountService: LinkAccountServiceProtocol
+        accountService: LinkAccountServiceProtocol,
+        allowsDefaultOptIn: Bool
     ) {
         self.init(viewModel: LinkInlineSignupViewModel(
             configuration: configuration,
             showCheckbox: showCheckbox,
             accountService: accountService,
+            allowsDefaultOptIn: allowsDefaultOptIn,
             linkAccount: linkAccount,
             country: country
         ))
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView-CheckboxElement.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView-CheckboxElement.swift
index 51a1d2f4f03b..04566bea3736 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView-CheckboxElement.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView-CheckboxElement.swift
@@ -17,6 +17,7 @@ extension LinkInlineSignupView {
 
         weak var delegate: ElementDelegate?
 
+        private let mode: LinkInlineSignupViewModel.Mode
         private let merchantName: String
         private let appearance: PaymentSheet.Appearance
         /// Controls the stroke color of the checkbox
@@ -42,26 +43,57 @@ extension LinkInlineSignupView {
             // Force the border to match the passed in borderColor
             appearanceCopy.colors.componentBorder = borderColor
 
-            let text = STPLocalizedString(
-                "Save your info for secure 1-click checkout with Link",
-                """
-                Label for a checkbox that when checked allows the payment information
-                to be saved and used in future checkout sessions.
-                """
+            let text = {
+                switch mode {
+                case .checkboxWithDefaultOptIn:
+                    return STPLocalizedString(
+                        "Save my info for faster checkout with Link",
+                        """
+                        Label for a checkbox that when checked allows the payment information
+                        to be saved and used in future checkout sessions.
+                        """
+                    )
+                case .checkbox, .textFieldsOnlyEmailFirst, .textFieldsOnlyPhoneFirst:
+                    return STPLocalizedString(
+                        "Save your info for secure 1-click checkout with Link",
+                        """
+                        Label for a checkbox that when checked allows the payment information
+                        to be saved and used in future checkout sessions.
+                        """
+                    )
+                }
+            }()
+
+            let description: String? = {
+                switch mode {
+                case .checkbox, .textFieldsOnlyEmailFirst, .textFieldsOnlyPhoneFirst:
+                    return String.Localized.pay_faster_at_$merchant_and_thousands_of_merchants(
+                        merchantDisplayName: merchantName
+                    )
+                case .checkboxWithDefaultOptIn:
+                    return nil
+                }
+            }()
+
+            let checkbox = CheckboxButton(
+                text: text,
+                description: description,
+                theme: appearanceCopy.asElementsTheme,
+                alwaysEmphasizeText: true
             )
-
-            let description = String.Localized.pay_faster_at_$merchant_and_thousands_of_merchants(
-                merchantDisplayName: merchantName
-            )
-
-            let checkbox = CheckboxButton(text: text, description: description, theme: appearanceCopy.asElementsTheme)
             checkbox.addTarget(self, action: #selector(didToggleCheckbox), for: .touchUpInside)
             checkbox.isSelected = false
 
             return checkbox
         }()
 
-        init(merchantName: String, appearance: PaymentSheet.Appearance, borderColor: UIColor) {
+        init(
+            mode: LinkInlineSignupViewModel.Mode,
+            merchantName: String,
+            appearance: PaymentSheet.Appearance,
+            borderColor: UIColor
+        ) {
+            self.mode = mode
             self.merchantName = merchantName
             self.appearance = appearance
             self.borderColor = borderColor
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView.swift
index 8cb1a2aceba0..b7943ac07afd 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView.swift
@@ -29,6 +29,7 @@ final class LinkInlineSignupView: UIView {
     let borderColor: UIColor
 
     private(set) lazy var checkboxElement = CheckboxElement(
+        mode: viewModel.mode,
         merchantName: viewModel.configuration.merchantDisplayName,
         appearance: viewModel.configuration.appearance,
         borderColor: borderColor
@@ -37,7 +38,7 @@ final class LinkInlineSignupView: UIView {
     private(set) lazy var emailElement: LinkEmailElement = {
         let element = LinkEmailElement(defaultValue: viewModel.emailAddress,
                                        isOptional: viewModel.isEmailOptional,
-                                       showLogo: viewModel.mode != .textFieldsOnlyPhoneFirst,
+                                       showLogo: viewModel.showLogoInEmailField,
                                        theme: theme)
         element.indicatorTintColor = theme.colors.primary
         return element
@@ -52,7 +53,7 @@ final class LinkInlineSignupView: UIView {
         // Don't allow a default phone number in textFieldsOnly mode.
         // Otherwise, we'd imply consumer consent when it hasn't occurred.
         switch viewModel.mode {
-        case .checkbox:
+        case .checkbox, .checkboxWithDefaultOptIn:
             return PhoneNumberElement(
                 defaultCountryCode: viewModel.configuration.defaultBillingDetails.address.country,
                 defaultPhoneNumber: viewModel.configuration.defaultBillingDetails.phone,
@@ -83,6 +84,7 @@ final class LinkInlineSignupView: UIView {
         let legalView = LinkLegalTermsView(textAlignment: .left,
                                            mode: viewModel.mode,
                                            delegate: self)
+
         legalView.font = theme.fonts.caption
         legalView.textColor = theme.colors.secondaryText
         legalView.tintColor = theme.colors.primary
@@ -96,15 +98,40 @@ final class LinkInlineSignupView: UIView {
         return SectionElement(elements: [emailSection, phoneNumberSection, nameElement], theme: theme)
     }()
 
+    private lazy var defaultOptInElement: Element? = {
+        guard let email = viewModel.emailAddress, let number = viewModel.configuration.defaultBillingDetails.phone else {
+            return nil
+        }
+
+        let phoneNumber = PhoneNumber.fromE164(number) ?? PhoneNumber(number: number, countryCode: viewModel.configuration.defaultBillingDetails.address.country)
+
+        guard let phoneNumber else {
+            return nil
+        }
+
+        let defaultOptInView = LinkDefaultOptInView(
+            email: email,
+            phoneNumber: phoneNumber,
+            theme: theme
+        )
+        defaultOptInView.delegate = self
+
+        let defaultOptInElement = StaticElement(view: defaultOptInView)
+        return SectionElement(elements: [defaultOptInElement], theme: theme)
+    }()
+
     private lazy var formElement: FormElement = {
         var elements: [Element] = []
-        if viewModel.mode == .textFieldsOnlyPhoneFirst {
-            elements.insert(contentsOf: [phoneNumberSection, emailSection, nameSection], at: 0)
-        } else if viewModel.mode == .textFieldsOnlyEmailFirst {
-            elements.insert(contentsOf: [emailSection, phoneNumberSection, nameSection], at: 0)
-        } else if viewModel.mode == .checkbox {
-            elements.insert(contentsOf: [checkboxElement], at: 0)
-            elements.insert(contentsOf: [combinedEmailNameSection], at: 1)
+
+        switch viewModel.mode {
+        case .checkbox:
+            elements.append(contentsOf: [checkboxElement, combinedEmailNameSection])
+        case .checkboxWithDefaultOptIn:
+            elements.append(contentsOf: [checkboxElement, defaultOptInElement, combinedEmailNameSection].compactMap { $0 })
+        case .textFieldsOnlyEmailFirst:
+            elements.append(contentsOf: [emailSection, phoneNumberSection, nameSection])
+        case .textFieldsOnlyPhoneFirst:
+            elements.append(contentsOf: [phoneNumberSection, emailSection, nameSection])
         }
 
         let style: FormElement.Style = viewModel.showCheckbox ? .plain : .bordered
@@ -177,13 +204,19 @@ final class LinkInlineSignupView: UIView {
         if viewModel.mode == .checkbox {
             formElement.toggleChild(combinedEmailNameSection, show: viewModel.shouldShowEmailField, animated: animated)
         }
+        if viewModel.mode == .checkboxWithDefaultOptIn {
+            if let defaultOptInElement {
+                formElement.toggleChild(defaultOptInElement, show: viewModel.shouldShowDefaultOptInView, animated: animated)
+            }
+            formElement.toggleChild(combinedEmailNameSection, show: viewModel.shouldShowEmailField, animated: animated)
+        }
         formElement.toggleChild(emailSection, show: viewModel.shouldShowEmailField, animated: animated)
         formElement.toggleChild(phoneNumberSection, show: viewModel.shouldShowPhoneField, animated: animated)
         formElement.toggleChild(nameSection, show: viewModel.shouldShowNameField, animated: animated)
         formElement.toggleChild(legalTermsElement, show: viewModel.shouldShowLegalTerms, animated: animated)
 
         switch viewModel.mode {
-        case .checkbox:
+        case .checkbox, .checkboxWithDefaultOptIn:
             // 2-way binding
             checkboxElement.isChecked = viewModel.saveCheckboxChecked
         case .textFieldsOnlyEmailFirst, .textFieldsOnlyPhoneFirst:
@@ -194,7 +227,15 @@ final class LinkInlineSignupView: UIView {
 
     private func updateAppearance() {
         backgroundColor = viewModel.configuration.appearance.colors.background
-        layer.cornerRadius = viewModel.configuration.appearance.cornerRadius
+
+        var cornerRadius = viewModel.configuration.appearance.cornerRadius
+        if !viewModel.bordered {
+            // If we're not bordered, the content is right at the border of the view.
+            // Remove corner radius so that we don't cut off anything.
+            cornerRadius = 0
+        }
+        layer.cornerRadius = cornerRadius
+
         // If the borders are hidden give Link a default 1.0 border that contrasts with the background color
         if viewModel.configuration.appearance.borderWidth == 0.0 ||
             viewModel.configuration.appearance.colors.componentBorder.rgba.alpha == 0.0 {
@@ -265,6 +306,14 @@ extension LinkInlineSignupView: LinkInlineSignupViewModelDelegate {
 
 }
 
+extension LinkInlineSignupView: LinkDefaultOptInViewDelegate {
+
+    func linkDefaultOptInViewDidSelectChange(_ view: LinkDefaultOptInView) {
+        viewModel.didAskToChangeSignupData = true
+        updateUI(animated: true)
+    }
+}
+
 extension LinkInlineSignupView: LinkLegalTermsViewDelegate {
 
     func legalTermsView(_ legalTermsView: LinkLegalTermsView, didTapOnLinkWithURL url: URL) -> Bool {
@@ -292,11 +341,24 @@ extension LinkInlineSignupView: EventHandler {
             switch event {
             case .shouldDisableUserInteraction:
                 self.checkboxElement.setUserInteraction(isUserInteractionEnabled: false)
+                self.defaultOptInElement?.setUserInteraction(isUserInteractionEnabled: false)
             case .shouldEnableUserInteraction:
                 self.checkboxElement.setUserInteraction(isUserInteractionEnabled: true)
+                self.defaultOptInElement?.setUserInteraction(isUserInteractionEnabled: true)
             default:
                 break
             }
         }
     }
 }
+
+private extension Element {
+    func setUserInteraction(isUserInteractionEnabled: Bool) {
+        view.isUserInteractionEnabled = false
+        if isUserInteractionEnabled {
+            view.alpha = 1.0
+        } else {
+            view.alpha = 0.6
+        }
+    }
+}
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/ViewModels/LinkInlineSignupViewModel.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/ViewModels/LinkInlineSignupViewModel.swift
index c675df4a213f..fe4bbe25f1c6 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/ViewModels/LinkInlineSignupViewModel.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/ViewModels/LinkInlineSignupViewModel.swift
@@ -23,6 +23,7 @@ final class LinkInlineSignupViewModel {
 
     enum Mode {
         case checkbox // shows the Link inline signup with the checkbox and nested form fields
+        case checkboxWithDefaultOptIn // shows the Link inline signup with a pre-checked checkbox and a label showing the used signup data
         case textFieldsOnlyEmailFirst // shows the Link inline signup without the checkbox, email field first
         case textFieldsOnlyPhoneFirst // shows the Link inline signup without the checkbox, phone number field first
     }
@@ -76,6 +77,11 @@ final class LinkInlineSignupViewModel {
     }
     var phoneNumberWasPrefilled: Bool = false
     var emailWasPrefilled: Bool = false
+    var didAskToChangeSignupData: Bool = false
+
+    private var defaultOptInInfoWasPrefilled: Bool {
+        emailWasPrefilled && phoneNumberWasPrefilled
+    }
 
     var consentAction: PaymentSheetLinkAccount.ConsentAction {
         switch mode {
@@ -87,6 +93,14 @@ final class LinkInlineSignupViewModel {
             } else {
                 return .checkbox_v0
             }
+        case .checkboxWithDefaultOptIn:
+            if phoneNumberWasPrefilled && emailWasPrefilled {
+                return .prechecked_opt_in_box_prefilled_all
+            } else if phoneNumberWasPrefilled || emailWasPrefilled {
+                return .prechecked_opt_in_box_prefilled_some
+            } else {
+                return .prechecked_opt_in_box_prefilled_none
+            }
         case .textFieldsOnlyEmailFirst:
             return .implied_v0
         case .textFieldsOnlyPhoneFirst:
@@ -151,6 +165,8 @@ final class LinkInlineSignupViewModel {
         switch mode {
         case .checkbox:
             return saveCheckboxChecked
+        case .checkboxWithDefaultOptIn:
+            return saveCheckboxChecked && (!defaultOptInInfoWasPrefilled || didAskToChangeSignupData)
         case .textFieldsOnlyEmailFirst:
             return true
         case .textFieldsOnlyPhoneFirst:
@@ -167,6 +183,8 @@ final class LinkInlineSignupViewModel {
                 return false
             }
             return !linkAccount.isRegistered && requiresNameCollection
+        case .checkboxWithDefaultOptIn:
+            return false
         case .textFieldsOnlyPhoneFirst:
             return requiresNameCollection && phoneNumber?.isComplete ?? false
         }
@@ -182,6 +200,9 @@ final class LinkInlineSignupViewModel {
             }
 
             return !linkAccount.isRegistered
+        case .checkboxWithDefaultOptIn:
+            let isExistingConsumer = linkAccount?.isRegistered ?? false
+            return saveCheckboxChecked && (!defaultOptInInfoWasPrefilled || didAskToChangeSignupData) && !isExistingConsumer
         case .textFieldsOnlyPhoneFirst:
             return true
         }
@@ -191,11 +212,20 @@ final class LinkInlineSignupViewModel {
         switch mode {
         case .checkbox:
             return saveCheckboxChecked
+        case .checkboxWithDefaultOptIn:
+            return saveCheckboxChecked
         case .textFieldsOnlyPhoneFirst, .textFieldsOnlyEmailFirst:
             return true
         }
     }
 
+    var shouldShowDefaultOptInView: Bool {
+        guard mode == .checkboxWithDefaultOptIn else {
+            return false
+        }
+        return saveCheckboxChecked && defaultOptInInfoWasPrefilled && !didAskToChangeSignupData
+    }
+
     var action: Action? {
         guard saveCheckboxChecked,
               !lookupFailed
@@ -244,14 +274,14 @@ final class LinkInlineSignupViewModel {
         switch mode {
         case .checkbox:
             return 16
-        case .textFieldsOnlyEmailFirst, .textFieldsOnlyPhoneFirst:
+        case .checkboxWithDefaultOptIn, .textFieldsOnlyEmailFirst, .textFieldsOnlyPhoneFirst:
             return 0
         }
     }
 
     var showCheckbox: Bool {
         switch mode {
-        case .checkbox:
+        case .checkbox, .checkboxWithDefaultOptIn:
             return true
         case .textFieldsOnlyEmailFirst, .textFieldsOnlyPhoneFirst:
             return false
@@ -262,14 +292,14 @@ final class LinkInlineSignupViewModel {
         switch mode {
         case .checkbox:
             return true
-        case .textFieldsOnlyEmailFirst, .textFieldsOnlyPhoneFirst:
+        case .checkboxWithDefaultOptIn, .textFieldsOnlyEmailFirst, .textFieldsOnlyPhoneFirst:
             return false
         }
     }
 
     var isEmailOptional: Bool {
         switch mode {
-        case .checkbox:
+        case .checkbox, .checkboxWithDefaultOptIn:
             return false
         case .textFieldsOnlyEmailFirst:
             return true
@@ -282,6 +312,8 @@ final class LinkInlineSignupViewModel {
         switch mode {
         case .checkbox:
             return false
+        case .checkboxWithDefaultOptIn:
+            return false
         case .textFieldsOnlyEmailFirst:
             return false
         case .textFieldsOnlyPhoneFirst:
@@ -289,10 +321,24 @@ final class LinkInlineSignupViewModel {
         }
     }
 
+    var showLogoInEmailField: Bool {
+        switch mode {
+        case .checkbox, .textFieldsOnlyEmailFirst:
+            return true
+        case .checkboxWithDefaultOptIn:
+            // We show it below the signup view
+            return false
+        case .textFieldsOnlyPhoneFirst:
+            // Already shown in the phone number field
+            return false
+        }
+    }
+
     init(
         configuration: PaymentElementConfiguration,
         showCheckbox: Bool,
         accountService: LinkAccountServiceProtocol,
+        allowsDefaultOptIn: Bool,
         linkAccount: PaymentSheetLinkAccount? = nil,
         country: String? = nil
     ) {
@@ -305,15 +351,17 @@ final class LinkInlineSignupViewModel {
             emailWasPrefilled = true
         }
         if showCheckbox {
-            self.mode = .checkbox
+            let allowsDefaultOptIn = PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn && allowsDefaultOptIn && country == "US"
+            self.mode = allowsDefaultOptIn ? .checkboxWithDefaultOptIn : .checkbox
         } else {
             // If we don't show a checkbox *and* we have a prefilled email, show the phone field first.
             self.mode = (self.emailAddress == nil) ? .textFieldsOnlyEmailFirst : .textFieldsOnlyPhoneFirst
         }
         self.legalName = configuration.defaultBillingDetails.name
         self.country = country
-    }
 
+        self.saveCheckboxChecked = mode == .checkboxWithDefaultOptIn
+    }
 }
 
 private extension LinkInlineSignupViewModel {
@@ -359,5 +407,4 @@ private extension LinkInlineSignupViewModel {
             }
         }
     }
-
 }
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Views/LinkLegalTermsView.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Views/LinkLegalTermsView.swift
index 062c0e30ad54..66f9edee86b9 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Views/LinkLegalTermsView.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Views/LinkLegalTermsView.swift
@@ -109,6 +109,11 @@ final class LinkLegalTermsView: UIView {
                     "By joining Link, you agree to the <terms>Terms</terms> and <privacy>Privacy Policy</privacy>.",
                     "Legal text shown when creating a Link account."
                 )
+            case .checkboxWithDefaultOptIn:
+                return STPLocalizedString(
+                    "By providing phone number and email, you agree to create a Link account subject to the Link <terms>Terms</terms> and <privacy>Privacy Policy</privacy>.",
+                    "Legal text shown when creating a Link account."
+                )
             case .textFieldsOnlyEmailFirst:
                 return STPLocalizedString(
                     "By providing your email, you agree to create a Link account and save your payment info to Link, according to the Link <terms>Terms</terms> and <privacy>Privacy Policy</privacy>.",
@@ -121,16 +126,44 @@ final class LinkLegalTermsView: UIView {
                 )
             }
         }()
+
+        let leadingIcon: NSTextAttachment? = {
+            guard mode == .checkboxWithDefaultOptIn else {
+                return nil
+            }
+            let iconImage = Image.link_logo_knockout.makeImage(template: false)
+            let iconImageAttachment = NSTextAttachment()
+
+            let font = LinkUI.font(forTextStyle: .caption)
+            let targetHeight = font.capHeight * 1.3
+            let aspectRatio = iconImage.size.width / iconImage.size.height
+            let targetWidth = targetHeight * aspectRatio
+
+            iconImageAttachment.bounds = CGRect(
+                x: 0,
+                y: (font.capHeight - targetHeight).rounded() / 2,
+                width: targetWidth,
+                height: targetHeight
+            )
+            iconImageAttachment.image = iconImage
+            return iconImageAttachment
+        }()
+
         let formattedString = STPStringUtils.applyLinksToString(template: string, links: links)
         let paragraphStyle = NSMutableParagraphStyle()
         paragraphStyle.lineSpacing = LinkUI.lineSpacing(
             fromRelativeHeight: Constants.lineHeight,
             textStyle: .caption
         )
-
         formattedString.addAttributes([.paragraphStyle: paragraphStyle], range: formattedString.extent)
 
-        return formattedString
+        let result = NSMutableAttributedString(string: "")
+        if let leadingIcon {
+            result.append(NSAttributedString(attachment: leadingIcon))
+            result.append(NSAttributedString(string: " • "))
+        }
+        result.append(formattedString)
+        return result
     }
 
 }
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+Link.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+Link.swift
index 54d2cd0d965a..46ad64aa2330 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+Link.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+Link.swift
@@ -128,5 +128,9 @@ extension PaymentSheet {
         /// Decides whether Link shows more actively in FlowController.
         /// Only enable this in the PaymentSheet playground.
         @_spi(STP) public static var enableLinkFlowControllerChanges: Bool = false
+
+        /// Decides whether Link default opt-in should be available.
+        /// Only enable this in the PaymentSheet playground.
+        @_spi(STP) public static var enableLinkDefaultOptIn: Bool = false
     }
 }
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+Card.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+Card.swift
index 9918ac0cea99..76cca4b79db6 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+Card.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+Card.swift
@@ -13,7 +13,10 @@ import StripePayments
 import UIKit
 
 extension PaymentSheetFormFactory {
-    func makeCard(cardBrandChoiceEligible: Bool = false) -> PaymentMethodElement {
+    func makeCard(
+        cardBrandChoiceEligible: Bool = false,
+        allowsLinkDefaultOptIn: Bool
+    ) -> PaymentMethodElement {
         let showLinkInlineSignup = showLinkInlineCardSignup
         let defaultCheckbox: Element? = {
             guard allowsSetAsDefaultPM else {
@@ -106,7 +109,8 @@ extension PaymentSheetFormFactory {
                 linkAccount: linkAccount,
                 country: countryCode,
                 showCheckbox: !shouldDisplaySaveCheckbox,
-                accountService: accountService
+                accountService: accountService,
+                allowsDefaultOptIn: allowsLinkDefaultOptIn
             )
             elements.append(inlineSignupElement)
         }
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory.swift
index 540c6a1abddf..83691bb7cc7a 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory.swift
@@ -38,6 +38,7 @@ class PaymentSheetFormFactory {
     let cardBrandChoiceEligible: Bool
     let savePaymentMethodConsentBehavior: SavePaymentMethodConsentBehavior
     let allowsSetAsDefaultPM: Bool
+    let allowsLinkDefaultOptIn: Bool
     let isFirstSavedPaymentMethod: Bool
     let analyticsHelper: PaymentSheetAnalyticsHelper?
     let paymentMethodIncentive: PaymentMethodIncentive?
@@ -127,6 +128,7 @@ class PaymentSheetFormFactory {
                   currency: intent.currency,
                   savePaymentMethodConsentBehavior: elementsSession.savePaymentMethodConsentBehavior,
                   allowsSetAsDefaultPM: elementsSession.paymentMethodSetAsDefaultForPaymentSheet,
+                  allowsLinkDefaultOptIn: elementsSession.allowsLinkDefaultOptIn,
                   isFirstSavedPaymentMethod: elementsSession.customer?.paymentMethods.isEmpty ?? true,
                   analyticsHelper: analyticsHelper,
                   paymentMethodIncentive: elementsSession.incentive)
@@ -147,6 +149,7 @@ class PaymentSheetFormFactory {
         currency: String? = nil,
         savePaymentMethodConsentBehavior: SavePaymentMethodConsentBehavior,
         allowsSetAsDefaultPM: Bool = false,
+        allowsLinkDefaultOptIn: Bool = false,
         isFirstSavedPaymentMethod: Bool = true,
         analyticsHelper: PaymentSheetAnalyticsHelper?,
         paymentMethodIncentive: PaymentMethodIncentive?
@@ -170,6 +173,7 @@ class PaymentSheetFormFactory {
         self.cardBrandChoiceEligible = cardBrandChoiceEligible
         self.savePaymentMethodConsentBehavior = savePaymentMethodConsentBehavior
         self.allowsSetAsDefaultPM = allowsSetAsDefaultPM
+        self.allowsLinkDefaultOptIn = allowsLinkDefaultOptIn
         self.isFirstSavedPaymentMethod = isFirstSavedPaymentMethod
         self.analyticsHelper = analyticsHelper
         self.paymentMethodIncentive = paymentMethodIncentive
@@ -188,7 +192,7 @@ class PaymentSheetFormFactory {
             // We have two ways to create the form for a payment method
             // 1. Custom, one-off forms
             if paymentMethod == .card {
-                return makeCard(cardBrandChoiceEligible: cardBrandChoiceEligible)
+                return makeCard(cardBrandChoiceEligible: cardBrandChoiceEligible, allowsLinkDefaultOptIn: allowsLinkDefaultOptIn)
             } else if paymentMethod == .USBankAccount {
                 return makeUSBankAccount(merchantName: configuration.merchantDisplayName)
             } else if paymentMethod == .UPI {
diff --git a/StripeUICore/StripeUICore/Source/Elements/Checkbox/CheckboxButton.swift b/StripeUICore/StripeUICore/Source/Elements/Checkbox/CheckboxButton.swift
index 08d57fbcf2fc..5c0d55defa27 100644
--- a/StripeUICore/StripeUICore/Source/Elements/Checkbox/CheckboxButton.swift
+++ b/StripeUICore/StripeUICore/Source/Elements/Checkbox/CheckboxButton.swift
@@ -111,10 +111,17 @@ import UIKit
         }
     }
 
+    private let alwaysEmphasizeText: Bool
+
     // MARK: - Initializers
 
-    public init(description: String? = nil, theme: ElementsAppearance = .default) {
+    public init(
+        description: String? = nil,
+        theme: ElementsAppearance = .default,
+        alwaysEmphasizeText: Bool = false
+    ) {
         self.theme = theme
+        self.alwaysEmphasizeText = alwaysEmphasizeText
         super.init(frame: .zero)
 
         isAccessibilityElement = true
@@ -130,8 +137,13 @@ import UIKit
         addGestureRecognizer(didTapGestureRecognizer)
     }
 
-    public convenience init(text: String, description: String? = nil, theme: ElementsAppearance = .default) {
-        self.init(description: description, theme: theme)
+    public convenience init(
+        text: String,
+        description: String? = nil,
+        theme: ElementsAppearance = .default,
+        alwaysEmphasizeText: Bool = false
+    ) {
+        self.init(description: description, theme: theme, alwaysEmphasizeText: alwaysEmphasizeText)
         setText(text)
     }
 
@@ -202,10 +214,11 @@ import UIKit
 
     private func updateLabels() {
         let hasDescription = descriptionLabel.text != nil
+        let emphasizeText = hasDescription || alwaysEmphasizeText
 
-        let textFont =  hasDescription ? emphasisFont : font
+        let textFont = emphasizeText ? emphasisFont : font
         textView.font = textFont
-        textView.textColor = hasDescription ? theme.colors.bodyText : theme.colors.secondaryText
+        textView.textColor = emphasizeText ? theme.colors.bodyText : theme.colors.secondaryText
 
         descriptionLabel.font = font
         descriptionLabel.isHidden = !hasDescription
diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testDefaultOptIn_fully_preFilled@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testDefaultOptIn_fully_preFilled@3x.png
new file mode 100644
index 000000000000..d2f8d44e34b4
Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testDefaultOptIn_fully_preFilled@3x.png differ
diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testDefaultOptIn_fully_preFilled_longEmail@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testDefaultOptIn_fully_preFilled_longEmail@3x.png
new file mode 100644
index 000000000000..1fc9d454aa91
Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testDefaultOptIn_fully_preFilled_longEmail@3x.png differ
diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testDefaultOptIn_partially_preFilled@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testDefaultOptIn_partially_preFilled@3x.png
new file mode 100644
index 000000000000..996df8e3f9f3
Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testDefaultOptIn_partially_preFilled@3x.png differ
diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testDefaultState_textFieldsOnly@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testDefaultState_textFieldsOnly@3x.png
index c0b5da20c794..0629de38e9e3 100644
Binary files a/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testDefaultState_textFieldsOnly@3x.png and b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testDefaultState_textFieldsOnly@3x.png differ
diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testExpandedState_nonUS_preFilled_textFieldsOnly@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testExpandedState_nonUS_preFilled_textFieldsOnly@3x.png
index 0333e27a4265..1e883b3b20d9 100644
Binary files a/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testExpandedState_nonUS_preFilled_textFieldsOnly@3x.png and b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testExpandedState_nonUS_preFilled_textFieldsOnly@3x.png differ
diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testExpandedState_nonUS_textFieldsOnly@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testExpandedState_nonUS_textFieldsOnly@3x.png
index 881bc5f6320c..b193912eace6 100644
Binary files a/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testExpandedState_nonUS_textFieldsOnly@3x.png and b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testExpandedState_nonUS_textFieldsOnly@3x.png differ
diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testExpandedState_textFieldsOnly@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testExpandedState_textFieldsOnly@3x.png
index e19227334c07..204a59eeadcd 100644
Binary files a/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testExpandedState_textFieldsOnly@3x.png and b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testExpandedState_textFieldsOnly@3x.png differ


Reviews:
user: login: mats-stripe
body: Mostly LGTM, but there's some visual bugs we should fix before merging!
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T20:04:32Z
user: login: tillh-stripe
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T21:28:00Z
user: login: tillh-stripe
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T21:28:07Z
user: login: tillh-stripe
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T21:28:14Z
user: login: mats-stripe
body: Layout changes LGTM - one small change needed before merging
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-30T13:31:59Z
user: login: mats-stripe
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-30T14:10:39Z

Review comments:
diff_hunk: @@ -0,0 +1,65 @@
+//
+//  LinkDefaultOptInView.swift
+//  StripePaymentSheet
+//
+//  Created by Till Hellmund on 5/26/25.
+//
+
+@_spi(STP) import StripeCore
+@_spi(STP) import StripeUICore
+import UIKit
+
+protocol LinkDefaultOptInViewDelegate: AnyObject {
+    func linkDefaultOptInViewDidSelectChange(_ view: LinkDefaultOptInView)
+}
+
+class LinkDefaultOptInView: UIView {
+    weak var delegate: LinkDefaultOptInViewDelegate?
+
+    init(
+        email: String,
+        phoneNumber: PhoneNumber,
+        theme: ElementsAppearance
+    ) {
+        super.init(frame: .zero)
+
+        let emailLabel = UILabel()
+        emailLabel.text = email
+        emailLabel.font = theme.fonts.footnote
+        emailLabel.textColor = theme.colors.bodyText
+
+        let phoneLabel = UILabel()
+        phoneLabel.text = phoneNumber.string(as: .national)
+        phoneLabel.font = theme.fonts.smallFootnote
+        phoneLabel.textColor = theme.colors.secondaryText
+
+        let contentStackView = UIStackView(arrangedSubviews: [emailLabel, phoneLabel])
+        contentStackView.axis = .vertical
+        contentStackView.isLayoutMarginsRelativeArrangement = true
+        contentStackView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+
+        let changeButton = UILabel()
path: StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkDefaultOptInView.swift
user: login: mats-stripe
body: Could this be a `UIButton`? I think this would work the same and be more idiomatic to UIKit:

```swift
let changeButton = UIButton(type: .system)
changeButton.translatesAutoresizingMaskIntoConstraints = false
changeButton.setTitle(.Localized.change, for: .normal)
changeButton.titleLabel?.font = theme.fonts.footnote
changeButton.setTitleColor(theme.colors.primary, for: .normal)
changeButton.addTarget(self, action: #selector(onChangeClick), for: .touchUpInside)
```
created_at: 2025-05-29T19:27:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,65 @@
+//
+//  LinkDefaultOptInView.swift
+//  StripePaymentSheet
+//
+//  Created by Till Hellmund on 5/26/25.
+//
+
+@_spi(STP) import StripeCore
+@_spi(STP) import StripeUICore
+import UIKit
+
+protocol LinkDefaultOptInViewDelegate: AnyObject {
+    func linkDefaultOptInViewDidSelectChange(_ view: LinkDefaultOptInView)
+}
+
+class LinkDefaultOptInView: UIView {
+    weak var delegate: LinkDefaultOptInViewDelegate?
+
+    init(
+        email: String,
+        phoneNumber: PhoneNumber,
+        theme: ElementsAppearance
+    ) {
+        super.init(frame: .zero)
+
+        let emailLabel = UILabel()
+        emailLabel.text = email
+        emailLabel.font = theme.fonts.footnote
+        emailLabel.textColor = theme.colors.bodyText
+
+        let phoneLabel = UILabel()
+        phoneLabel.text = phoneNumber.string(as: .national)
+        phoneLabel.font = theme.fonts.smallFootnote
+        phoneLabel.textColor = theme.colors.secondaryText
+
+        let contentStackView = UIStackView(arrangedSubviews: [emailLabel, phoneLabel])
+        contentStackView.axis = .vertical
+        contentStackView.isLayoutMarginsRelativeArrangement = true
+        contentStackView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+
+        let changeButton = UILabel()
path: StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkDefaultOptInView.swift
user: login: mats-stripe
body: I also noticed the `contentStackView` only takes up the necessary space it needs, and the change button fills up the rest:

<img width="555" alt="image" src="https://github.com/user-attachments/assets/be5c8968-33ee-41f8-8662-60be4fb0da0c" />

We should probably constrain the button's width and set the text alignment to trailing

created_at: 2025-05-29T19:29:07Z
author_association: COLLABORATOR
diff_hunk: @@ -42,26 +43,57 @@ extension LinkInlineSignupView {
             // Force the border to match the passed in borderColor
             appearanceCopy.colors.componentBorder = borderColor
 
-            let text = STPLocalizedString(
-                "Save your info for secure 1-click checkout with Link",
-                """
-                Label for a checkbox that when checked allows the payment information
-                to be saved and used in future checkout sessions.
-                """
+            let text = {
+                switch mode {
+                case .checkboxWithDefaultOptIn:
+                    return STPLocalizedString(
+                        "Save my info for faster checkout with Link",
+                        """
+                        Label for a checkbox that when checked allows the payment information
+                        to be saved and used in future checkout sessions.
+                        """
+                    )
+                case .checkbox, .textFieldsOnlyEmailFirst, .textFieldsOnlyPhoneFirst:
+                    return STPLocalizedString(
+                        "Save your info for secure 1-click checkout with Link",
+                        """
+                        Label for a checkbox that when checked allows the payment information
+                        to be saved and used in future checkout sessions.
+                        """
+                    )
+                }
+            }()
+
+            let description: String? = {
+                switch mode {
+                case .checkbox, .textFieldsOnlyEmailFirst, .textFieldsOnlyPhoneFirst:
+                    return String.Localized.pay_faster_at_$merchant_and_thousands_of_merchants(
path: StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView-CheckboxElement.swift
user: login: mats-stripe
body: Haven't seen a property with a `$` in it before 
created_at: 2025-05-29T19:37:00Z
author_association: COLLABORATOR
diff_hunk: @@ -265,6 +306,14 @@ extension LinkInlineSignupView: LinkInlineSignupViewModelDelegate {
 
 }
 
+extension LinkInlineSignupView: LinkDefaultOptInViewDelegate {
+
+    func linkDefaultOptInViewDidSelectChange(_ view: LinkDefaultOptInView) {
+        viewModel.didUncheckDefaultOptIn = true
path: StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView.swift
user: login: mats-stripe
body: This naming here is unexpected to me, as tapping `Change` seems like a different action than unchecking, right?
created_at: 2025-05-29T19:47:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,65 @@
+//
+//  LinkDefaultOptInView.swift
+//  StripePaymentSheet
+//
+//  Created by Till Hellmund on 5/26/25.
+//
+
+@_spi(STP) import StripeCore
+@_spi(STP) import StripeUICore
+import UIKit
+
+protocol LinkDefaultOptInViewDelegate: AnyObject {
+    func linkDefaultOptInViewDidSelectChange(_ view: LinkDefaultOptInView)
+}
+
+class LinkDefaultOptInView: UIView {
path: StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkDefaultOptInView.swift
user: login: mats-stripe
body: Mentioned over Slack but flagging here so it isn't lost; we should dim this view like the others while MPE is in a processing state: 

<img width=50% src="https://github.com/user-attachments/assets/278bc0b0-37e5-483f-afe4-78ff8f0e1bd3">

created_at: 2025-05-29T19:56:34Z
author_association: COLLABORATOR
path: Tests/ReferenceImages_64/StripeiOS_Tests.LinkInlineSignupElementSnapshotTests/testExpandedState_textFieldsOnly@3x.png
user: login: mats-stripe
body: Could we add a similar snapshot test for the default opt in view / footer? 🙏 
created_at: 2025-05-29T20:03:58Z
author_association: COLLABORATOR
diff_hunk: @@ -265,6 +306,14 @@ extension LinkInlineSignupView: LinkInlineSignupViewModelDelegate {
 
 }
 
+extension LinkInlineSignupView: LinkDefaultOptInViewDelegate {
+
+    func linkDefaultOptInViewDidSelectChange(_ view: LinkDefaultOptInView) {
+        viewModel.didUncheckDefaultOptIn = true
path: StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView.swift
user: login: tillh-stripe
body: Renamed!
created_at: 2025-05-29T21:28:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,65 @@
+//
+//  LinkDefaultOptInView.swift
+//  StripePaymentSheet
+//
+//  Created by Till Hellmund on 5/26/25.
+//
+
+@_spi(STP) import StripeCore
+@_spi(STP) import StripeUICore
+import UIKit
+
+protocol LinkDefaultOptInViewDelegate: AnyObject {
+    func linkDefaultOptInViewDidSelectChange(_ view: LinkDefaultOptInView)
+}
+
+class LinkDefaultOptInView: UIView {
+    weak var delegate: LinkDefaultOptInViewDelegate?
+
+    init(
+        email: String,
+        phoneNumber: PhoneNumber,
+        theme: ElementsAppearance
+    ) {
+        super.init(frame: .zero)
+
+        let emailLabel = UILabel()
+        emailLabel.text = email
+        emailLabel.font = theme.fonts.footnote
+        emailLabel.textColor = theme.colors.bodyText
+
+        let phoneLabel = UILabel()
+        phoneLabel.text = phoneNumber.string(as: .national)
+        phoneLabel.font = theme.fonts.smallFootnote
+        phoneLabel.textColor = theme.colors.secondaryText
+
+        let contentStackView = UIStackView(arrangedSubviews: [emailLabel, phoneLabel])
+        contentStackView.axis = .vertical
+        contentStackView.isLayoutMarginsRelativeArrangement = true
+        contentStackView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+
+        let changeButton = UILabel()
path: StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkDefaultOptInView.swift
user: login: tillh-stripe
body: Fixed!
created_at: 2025-05-29T21:28:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,65 @@
+//
+//  LinkDefaultOptInView.swift
+//  StripePaymentSheet
+//
+//  Created by Till Hellmund on 5/26/25.
+//
+
+@_spi(STP) import StripeCore
+@_spi(STP) import StripeUICore
+import UIKit
+
+protocol LinkDefaultOptInViewDelegate: AnyObject {
+    func linkDefaultOptInViewDidSelectChange(_ view: LinkDefaultOptInView)
+}
+
+class LinkDefaultOptInView: UIView {
path: StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkDefaultOptInView.swift
user: login: tillh-stripe
body: Fixed!
created_at: 2025-05-29T21:28:13Z
author_association: COLLABORATOR
diff_hunk: @@ -305,15 +353,17 @@ final class LinkInlineSignupViewModel {
             emailWasPrefilled = true
         }
         if showCheckbox {
-            self.mode = .checkbox
+            let allowsDefaultOptIn = PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn && allowsDefaultOptIn // && country == "US"
path: StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/ViewModels/LinkInlineSignupViewModel.swift
user: login: mats-stripe
body: 👀 
```suggestion
            let allowsDefaultOptIn = PaymentSheet.LinkFeatureFlags.enableLinkDefaultOptIn && allowsDefaultOptIn && country == "US"
```
created_at: 2025-05-30T13:21:11Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-28T15:12:38Z
body: ⚠️ Missing Translations
The following strings have been uploaded to Lokalise but are not yet translated.

By providing phone number and email, you agree to create a Link account subject to the Link <terms>Terms</terms> and <privacy>Privacy Policy</privacy>., Save my info for faster checkout with Link

If it's okay for these strings to be unlocalized in master (e.g. this is for an unshipped feature), add the label `ship without translations` to acknowledge that there are missing translations. Otherwise, wait until translations are available in Lokalise and re-run this job.

New strings are localized on a weekly basis and are downloaded as part of the release process. For more details on how to localize a string, you can refer to [this link](https://trailhead.corp.stripe.com/docs/mobile-sdk/shared-infrastructure/mobile-sdk-localizations).
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T15:26:38Z
body: 🚨 New dead code detected in this PR:

```diff
LinkDefaultOptInView.swift:13 warning: Parameter 'view' is unused
```

Please remove the dead code before merging.

If this is intentional, you can bypass this check by adding the label `skip dead code check` to this PR.

ℹ️ If this comment appears to be left in error, double check that the flagged code is actually used and/or make sure your branch is up-to-date with `master`.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-29T20:41:21Z
body: ⚠️ Public API changes detected:


### StripePaymentSheet
```diff
- public var formInsets: UIKit.UIEdgeInsets
```

If you are adding a new public API consider the following:
- Do these APIs need to be `public` or can they be protected with `@_spi(STP)`?
- If these APIs need to be `public`, assess whether they require an API review.

If you are modifying or removing a public API:
- Does this require a breaking version change?
- Do these changes require API review?

If you confirm these APIs need to be added/updated and have undergone necessary review, add the label `modifies public API` to this PR to acknowledge and bypass this check.

ℹ️ If this comment appears to be left in error, make sure your branch is up-to-date with `master`.
author_association: NONE