diff --git a/.github/workflows/find-dead-code.yml b/.github/workflows/find-dead-code.yml new file mode 100644 index 00000000000..56b03674df3 --- /dev/null +++ b/.github/workflows/find-dead-code.yml @@ -0,0 +1,110 @@ +name: Dead code detection +on: + pull_request: + types: [opened, labeled, unlabeled, synchronize] + paths: + - '**/*.swift' + +jobs: + dead-code-check: + runs-on: macos-13 + permissions: + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Install Periphery + run: brew install peripheryapp/periphery/periphery + + - name: Build project and run Periphery scan + id: periphery-scan + run: | + periphery scan --config .periphery.yml --clean-build 2>&1 | sed -E 's#.*/##; s/:[0-9]+:[0-9]+:/: /' | grep 'is unused' > periphery_report_feature_formatted_sorted.txt + ruby ci_scripts/dead_code/process_periphery_output.rb periphery_report_feature_formatted_sorted.txt unused_code_feature.json + + - name: Copy .periphery.yml to temporary location + run: | + # Copy necessary files to /tmp/ before checking out master + cp .periphery.yml /tmp/ + cp ci_scripts/dead_code/process_periphery_output.rb /tmp/ + + - name: Compare Periphery output with master baseline + run: | + git fetch origin master:master + git checkout master + cp /tmp/.periphery.yml .periphery.yml + mkdir -p ci_scripts/dead_code/ + cp /tmp/process_periphery_output.rb ci_scripts/dead_code/ + + periphery scan --config .periphery.yml --clean-build 2>&1 | sed -E 's#.*/##; s/:[0-9]+:[0-9]+:/: /' | grep 'is unused' > periphery_report_master_formatted_sorted.txt + ruby ci_scripts/dead_code/process_periphery_output.rb periphery_report_master_formatted_sorted.txt unused_code_master.json + + - name: Compare Unused Code JSON Files + id: compare-dead-code + run: | + # Compare the keys in the JSON files to find new dead code + ruby -r json -e ' + master_file = "unused_code_master.json" + feature_file = "unused_code_feature.json" + output_file = "new_dead_code.json" + + master_unused_code = JSON.parse(File.read(master_file)) + feature_unused_code = JSON.parse(File.read(feature_file)) + + + new_dead_code = feature_unused_code.reject { |k, _| master_unused_code.key?(k) } + + if new_dead_code.size > 200 + puts "More than 200 keys present, skipping. This usually happens if a build fails" + elsif new_dead_code.empty? + puts "No new dead code detected." + else + File.write(output_file, JSON.pretty_generate(new_dead_code) + "\n") + end + ' + + # Check if new_dead_code.json exists and is not empty + if [ -s new_dead_code.json ]; then + echo "New dead code detected." + echo "diff<> $GITHUB_ENV + cat new_dead_code.json >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + else + echo "No new dead code detected." + fi + + - uses: peter-evans/find-comment@v3 + id: find_comment + with: + issue-number: ${{ github.event.pull_request.number }} + body-includes: '🚨 New dead code detected' + + - uses: peter-evans/create-or-update-comment@v3 + id: create_update_comment + if: env.diff != '' + with: + issue-number: ${{ github.event.pull_request.number }} + body: | + 🚨 New dead code detected in this PR: + + ```diff + ${{ env.diff }} + ``` + + 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, make sure your branch is up-to-date with `master`. + + edit-mode: replace + comment-id: ${{ steps.find_comment.outputs.comment-id }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Fail if not acknowledged + if: env.diff != '' && !contains(github.event.pull_request.labels.*.name, 'skip dead code check') + run: exit 1 diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 00000000000..0fd07506584 --- /dev/null +++ b/.periphery.yml @@ -0,0 +1,106 @@ +# Detects dead code +# Usage periphery scan --config .periphery.yml 2>&1 | sed 's#.*/##' | grep 'is unused' | sort > periphery_report_feature_formatted_sorted.txt +# Install/info https://github.com/peripheryapp/periphery +# Why are so many targets/schemes commented out? This script takes a long time to run in CI, so we skip some schemes to speed it up. Note: AllStripeFrameworks covers a lot of the commented out schemes, e.g. AllStripeFrameworks contains StripeApplePay. + +workspace: Stripe.xcworkspace + +schemes: + - AllStripeFrameworks + # - IntegrationTester + # - Stripe3DS2 + # - Stripe3DS2DemoUI + # - StripeApplePay + # - StripeCameraCore + # - StripeCardScan + - StripeConnect + # - StripeCore + # - StripeFinancialConnections + # - StripeIdentity + # - StripePaymentSheet + # - StripePayments + # - StripePaymentsUI + # - StripeUICore + # - StripeiOS + # - PaymentSheet Example + # - AppClipExample + # - CardImageVerification Example + # - FinancialConnections Example + # - IdentityVerification Example + # - StripeConnect Example + # - Non-Card Payment Examples + # - UI Examples + +targets: + # - AppClipExample + # - AppClipExampleClip + # - AppClipExampleClipTests + # - AppClipExampleClipUITests + # - AppClipExampleTests iOS + # - CardImageVerification Example + # - CardImageVerification ExampleUITests + # - Common + # - FinancialConnections Example + # - FinancialConnectionsUITests + # - IdentityVerification Example + # - IntegrationTester + # - IntegrationTesterUITests + # - Non-Card Payment Examples + # - PaymentSheet Example + # - PaymentSheetLocalizationScreenshotGenerator + # - PaymentSheetUITest + - Stripe3DS2 + - Stripe3DS2Tests + - StripeApplePay + - StripeApplePayTests + - StripeCameraCore + - StripeCameraCoreTestUtils + - StripeCameraCoreTests + - StripeCardScan + - StripeCardScanTests + - StripeConnect + # - StripeConnect Example + # - StripeConnect ExampleUITests + - StripeConnectTests + - StripeCore + - StripeCoreTestUtils + - StripeCoreTests + - StripeFinancialConnections + - StripeFinancialConnectionsTests + - StripeIdentity + - StripeIdentityTests + - StripePaymentSheet + - StripePaymentSheetTestHostApp + - StripePaymentSheetTests + - StripePayments + - StripePaymentsObjcTestUtils + - StripePaymentsTestHostApp + - StripePaymentsTestUtils + - StripePaymentsTests + - StripePaymentsUI + - StripePaymentsUITests + - StripeUICore + - StripeUICoreTests + - StripeiOS + - StripeiOSAppHostedTests + - StripeiOSTestHostApp + - StripeiOSTests + # - UI Examples + +retain_public: true +retain_objc_accessible: true +retain_objc_annotated: true +retain_objc_protocols: true + +retain_ibaction: true +retain_iboutlet: true +retain_ibinspectable: true + +analyze_tests: true + +verbose: true + +build_arguments: + - -destination + - 'generic/platform=iOS Simulator' + \ No newline at end of file diff --git a/Example/PaymentSheet Example/PaymentSheet Example.xcodeproj/project.pbxproj b/Example/PaymentSheet Example/PaymentSheet Example.xcodeproj/project.pbxproj index 75965d92db8..4ed23d11305 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example.xcodeproj/project.pbxproj +++ b/Example/PaymentSheet Example/PaymentSheet Example.xcodeproj/project.pbxproj @@ -51,6 +51,7 @@ A262D76C3319002A2F6EE395 /* CustomerSheetTestPlaygroundSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2A8BAE3842A0E892D06BB9 /* CustomerSheetTestPlaygroundSettings.swift */; }; AEC3BC636297A0D3E2DDF522 /* MockFiles in Resources */ = {isa = PBXBuildFile; fileRef = 9496755DA1916325D38ECB2F /* MockFiles */; }; B36A24145C97D73C981DDBAC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F4B26C754925F8D2A5183B2E /* Main.storyboard */; }; + B615E86F2CA4B267007D684C /* ExampleEmbeddedElementCheckoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B615E86E2CA4B266007D684C /* ExampleEmbeddedElementCheckoutViewController.swift */; }; B641A4192C2BA25D00AE654A /* PaymentSheetVerticalUITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B641A4182C2BA25D00AE654A /* PaymentSheetVerticalUITest.swift */; }; B6CA975C2C486DE700DAE441 /* PaymentSheetLPMUITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CA975B2C486DE700DAE441 /* PaymentSheetLPMUITest.swift */; }; B6D6AAA666859847BB59749C /* PaymentSheet+AddressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61C5739DBE6405A4D9FD5F2 /* PaymentSheet+AddressTests.swift */; }; @@ -244,6 +245,7 @@ ADBEC0CE822B92C078E5D758 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; B33D2066D7BA7984CABC23A5 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/LaunchScreen.strings"; sourceTree = ""; }; B54E58F2CA450CF49ECD5637 /* CustomerSheetTestPlaygroundController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerSheetTestPlaygroundController.swift; sourceTree = ""; }; + B615E86E2CA4B266007D684C /* ExampleEmbeddedElementCheckoutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleEmbeddedElementCheckoutViewController.swift; sourceTree = ""; }; B641A4182C2BA25D00AE654A /* PaymentSheetVerticalUITest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetVerticalUITest.swift; sourceTree = ""; }; B69C155A2B9FDCBD009CE667 /* PaymentSheet Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = "PaymentSheet Example.entitlements"; path = "PaymentSheet Example/PaymentSheet Example.entitlements"; sourceTree = ""; }; B6CA975B2C486DE700DAE441 /* PaymentSheetLPMUITest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetLPMUITest.swift; sourceTree = ""; }; @@ -448,6 +450,7 @@ B7AFD32B5EAD3BEEEC3D4260 /* ExampleSwiftUIPaymentSheet.swift */, D20765908089ACB79B556592 /* ExampleSwiftUIViews.swift */, 61FB6BC62C88C8BF00F8E074 /* EmbeddedPlaygroundViewController.swift */, + B615E86E2CA4B266007D684C /* ExampleEmbeddedElementCheckoutViewController.swift */, ADBEC0CE822B92C078E5D758 /* Info.plist */, 27BA81F620FAB6E5505E0B32 /* PaymentSheetTestPlayground.swift */, 1261A6BDCDB7E81D08F137F0 /* PaymentSheetTestPlaygroundSettings.swift */, @@ -672,6 +675,7 @@ buildActionMask = 2147483647; files = ( EB1DCD930408180734A8D7CA /* AppDelegate.swift in Sources */, + B615E86F2CA4B267007D684C /* ExampleEmbeddedElementCheckoutViewController.swift in Sources */, 4694B03B08B7DA9706A2ED9D /* AppearancePlaygroundView.swift in Sources */, DDF30DE9D7AA4BCC47CC12FB /* CustomerSheetTestPlayground.swift in Sources */, 8D9AAFD1D1D49112A7777414 /* CustomerSheetTestPlaygroundController.swift in Sources */, diff --git a/Example/PaymentSheet Example/PaymentSheet Example/AppearancePlaygroundView.swift b/Example/PaymentSheet Example/PaymentSheet Example/AppearancePlaygroundView.swift index 1b740357c9e..e3b97a964a5 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/AppearancePlaygroundView.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/AppearancePlaygroundView.swift @@ -6,7 +6,7 @@ // Copyright © 2022 stripe-ios. All rights reserved. // -@_spi(STP) import StripePaymentSheet +@_spi(EmbeddedPaymentElementPrivateBeta) import StripePaymentSheet import SwiftUI @available(iOS 14.0, *) @@ -40,6 +40,11 @@ struct AppearancePlaygroundView: View { set: { self.appearance.colors.componentBorder = UIColor($0) } ) + let selectedComponentBorderColorBinding = Binding( + get: { Color(self.appearance.colors.selectedComponentBorder ?? self.appearance.colors.primary) }, + set: { self.appearance.colors.selectedComponentBorder = UIColor($0) } + ) + let componentDividerColorBinding = Binding( get: { Color(self.appearance.colors.componentDivider) }, set: { self.appearance.colors.componentDivider = UIColor($0) } @@ -84,6 +89,11 @@ struct AppearancePlaygroundView: View { get: { self.appearance.borderWidth }, set: { self.appearance.borderWidth = $0 } ) + + let selectedBorderWidthBinding = Binding( + get: { appearance.selectedBorderWidth ?? appearance.borderWidth * 1.5 }, + set: { self.appearance.selectedBorderWidth = $0 } + ) let componentShadowColorBinding = Binding( get: { Color(self.appearance.shadow.color) }, @@ -212,6 +222,47 @@ struct AppearancePlaygroundView: View { } ) + let embeddedPaymentElementFlatSeparatorColorBinding = Binding( + get: { Color(self.appearance.embeddedPaymentElement.row.flat.separatorColor ?? appearance.colors.componentBorder) }, + set: { + if self.appearance.embeddedPaymentElement.row.flat.separatorColor == nil { + self.appearance.embeddedPaymentElement.row.flat.separatorColor = appearance.colors.componentBorder + } else { + self.appearance.embeddedPaymentElement.row.flat.separatorColor = UIColor($0) + } + } + ) + + let embeddedPaymentElementFlatRadioColorSelected = Binding( + get: { Color(self.appearance.embeddedPaymentElement.row.flat.radio.selectedColor ?? appearance.colors.primary) }, + set: { + self.appearance.embeddedPaymentElement.row.flat.radio.selectedColor = UIColor($0) + } + ) + + let embeddedPaymentElementFlatRadioColorUnselected = Binding( + get: { Color(self.appearance.embeddedPaymentElement.row.flat.radio.unselectedColor ?? appearance.colors.componentBorder) }, + set: { + self.appearance.embeddedPaymentElement.row.flat.radio.unselectedColor = UIColor($0) + } + ) + + let embeddedPaymentElementFlatLeftSeparatorInset = Binding( + get: { self.appearance.embeddedPaymentElement.row.flat.separatorInsets?.left ?? 0 }, + set: { + let prevInsets = self.appearance.embeddedPaymentElement.row.flat.separatorInsets ?? .zero + self.appearance.embeddedPaymentElement.row.flat.separatorInsets = UIEdgeInsets(top: 0, left: $0, bottom: 0, right: prevInsets.right) + } + ) + + let embeddedPaymentElementFlatRightSeparatorInset = Binding( + get: { self.appearance.embeddedPaymentElement.row.flat.separatorInsets?.right ?? 0 }, + set: { + let prevInsets = self.appearance.embeddedPaymentElement.row.flat.separatorInsets ?? .zero + self.appearance.embeddedPaymentElement.row.flat.separatorInsets = UIEdgeInsets(top: 0, left: prevInsets.left, bottom: 0, right: $0) + } + ) + let regularFonts = ["AvenirNext-Regular", "PingFangHK-Regular", "ChalkboardSE-Light"] NavigationView { @@ -222,6 +273,7 @@ struct AppearancePlaygroundView: View { ColorPicker("background", selection: backgroundColorBinding) ColorPicker("componentBackground", selection: componentBackgroundColorBinding) ColorPicker("componentBorder", selection: componentBorderColorBinding) + ColorPicker("selectedComponentBorder", selection: selectedComponentBorderColorBinding) ColorPicker("componentDivider", selection: componentDividerColorBinding) ColorPicker("text", selection: textColorBinding) ColorPicker("textSecondary", selection: textSecondaryColorBinding) @@ -237,6 +289,7 @@ struct AppearancePlaygroundView: View { Section(header: Text("Miscellaneous")) { Stepper(String(format: "cornerRadius: %.1f", appearance.cornerRadius), value: cornerRadiusBinding, in: 0...30) Stepper(String(format: "borderWidth: %.1f", appearance.borderWidth), value: borderWidthBinding, in: 0.0...2.0, step: 0.5) + Stepper(String(format: "selectedBorderWidth: %.1f", appearance.selectedBorderWidth ?? appearance.borderWidth * 1.5), value: selectedBorderWidthBinding, in: 0.0...2.0, step: 0.5) VStack { Text("componentShadow") ColorPicker("color", selection: componentShadowColorBinding) @@ -310,6 +363,56 @@ struct AppearancePlaygroundView: View { } } + Section(header: Text("EmbeddedPaymentElement")) { + DisclosureGroup { + Picker("Style", selection: $appearance.embeddedPaymentElement.style) { + ForEach(PaymentSheet.Appearance.EmbeddedPaymentElement.Style.allCases, id: \.self) { + Text(String(describing: $0)) + } + } + + DisclosureGroup { + Stepper("additionalInsets: \(Int(appearance.embeddedPaymentElement.row.additionalInsets))", + value: $appearance.embeddedPaymentElement.row.additionalInsets, in: 0...40) + + DisclosureGroup { + Stepper("separatorThickness: \(Int(appearance.embeddedPaymentElement.row.flat.separatorThickness))", + value: $appearance.embeddedPaymentElement.row.flat.separatorThickness, in: 0...10) + ColorPicker("separatorColor", selection: embeddedPaymentElementFlatSeparatorColorBinding) + Stepper("leftSeparatorInset: \(Int(appearance.embeddedPaymentElement.row.flat.separatorInsets?.left ?? 0))", + value: embeddedPaymentElementFlatLeftSeparatorInset, in: 0...20) + Stepper("rightSeparatorInset: \(Int(appearance.embeddedPaymentElement.row.flat.separatorInsets?.right ?? 0))", + value: embeddedPaymentElementFlatRightSeparatorInset, in: 0...20) + Toggle("topSeparatorEnabled", isOn: $appearance.embeddedPaymentElement.row.flat.topSeparatorEnabled) + Toggle("bottomSeparatorEnabled", isOn: $appearance.embeddedPaymentElement.row.flat.bottomSeparatorEnabled) + + DisclosureGroup { + ColorPicker("selectedColor", selection: embeddedPaymentElementFlatRadioColorSelected) + ColorPicker("unselectedColor", selection: embeddedPaymentElementFlatRadioColorUnselected) + } label: { + Text("Radio") + } + + } label: { + Text("FlatWithRadio") + } + + DisclosureGroup { + Stepper("Spacing: \(Int(appearance.embeddedPaymentElement.row.floating.spacing))", + value: $appearance.embeddedPaymentElement.row.floating.spacing, in: 0...40) + + } label: { + Text("FloatingButton") + } + } label: { + Text("Row") + } + + } label: { + Text("EmbeddedPaymentElement") + } + } + Button { appearance = PaymentSheet.Appearance() doneAction(appearance) diff --git a/Example/PaymentSheet Example/PaymentSheet Example/ExampleEmbeddedElementCheckoutViewController.swift b/Example/PaymentSheet Example/PaymentSheet Example/ExampleEmbeddedElementCheckoutViewController.swift new file mode 100644 index 00000000000..6e4e5eb2b51 --- /dev/null +++ b/Example/PaymentSheet Example/PaymentSheet Example/ExampleEmbeddedElementCheckoutViewController.swift @@ -0,0 +1,391 @@ +// +// ExampleEmbeddedElementCheckoutViewController.swift +// PaymentSheet Example +// +// Created by Yuki Tokuhiro on 9/25/24. +// + +@_spi(EmbeddedPaymentElementPrivateBeta) import StripePaymentSheet +import UIKit + +// View the backend code here: https://glitch.com/edit/#!/stripe-mobile-payment-sheet-custom-deferred +private let baseUrl = "https://stripe-mobile-payment-sheet-custom-deferred.glitch.me" + +class ExampleEmbeddedElementCheckoutViewController: UIViewController { + @IBOutlet weak var buyButton: UIButton! + @IBOutlet weak var paymentMethodButton: UIButton! + @IBOutlet weak var paymentMethodImage: UIImageView! + + @IBOutlet weak var hotDogQuantityLabel: UILabel! + @IBOutlet weak var saladQuantityLabel: UILabel! + @IBOutlet weak var hotDogStepper: UIStepper! + @IBOutlet weak var saladStepper: UIStepper! + @IBOutlet weak var subtotalLabel: UILabel! + @IBOutlet weak var salesTaxLabel: UILabel! + @IBOutlet weak var totalLabel: UILabel! + @IBOutlet weak var subscribeSwitch: UISwitch! + + var embeddedPaymentElement: EmbeddedPaymentElement! + + private let backendCheckoutUrl = URL(string: baseUrl + "/checkout")! + private let confirmIntentUrl = URL(string: baseUrl + "/confirm_intent")! + private let computeTotalsUrl = URL(string: baseUrl + "/compute_totals")! + + private struct ComputedTotals: Decodable { + let subtotal: Double + let tax: Double + let total: Double + } + + private var computedTotals: ComputedTotals! + + // MARK: - Create an IntentConfiguration + private var intentConfig: PaymentSheet.IntentConfiguration { + return .init(mode: .payment(amount: Int(computedTotals.total), + currency: "USD", + setupFutureUsage: subscribeSwitch.isOn ? .offSession : nil) + ) { [weak self] paymentMethod, shouldSavePaymentMethod, intentCreationCallback in + Task { + do { + // Create and confirm an intent on your server and invoke `intentCreationCallback` with the client secret or an error. + // TODO(yuki): Show client-side confirm, not server-side confirm. + guard let self else { + intentCreationCallback(.failure(ExampleError())) + return + } + let clientSecret = try await self.confirmIntent(paymentMethodID: paymentMethod.stripeId, shouldSavePaymentMethod: shouldSavePaymentMethod) + intentCreationCallback(.success(clientSecret)) + } catch { + intentCreationCallback(.failure(error)) + } + } + } + } + + private var currencyFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.maximumFractionDigits = 2 + return formatter + } + + override func viewDidLoad() { + super.viewDidLoad() + + buyButton.addTarget(self, action: #selector(didTapCheckoutButton), for: .touchUpInside) + buyButton.isEnabled = false + + paymentMethodButton.addTarget(self, action: #selector(didTapPaymentMethodButton), for: .touchUpInside) + paymentMethodButton.isEnabled = false + + hotDogStepper.isEnabled = false + saladStepper.isEnabled = false + subscribeSwitch.isEnabled = false + + Task { + await self.loadCheckout() + } + } + + @objc + func didTapPaymentMethodButton() { + let paymentMethodsViewController = PaymentMethodsViewController(embeddedPaymentElement: embeddedPaymentElement) + let navController = UINavigationController(rootViewController: paymentMethodsViewController) + present(navController, animated: true) + } + + @objc + func didTapCheckoutButton() async { + // MARK: - Confirm the payment + let result = await embeddedPaymentElement.confirm() + handlePaymentResult(result) + } + + @IBAction func hotDogStepperDidChange() { + updateUI() + } + + @IBAction func saladStepperDidChange() { + updateUI() + } + + @IBAction func subscribeSwitchDidChange() { + updateUI() + } + + private func updateUI() { + // Disable buy and payment method buttons while we're updating + buyButton.isEnabled = false + paymentMethodButton.isEnabled = false + + // Update the payment details + fetchTotals { [weak self] in + guard let self = self else { return } + self.updateLabels() + + Task { + // MARK: - Update payment details + // Update Embedded Payment Element with the latest `intentConfig` + let updateResult = await self.embeddedPaymentElement.update(intentConfiguration: self.intentConfig) + Task.detached { @MainActor [weak self] in + guard let self else { return } + paymentMethodButton.isEnabled = true + switch updateResult { + case .canceled: + // Do nothing; this happens when a subsequent `update` call cancels this one + break + case .failed(error: let error): + // Display error to user in an alert, let them retry + let alertController = UIAlertController(title: "Error", message: "\(error)", preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "Retry", style: .default) { _ in + self.updateUI() + }) + alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in }) + present(alertController, animated: true, completion: nil) + case .succeeded: + self.updateButtons() + } + } + } + } + } + + func updateButtons() { + // MARK: Update the payment method and buy buttons using `paymentOption` + if let paymentOption = embeddedPaymentElement.paymentOption { + paymentMethodButton.setTitle(paymentOption.label, for: .normal) + paymentMethodButton.setTitleColor(.black, for: .normal) + paymentMethodImage.image = paymentOption.image + buyButton.isEnabled = true + } else { + paymentMethodButton.setTitle("Select", for: .normal) + paymentMethodButton.setTitleColor(.systemBlue, for: .normal) + paymentMethodImage.image = nil + buyButton.isEnabled = false + } + } + + func updateLabels() { + hotDogQuantityLabel.text = "\(Int(hotDogStepper.value))" + saladQuantityLabel.text = "\(Int(saladStepper.value))" + + subtotalLabel.text = "\(currencyFormatter.string(from: NSNumber(value: computedTotals.subtotal / 100)) ?? "")" + salesTaxLabel.text = "\(currencyFormatter.string(from: NSNumber(value: computedTotals.tax / 100)) ?? "")" + totalLabel.text = "\(currencyFormatter.string(from: NSNumber(value: computedTotals.total / 100)) ?? "")" + } + + func displayAlert(_ message: String, shouldDismiss: Bool) { + let alertController = UIAlertController(title: "", message: message, preferredStyle: .alert) + let OKAction = UIAlertAction(title: "OK", style: .default) { (_) in + alertController.dismiss(animated: true) { + if shouldDismiss { + self.navigationController?.popViewController(animated: true) + } + } + } + alertController.addAction(OKAction) + present(alertController, animated: true, completion: nil) + } + + private func fetchTotals(completion: @escaping () -> Void) { + var request = URLRequest(url: computeTotalsUrl) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-type") + + let body: [String: Any?] = [ + "hot_dog_count": hotDogStepper.value, + "salad_count": saladStepper.value, + "is_subscribing": subscribeSwitch.isOn, + ] + + request.httpBody = try! JSONSerialization.data(withJSONObject: body, options: []) + + let task = URLSession.shared.dataTask( + with: request, + completionHandler: { [weak self] (data, _, _) in + guard let data = data, + let totals = try? JSONDecoder().decode(ComputedTotals.self, from: data) else { + fatalError("Failed to decode compute_totals response") + } + + self?.computedTotals = totals + DispatchQueue.main.async { + completion() + } + }) + + task.resume() + } + + private func loadCheckout() async { + var request = URLRequest(url: backendCheckoutUrl) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-type") + + let body: [String: Any?] = [ + "hot_dog_count": hotDogStepper.value, + "salad_count": saladStepper.value, + "is_subscribing": subscribeSwitch.isOn, + ] + + do { + request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) + weak var weakSelf = self + let (data, _) = try await URLSession.shared.data(for: request) + guard + let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let customerId = json["customer"] as? String, + let customerEphemeralKeySecret = json["ephemeralKey"] as? String, + let publishableKey = json["publishableKey"] as? String, + let subtotal = json["subtotal"] as? Double, + let tax = json["tax"] as? Double, + let total = json["total"] as? Double, + let self = weakSelf + else { + weakSelf?.displayAlert("Bad network response", shouldDismiss: true) + return + } + self.computedTotals = ComputedTotals(subtotal: subtotal, tax: tax, total: total) + + // MARK: - Create a EmbeddedPaymentElement instance + var configuration = EmbeddedPaymentElement.Configuration( + formSheetAction: .confirm(completion: { [weak self] result in + self?.handlePaymentResult(result) + }) + ) + configuration.merchantDisplayName = "Example, Inc." + // Set your Stripe publishable key - this allows the SDK to make requests to Stripe for your account + configuration.apiClient.publishableKey = publishableKey + configuration.applePay = .init( + merchantId: "merchant.com.stripe.umbrella.test", // Be sure to use your own merchant ID here! + merchantCountryCode: "US" + ) + configuration.customer = .init( + id: customerId, ephemeralKeySecret: customerEphemeralKeySecret) + configuration.returnURL = "payments-example://stripe-redirect" + // Set allowsDelayedPaymentMethods to true if your business can handle payment methods that complete payment after a delay, like SEPA Debit and Sofort. + configuration.allowsDelayedPaymentMethods = true + let embeddedPaymentElement = try await EmbeddedPaymentElement.create( + intentConfiguration: self.intentConfig, + configuration: configuration + ) + self.embeddedPaymentElement = embeddedPaymentElement + self.paymentMethodButton.isEnabled = true + self.hotDogStepper.isEnabled = true + self.saladStepper.isEnabled = true + self.subscribeSwitch.isEnabled = true + self.updateButtons() + self.updateLabels() + } catch { + // Handle error here + self.displayAlert("Error: \(error)", shouldDismiss: true) + } + } + + // MARK: - Handle payment result + func handlePaymentResult(_ result: EmbeddedPaymentElementResult) { + switch result { + case .completed: + displayAlert("Your order is confirmed!", shouldDismiss: true) + case .canceled: + print("Canceled!") + case .failed(let error): + print(error) + displayAlert("Payment failed: \n\(error)", shouldDismiss: false) + } + } + + func confirmIntent(paymentMethodID: String, shouldSavePaymentMethod: Bool) async throws -> String { + var request = URLRequest(url: confirmIntentUrl) + request.httpMethod = "POST" + + let body: [String: Any?] = [ + "payment_method_id": paymentMethodID, + "currency": "USD", + "hot_dog_count": hotDogStepper.value, + "salad_count": saladStepper.value, + "is_subscribing": subscribeSwitch.isOn, + "should_save_payment_method": shouldSavePaymentMethod, + "return_url": "payments-example://stripe-redirect", + "customer_id": embeddedPaymentElement.configuration.customer?.id, + ] + + request.httpBody = try! JSONSerialization.data(withJSONObject: body, options: []) + request.setValue("application/json", forHTTPHeaderField: "Content-type") + + let (data, _) = try await URLSession.shared.data(for: request) + let json = try JSONDecoder().decode([String: String].self, from: data) + guard + let clientSecret = json["intentClientSecret"] + else { + throw ExampleError(errorDescription: json["error"] ?? "An unknown error occurred.") + } + return clientSecret + } + + struct ExampleError: LocalizedError { + var errorDescription: String? + } +} + +private class PaymentMethodsViewController: UIViewController { + let embeddedPaymentElement: EmbeddedPaymentElement + lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + return UIScrollView() + }() + + init(embeddedPaymentElement: EmbeddedPaymentElement) { + self.embeddedPaymentElement = embeddedPaymentElement + super.init(nibName: nil, bundle: nil) + // MARK: - Set Embedded Payment Element properties + self.embeddedPaymentElement.presentingViewController = self + self.embeddedPaymentElement.delegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .white + view.addSubview(scrollView) + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + + embeddedPaymentElement.view.translatesAutoresizingMaskIntoConstraints = false + let embeddedPaymentElementView = embeddedPaymentElement.view + scrollView.addSubview(embeddedPaymentElementView) + NSLayoutConstraint.activate([ + scrollView.contentLayoutGuide.topAnchor.constraint(equalTo: embeddedPaymentElementView.topAnchor), + scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: embeddedPaymentElementView.bottomAnchor), + scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: embeddedPaymentElementView.leadingAnchor), + scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: embeddedPaymentElementView.trailingAnchor), + scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: embeddedPaymentElementView.leadingAnchor), + scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: embeddedPaymentElementView.trailingAnchor), + ]) + + // Nav bar + title = "Choose your payment method" + let closeButton = UIBarButtonItem(title: "Close", style: .plain, target: self, action: #selector(closeButtonTapped)) + navigationItem.leftBarButtonItem = closeButton + } + + @objc private func closeButtonTapped() { + dismiss(animated: true, completion: nil) + } +} + +// MARK: - EmbeddedPaymentElementDelegate +extension PaymentMethodsViewController: EmbeddedPaymentElementDelegate { + func embeddedPaymentElementDidUpdateHeight(embeddedPaymentElement: EmbeddedPaymentElement) { + // Lay out the scroll view that contains the Embedded Payment Element view + scrollView.setNeedsLayout() + scrollView.layoutIfNeeded() + } +} diff --git a/Example/PaymentSheet Example/PaymentSheet Example/Resources/Base.lproj/Main.storyboard b/Example/PaymentSheet Example/PaymentSheet Example/Resources/Base.lproj/Main.storyboard index e73093deda3..a0e6ee1712f 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/Resources/Base.lproj/Main.storyboard +++ b/Example/PaymentSheet Example/PaymentSheet Example/Resources/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -93,7 +93,7 @@ + @@ -1928,6 +1936,397 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElement.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElement.swift index c7874f38df4..6c09ac82caf 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElement.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElement.swift @@ -20,6 +20,9 @@ public class EmbeddedPaymentElement { /// A view controller to present on. public var presentingViewController: UIViewController? + /// This contains the `configuration` you passed in to `create`. + public let configuration: Configuration + /// See `EmbeddedPaymentElementDelegate`. public weak var delegate: EmbeddedPaymentElementDelegate? @@ -88,8 +91,8 @@ public class EmbeddedPaymentElement { } /// The result of an `update` call - public enum UpdateResult { - /// The update succeeded + @frozen public enum UpdateResult { + /// The update succeded case succeeded /// The update was canceled. This is only returned when a subsequent `update` call cancels previous ones. case canceled @@ -106,7 +109,7 @@ public class EmbeddedPaymentElement { intentConfiguration: IntentConfiguration ) async -> UpdateResult { // TODO(https://jira.corp.stripe.com/browse/MOBILESDK-2524) - return .canceled + return .succeeded } /// Completes the payment or setup. @@ -119,9 +122,10 @@ public class EmbeddedPaymentElement { // MARK: - Internal - private init(view: UIView, delegate: EmbeddedPaymentElementDelegate? = nil) { + private init(view: UIView, configuration: Configuration, delegate: EmbeddedPaymentElementDelegate? = nil) { self.view = view self.delegate = delegate + self.configuration = configuration } } @@ -189,7 +193,7 @@ extension EmbeddedPaymentElement { // MARK: - Typealiases -@_spi(STP) public typealias EmbeddedPaymentElementResult = PaymentSheetResult +@_spi(EmbeddedPaymentElementPrivateBeta) public typealias EmbeddedPaymentElementResult = PaymentSheetResult extension EmbeddedPaymentElement { public typealias IntentConfiguration = PaymentSheet.IntentConfiguration public typealias UserInterfaceStyle = PaymentSheet.UserInterfaceStyle diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElementDelegate.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElementDelegate.swift index 33ef5d7d469..731ae6f5370 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElementDelegate.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElementDelegate.swift @@ -19,7 +19,7 @@ public protocol EmbeddedPaymentElementDelegate: AnyObject { func embeddedPaymentElementDidUpdatePaymentOption(embeddedPaymentElement: EmbeddedPaymentElement) } -extension EmbeddedPaymentElementDelegate { +public extension EmbeddedPaymentElementDelegate { func embeddedPaymentElementWillPresent(embeddedPaymentElement: EmbeddedPaymentElement) { // Default implementation does nothing } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentMethodsView.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentMethodsView.swift index 0a37f60bc82..239d84b89f3 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentMethodsView.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentMethodsView.swift @@ -17,7 +17,7 @@ import UIKit lazy var stackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical - stackView.spacing = appearance.paymentOptionView.style == .floating ? appearance.paymentOptionView.paymentMethodRow.floating.spacing : 0 + stackView.spacing = appearance.embeddedPaymentElement.style == .floatingButton ? appearance.embeddedPaymentElement.row.floating.spacing : 0 return stackView }() @@ -29,7 +29,7 @@ import UIKit savedPaymentMethodAccessoryType: RowButton.RightAccessoryButton.AccessoryType?) { self.appearance = appearance super.init(frame: .zero) - let rowButtonAppearance = appearance.paymentOptionView.style.appearanceForStyle(appearance: appearance) + let rowButtonAppearance = appearance.embeddedPaymentElement.style.appearanceForStyle(appearance: appearance) if let savedPaymentMethod { let accessoryButton: RowButton.RightAccessoryButton? = { @@ -78,13 +78,13 @@ import UIKit didTap: handleRowSelection(selectedRowButton:))) } - if appearance.paymentOptionView.style != .floating { - stackView.addSeparators(color: appearance.paymentOptionView.paymentMethodRow.flat.separatorColor ?? appearance.colors.componentBorder, + if appearance.embeddedPaymentElement.style != .floatingButton { + stackView.addSeparators(color: appearance.embeddedPaymentElement.row.flat.separatorColor ?? appearance.colors.componentBorder, backgroundColor: appearance.colors.componentBackground, - thickness: appearance.paymentOptionView.paymentMethodRow.flat.separatorThickness, - inset: appearance.paymentOptionView.paymentMethodRow.flat.separatorInset ?? appearance.paymentOptionView.style.defaultInsets, - addTopSeparator: appearance.paymentOptionView.paymentMethodRow.flat.topSeparatorEnabled, - addBottomSeparator: appearance.paymentOptionView.paymentMethodRow.flat.bottomSeparatorEnabled) + thickness: appearance.embeddedPaymentElement.row.flat.separatorThickness, + inset: appearance.embeddedPaymentElement.row.flat.separatorInsets ?? appearance.embeddedPaymentElement.style.defaultInsets, + addTopSeparator: appearance.embeddedPaymentElement.row.flat.topSeparatorEnabled, + addBottomSeparator: appearance.embeddedPaymentElement.row.flat.bottomSeparatorEnabled) } addAndPinSubview(stackView) @@ -106,28 +106,28 @@ import UIKit } } -extension PaymentSheet.Appearance.PaymentOptionView.Style { +extension PaymentSheet.Appearance.EmbeddedPaymentElement.Style { var defaultInsets: UIEdgeInsets { switch self { - case .flatRadio: + case .flatWithRadio: return UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 0) - case .floating: + case .floatingButton: return .zero } } fileprivate func appearanceForStyle(appearance: PaymentSheet.Appearance) -> PaymentSheet.Appearance { switch self { - case .flatRadio: + case .flatWithRadio: // TODO(porter) See if there is a better way to do this, less sneaky var appearance = appearance appearance.borderWidth = 0.0 - appearance.colors.componentBorderSelected = .clear + appearance.colors.selectedComponentBorder = .clear appearance.cornerRadius = 0.0 appearance.shadow = .disabled return appearance - case .floating: + case .floatingButton: return appearance } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/RadioButton.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/RadioButton.swift index 689fc5cc359..3ed19eb9462 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/RadioButton.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/RadioButton.swift @@ -24,11 +24,11 @@ class RadioButton: UIView { } private var selectedColor: CGColor { - appearance.paymentOptionView.paymentMethodRow.flat.radio.colorSelected?.cgColor ?? appearance.colors.primary.cgColor + appearance.embeddedPaymentElement.row.flat.radio.selectedColor?.cgColor ?? appearance.colors.primary.cgColor } private var unselectedColor: CGColor { - appearance.paymentOptionView.paymentMethodRow.flat.radio.colorUnselected?.cgColor ?? appearance.colors.componentBorder.cgColor + appearance.embeddedPaymentElement.row.flat.radio.unselectedColor?.cgColor ?? appearance.colors.componentBorder.cgColor } private let didTap: () -> Void diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetAppearance.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetAppearance.swift index 2467ac7b5dc..42c75d32f69 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetAppearance.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetAppearance.swift @@ -39,14 +39,14 @@ public extension PaymentSheet { /// The border width used for selected buttons and tabs in PaymentSheet /// - Note: If `nil`, defaults to `borderWidth * 1.5` /// - Note: The behavior of this property is consistent with the behavior of border width on `CALayer` - @_spi(EmbeddedPaymentElementPrivateBeta) public var borderWidthSelected: CGFloat? + @_spi(EmbeddedPaymentElementPrivateBeta) public var selectedBorderWidth: CGFloat? /// The shadow used for inputs and tabs in PaymentSheet /// - Note: Set this to `.disabled` to disable shadows public var shadow: Shadow = Shadow() - /// Describes the appearance of the embeddable payment element - @_spi(EmbeddedPaymentElementPrivateBeta) public var paymentOptionView: PaymentOptionView = PaymentOptionView() + /// Describes the appearance of the Embedded Mobile Payment Element + @_spi(EmbeddedPaymentElementPrivateBeta) public var embeddedPaymentElement: EmbeddedPaymentElement = EmbeddedPaymentElement() // MARK: Fonts @@ -104,7 +104,7 @@ public extension PaymentSheet { /// The border color used for selected buttons and tabs in PaymentSheet /// - Note: If `nil`, defaults to `appearance.colors.primary` - @_spi(EmbeddedPaymentElementPrivateBeta) public var componentBorderSelected: UIColor? + @_spi(EmbeddedPaymentElementPrivateBeta) public var selectedComponentBorder: UIColor? /// The color of the divider lines used inside inputs, tabs, and other components public var componentDivider: UIColor = .systemGray3 @@ -234,54 +234,55 @@ public extension PaymentSheet { } @_spi(EmbeddedPaymentElementPrivateBeta) public extension PaymentSheet.Appearance { - /// Describes the appearance of the embedded payment element - struct PaymentOptionView: Equatable { + /// Describes the appearance of the Embedded Mobile Payment Element + @_spi(EmbeddedPaymentElementPrivateBeta) struct EmbeddedPaymentElement: Equatable { - /// The display style options for the embedded payment element - public enum Style { + /// The display style options for the Embedded Mobile Payment Element + public enum Style: CaseIterable { /// A flat style with radio buttons - case flatRadio - /// A floating style - case floating + case flatWithRadio + /// A floating button style + case floatingButton } - /// Creates a `PaymentSheet.Appearance.PaymentOptionView` with default values + /// Creates a `PaymentSheet.Appearance.EmbeddedPaymentElement` with default values public init() {} - /// Describes the appearance of the payment method row - public var paymentMethodRow: PaymentMethodRow = PaymentMethodRow() + /// Describes the appearance of the row in the Embedded Mobile Payment Element + public var row: Row = Row() - /// The display style of the embedded payment element - public var style: Style = .flatRadio + /// The display style of the Embedded Mobile Payment Element + public var style: Style = .flatWithRadio - /// Describes the appearance of the payment method row - public struct PaymentMethodRow: Equatable { + /// Describes the appearance of the row in the Embedded Mobile Payment Element + public struct Row: Equatable { /// Additional vertical insets applied to a payment method row - /// - Note: Increasing this value increases the height of the row + /// - Note: Increasing this value increases the height of each row public var additionalInsets: CGFloat = 4.0 /// Appearance settings for the flat style public var flat: Flat = Flat() - /// Appearance settings for the floating style + /// Appearance settings for the floating button style public var floating: Floating = Floating() - /// Describes the appearance of the flat style of the embedded payment element + /// Describes the appearance of the flat style of the Embedded Mobile Payment Element public struct Flat: Equatable { - /// The thickness of the separator line between payment method rows + /// The thickness of the separator line between rows public var separatorThickness: CGFloat = 1.0 - /// The color of the separator line between payment method rows + /// The color of the separator line between rows /// - Note: If `nil`, defaults to `appearance.colors.componentBorder` public var separatorColor: UIColor? - /// The insets of the separator line between payment method rows - public var separatorInset: UIEdgeInsets? + /// The insets of the separator line between rows + /// - Note: If `nil`, defaults to `UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 0)` for style of `flatWithRadio` and to `UIEdgeInsets.zero` for style of `floatingButton`. + public var separatorInsets: UIEdgeInsets? - /// Determines if the top separator is visible at the top of the embedded payment element + /// Determines if the top separator is visible at the top of the Embedded Mobile Payment Element public var topSeparatorEnabled: Bool = true - /// Determines if the bottom separator is visible at the bottom of the embedded payment element + /// Determines if the bottom separator is visible at the bottom of the Embedded Mobile Payment Element public var bottomSeparatorEnabled: Bool = true /// Appearance settings for the radio button @@ -291,15 +292,15 @@ public extension PaymentSheet { public struct Radio: Equatable { /// The color of the radio button when selected /// - Note: If `nil`, defaults to `appearance.color.primaryColor` - public var colorSelected: UIColor? + public var selectedColor: UIColor? /// The color of the radio button when unselected /// - Note: If `nil`, defaults to `appearance.colors.componentBorder` - public var colorUnselected: UIColor? + public var unselectedColor: UIColor? } } - /// Describes the appearance of the floating style payment method row + /// Describes the appearance of the floating button style payment method row public struct Floating: Equatable { /// The spacing between payment method rows public var spacing: CGFloat = 12.0 diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Vertical Main Screen/RowButton.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Vertical Main Screen/RowButton.swift index 270ad9367b0..74ed3a85a58 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Vertical Main Screen/RowButton.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Vertical Main Screen/RowButton.swift @@ -15,7 +15,7 @@ import UIKit class RowButton: UIView { private let shadowRoundedRect: ShadowedRoundedRectangle private lazy var radioButton: RadioButton? = { - guard isEmbedded, appearance.paymentOptionView.style == .flatRadio else { return nil } + guard isEmbedded, appearance.embeddedPaymentElement.style == .flatWithRadio else { return nil } return RadioButton(appearance: appearance) { [weak self] in guard let self else { return } self.didTap(self) @@ -115,16 +115,16 @@ class RowButton: UIView { imageView.leadingAnchor.constraint(equalTo: radioButton?.trailingAnchor ?? leadingAnchor, constant: 12), imageView.centerYAnchor.constraint(equalTo: centerYAnchor), - imageView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 10 + appearance.paymentOptionView.paymentMethodRow.additionalInsets), - imageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -10 - appearance.paymentOptionView.paymentMethodRow.additionalInsets), + imageView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 10 + appearance.embeddedPaymentElement.row.additionalInsets), + imageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -10 - appearance.embeddedPaymentElement.row.additionalInsets), imageView.heightAnchor.constraint(equalToConstant: 20), imageView.widthAnchor.constraint(equalToConstant: 24), labelsStackView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 12), labelsStackView.trailingAnchor.constraint(equalTo: rightAccessoryView?.leadingAnchor ?? trailingAnchor, constant: -12), labelsStackView.centerYAnchor.constraint(equalTo: centerYAnchor), - labelsStackView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: appearance.paymentOptionView.paymentMethodRow.additionalInsets), - labelsStackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -appearance.paymentOptionView.paymentMethodRow.additionalInsets), + labelsStackView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: appearance.embeddedPaymentElement.row.additionalInsets), + labelsStackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -appearance.embeddedPaymentElement.row.additionalInsets), imageViewBottomConstraint, imageViewTopConstraint, diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/ShadowedRoundedRectangleView.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/ShadowedRoundedRectangleView.swift index ed6a005ee80..624c93a644a 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/ShadowedRoundedRectangleView.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/ShadowedRoundedRectangleView.swift @@ -49,14 +49,14 @@ class ShadowedRoundedRectangle: UIView { // Border if isSelected { - let selectedBorderWidth = appearance.borderWidthSelected ?? appearance.borderWidth + let selectedBorderWidth = appearance.selectedBorderWidth ?? appearance.borderWidth if selectedBorderWidth > 0 { layer.borderWidth = selectedBorderWidth * 1.5 } else { // Without a border, the customer can't tell this is selected and it looks bad layer.borderWidth = 1.5 } - layer.borderColor = appearance.colors.componentBorderSelected?.cgColor ?? appearance.colors.primary.cgColor + layer.borderColor = appearance.colors.selectedComponentBorder?.cgColor ?? appearance.colors.primary.cgColor } else { layer.borderWidth = appearance.borderWidth layer.borderColor = appearance.colors.componentBorder.cgColor diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/EmbeddedPaymentMethodsViewSnapshotTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/EmbeddedPaymentMethodsViewSnapshotTests.swift index 9c6961012d4..e24955b9cd7 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/EmbeddedPaymentMethodsViewSnapshotTests.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/EmbeddedPaymentMethodsViewSnapshotTests.swift @@ -62,7 +62,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_flatRadio_rowHeight() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.paymentMethodRow.additionalInsets = 20 + appearance.embeddedPaymentElement.row.additionalInsets = 20 let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp), .stripe(.klarna)], savedPaymentMethod: nil, @@ -73,16 +73,16 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { verify(embeddedView) // Assert height let defaultHeight = RowButton.calculateTallestHeight(appearance: .default) - let defaultInset = PaymentSheet.Appearance.default.paymentOptionView.paymentMethodRow.additionalInsets + let defaultInset = PaymentSheet.Appearance.default.embeddedPaymentElement.row.additionalInsets for case let rowButton as RowButton in embeddedView.stackView.arrangedSubviews { let newHeight = rowButton.frame.size.height - XCTAssertEqual((appearance.paymentOptionView.paymentMethodRow.additionalInsets - defaultInset) * 2, newHeight - defaultHeight) + XCTAssertEqual((appearance.embeddedPaymentElement.row.additionalInsets - defaultInset) * 2, newHeight - defaultHeight) } } func testEmbeddedPaymentMethodsView_flatRadio_rowHeightSingleLine() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.paymentMethodRow.additionalInsets = 20 + appearance.embeddedPaymentElement.row.additionalInsets = 20 let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -95,16 +95,16 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { // Assert height let defaultHeight = RowButton.calculateTallestHeight(appearance: .default) - let defaultInset = PaymentSheet.Appearance.default.paymentOptionView.paymentMethodRow.additionalInsets + let defaultInset = PaymentSheet.Appearance.default.embeddedPaymentElement.row.additionalInsets for case let rowButton as RowButton in embeddedView.stackView.arrangedSubviews { let newHeight = rowButton.frame.size.height - XCTAssertEqual((appearance.paymentOptionView.paymentMethodRow.additionalInsets - defaultInset) * 2, newHeight - defaultHeight) + XCTAssertEqual((appearance.embeddedPaymentElement.row.additionalInsets - defaultInset) * 2, newHeight - defaultHeight) } } func testEmbeddedPaymentMethodsView_flatRadio_separatorThickness() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.paymentMethodRow.flat.separatorThickness = 10 + appearance.embeddedPaymentElement.row.flat.separatorThickness = 10 let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -118,7 +118,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_flatRadio_separatorColor() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.paymentMethodRow.flat.separatorColor = .red + appearance.embeddedPaymentElement.row.flat.separatorColor = .red let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -132,7 +132,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_flatRadio_separatorInset() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.paymentMethodRow.flat.separatorInset = UIEdgeInsets(top: 0, left: 50, bottom: 0, right: 50) + appearance.embeddedPaymentElement.row.flat.separatorInsets = UIEdgeInsets(top: 0, left: 50, bottom: 0, right: 50) let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -146,7 +146,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_flatRadio_topSeparatorDisabled() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.paymentMethodRow.flat.topSeparatorEnabled = false + appearance.embeddedPaymentElement.row.flat.topSeparatorEnabled = false let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -160,7 +160,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_flatRadio_bottomSeparatorDisabled() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.paymentMethodRow.flat.bottomSeparatorEnabled = false + appearance.embeddedPaymentElement.row.flat.bottomSeparatorEnabled = false let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -174,7 +174,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_flatRadio_colorSelected() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.paymentMethodRow.flat.radio.colorSelected = .red + appearance.embeddedPaymentElement.row.flat.radio.selectedColor = .red let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -193,7 +193,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_flatRadio_colorUnselected() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.paymentMethodRow.flat.radio.colorUnselected = .red + appearance.embeddedPaymentElement.row.flat.radio.unselectedColor = .red let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -253,7 +253,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_floating() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.style = .floating + appearance.embeddedPaymentElement.style = .floatingButton let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -267,7 +267,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_floating_savedPaymentMethod() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.style = .floating + appearance.embeddedPaymentElement.style = .floatingButton let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: STPPaymentMethod._testCard(), @@ -281,7 +281,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_floating_noApplePay() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.style = .floating + appearance.embeddedPaymentElement.style = .floatingButton let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -295,7 +295,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_floating_noLink() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.style = .floating + appearance.embeddedPaymentElement.style = .floatingButton let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -309,8 +309,8 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_floating_rowHeight() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.style = .floating - appearance.paymentOptionView.paymentMethodRow.additionalInsets = 20 + appearance.embeddedPaymentElement.style = .floatingButton + appearance.embeddedPaymentElement.row.additionalInsets = 20 let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp), .stripe(.afterpayClearpay)], savedPaymentMethod: nil, @@ -323,17 +323,17 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { // Assert height let defaultHeight = RowButton.calculateTallestHeight(appearance: .default) - let defaultInset = PaymentSheet.Appearance.default.paymentOptionView.paymentMethodRow.additionalInsets + let defaultInset = PaymentSheet.Appearance.default.embeddedPaymentElement.row.additionalInsets for case let rowButton as RowButton in embeddedView.stackView.arrangedSubviews { let newHeight = rowButton.frame.size.height - XCTAssertEqual((appearance.paymentOptionView.paymentMethodRow.additionalInsets - defaultInset) * 2, newHeight - defaultHeight) + XCTAssertEqual((appearance.embeddedPaymentElement.row.additionalInsets - defaultInset) * 2, newHeight - defaultHeight) } } func testEmbeddedPaymentMethodsView_floating_rowHeightSingleLine() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.style = .floating - appearance.paymentOptionView.paymentMethodRow.additionalInsets = 20 + appearance.embeddedPaymentElement.style = .floatingButton + appearance.embeddedPaymentElement.row.additionalInsets = 20 let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -346,17 +346,17 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { // Assert height let defaultHeight = RowButton.calculateTallestHeight(appearance: .default) - let defaultInset = PaymentSheet.Appearance.default.paymentOptionView.paymentMethodRow.additionalInsets + let defaultInset = PaymentSheet.Appearance.default.embeddedPaymentElement.row.additionalInsets for case let rowButton as RowButton in embeddedView.stackView.arrangedSubviews { let newHeight = rowButton.frame.size.height - XCTAssertEqual((appearance.paymentOptionView.paymentMethodRow.additionalInsets - defaultInset) * 2, newHeight - defaultHeight) + XCTAssertEqual((appearance.embeddedPaymentElement.row.additionalInsets - defaultInset) * 2, newHeight - defaultHeight) } } func testEmbeddedPaymentMethodsView_floating_spacing() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.style = .floating - appearance.paymentOptionView.paymentMethodRow.floating.spacing = 30 + appearance.embeddedPaymentElement.style = .floatingButton + appearance.embeddedPaymentElement.row.floating.spacing = 30 let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -370,9 +370,9 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_floating_selectedBorder() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView .style = .floating - appearance.borderWidthSelected = 5.0 - appearance.colors.componentBorderSelected = .red + appearance.embeddedPaymentElement .style = .floatingButton + appearance.selectedBorderWidth = 5.0 + appearance.colors.selectedComponentBorder = .red let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], savedPaymentMethod: nil, @@ -391,7 +391,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_floating_borderWidth() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView .style = .floating + appearance.embeddedPaymentElement .style = .floatingButton appearance.borderWidth = 5.0 appearance.colors.primary = .red @@ -412,7 +412,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_floating_componentBackgroundColor() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.style = .floating + appearance.embeddedPaymentElement.style = .floatingButton appearance.colors.componentBackground = .purple let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], @@ -427,7 +427,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_floating_cornerRadius() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.style = .floating + appearance.embeddedPaymentElement.style = .floatingButton appearance.cornerRadius = 15 let embeddedView = EmbeddedPaymentMethodsView(paymentMethodTypes: [.stripe(.card), .stripe(.cashApp)], @@ -442,7 +442,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_floating_smallFont() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.style = .floating + appearance.embeddedPaymentElement.style = .floatingButton appearance.font.sizeScaleFactor = 0.5 appearance.font.base = UIFont(name: "AmericanTypewriter", size: 12)! @@ -458,7 +458,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase { func testEmbeddedPaymentMethodsView_floating_largeFont() { var appearance: PaymentSheet.Appearance = .default - appearance.paymentOptionView.style = .floating + appearance.embeddedPaymentElement.style = .floatingButton appearance.font.sizeScaleFactor = 1.5 appearance.font.base = UIFont(name: "AmericanTypewriter", size: 12)! diff --git a/ci_scripts/dead_code/process_periphery_output.rb b/ci_scripts/dead_code/process_periphery_output.rb new file mode 100644 index 00000000000..f450e97e23e --- /dev/null +++ b/ci_scripts/dead_code/process_periphery_output.rb @@ -0,0 +1,37 @@ +#!/usr/bin/env ruby +require 'json' + +# Check for correct usage +if ARGV.length != 2 + puts "Usage: ruby ci_scripts/dead_code/process_periphery_output.rb " + exit 1 +end + +periphery_output_file = ARGV[0] +output_json_file = ARGV[1] + +unused_code = {} + +File.foreach(periphery_output_file) do |line| + line.chomp! + + # Remove full file path and extract relevant information + # Example line: + # /path/to/file/SimpleMandateTextView.swift:29:17: warning: Initializer 'init(mandateText:theme:)' is unused + + if line =~ %r{^(?:.*/)?(.+?)(?::\d+:\d+)?\s*:?\s*warning:\s*(?:.+?)\s+'(.+?)'\s+is unused$} + filename = $1 + identifier = $2 + + key = "#{filename}_#{identifier}" + + unused_code[key] = "#{filename}: warning: #{identifier} is unused" + else + # Handle lines that don't match the expected pattern + puts "Skipping unrecognized line: #{line}" + end +end + +File.open(output_json_file, 'w') do |f| + f.write(JSON.pretty_generate(unused_code)) +end