diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 231a156b..9cc9b230 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -2,7 +2,6 @@ name: Build and Test env: afterpay-scheme: Afterpay - DEVELOPER_DIR: /Applications/Xcode_16.1.0.app/Contents/Developer on: push: @@ -17,7 +16,6 @@ jobs: runs-on: macos-latest env: - destination: platform=iOS Simulator,name=iPhone 16,OS=18.1 example-scheme: Example example-ui-test-scheme: ExampleUITests workspace: Afterpay.xcworkspace @@ -25,6 +23,21 @@ jobs: steps: - uses: actions/checkout@v4 + - name: List Available Simulators + run: xcrun simctl list devices available + + - name: Set Destination + run: | + # Find any available iPhone simulator + SIMULATOR_NAME=$(xcrun simctl list devices available | grep 'iPhone' | grep -v 'unavailable' | head -1 | sed -E 's/.*iPhone ([0-9]+).*/iPhone \1/') + if [ -z "$SIMULATOR_NAME" ]; then + echo "No iPhone simulator found, falling back to generic iOS Simulator" + echo "destination=platform=iOS Simulator" >> $GITHUB_ENV + else + echo "Using simulator: $SIMULATOR_NAME" + echo "destination=platform=iOS Simulator,name=$SIMULATOR_NAME" >> $GITHUB_ENV + fi + - name: Cache Mint Packages uses: actions/cache@v4 with: diff --git a/.gitignore b/.gitignore index cc718424..7491fbae 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,5 @@ iOSInjectionProject/ # Generated doc files # docs/_site +CLAUDE.md +/.claude diff --git a/Example/Example/Components/ComponentsViewController.swift b/Example/Example/Components/ComponentsViewController.swift index dff21542..44af609c 100644 --- a/Example/Example/Components/ComponentsViewController.swift +++ b/Example/Example/Components/ComponentsViewController.swift @@ -61,6 +61,12 @@ final class ComponentsViewController: UIViewController { embed: contentStack.addArrangedSubview ) + let divider = UIView() + divider.backgroundColor = .separator + divider.translatesAutoresizingMaskIntoConstraints = false + divider.heightAnchor.constraint(equalToConstant: 1).isActive = true + contentStack.addArrangedSubview(divider) + install( ContentStackViewController(stackTitle: "Dark Theme", userInterfaceStyle: .dark), embed: contentStack.addArrangedSubview @@ -143,7 +149,7 @@ private final class ContentStackViewController: UIViewController, PriceBreakdown let stack = UIStackView() stack.translatesAutoresizingMaskIntoConstraints = false stack.axis = .vertical - stack.spacing = 8 + stack.spacing = 16 let titleLabel = UILabel() titleLabel.text = stackTitle @@ -152,76 +158,215 @@ private final class ContentStackViewController: UIViewController, PriceBreakdown stack.addArrangedSubview(titleLabel) if Afterpay.enabled { + // MARK: - BadgeView (not available in Cash App Afterpay regions) if !Afterpay.isCashAppAfterpayRegion { - let badge = BadgeView(colorScheme: .dynamic(lightPalette: .default, darkPalette: .alt)) - badge.widthAnchor.constraint(equalToConstant: 64).isActive = true + stack.addArrangedSubview(sectionHeader("BadgeView")) + + let badgeDefault = BadgeView(colorScheme: .static(.default)) + badgeDefault.widthAnchor.constraint(equalToConstant: 64).isActive = true + stack.addArrangedSubview(labeledView(badgeDefault, caption: "BadgeView - default")) - let badge2 = BadgeView(colorScheme: .dynamic(lightPalette: .darkMono, darkPalette: .lightMono)) - badge.widthAnchor.constraint(equalToConstant: 64).isActive = true + let badgeAlt = BadgeView(colorScheme: .static(.alt)) + badgeAlt.widthAnchor.constraint(equalToConstant: 64).isActive = true + stack.addArrangedSubview(labeledView(badgeAlt, caption: "BadgeView - alt")) - let badgeStack = UIStackView(arrangedSubviews: [badge, badge2, UIView()]) - stack.addArrangedSubview(badgeStack) - let lockup = LockupView(colorScheme: .dynamic(lightPalette: .lightMono, darkPalette: .darkMono)) - lockup.widthAnchor.constraint(equalToConstant: 64).isActive = true + let badgeDarkMono = BadgeView(colorScheme: .static(.darkMono)) + badgeDarkMono.widthAnchor.constraint(equalToConstant: 64).isActive = true + stack.addArrangedSubview(labeledView(badgeDarkMono, caption: "BadgeView - darkMono")) - let lockup2 = LockupView(colorScheme: .dynamic(lightPalette: .darkMono, darkPalette: .default)) - lockup.widthAnchor.constraint(equalToConstant: 64).isActive = true - let lockupStack = UIStackView(arrangedSubviews: [lockup, lockup2, UIView()]) - stack.addArrangedSubview(lockupStack) + let badgeLightMono = BadgeView(colorScheme: .static(.lightMono)) + badgeLightMono.widthAnchor.constraint(equalToConstant: 64).isActive = true + stack.addArrangedSubview(labeledView(badgeLightMono, caption: "BadgeView - lightMono")) } - let payButton: UIButton = PaymentButton( - colorScheme: .dynamic(lightPalette: .default, darkPalette: .alt), - buttonKind: .buyNow - ) - stack.addArrangedSubview(payButton) - let payButton2: UIButton = PaymentButton( - colorScheme: .dynamic(lightPalette: .lightMono, darkPalette: .darkMono), - buttonKind: .checkout - ) - stack.addArrangedSubview(payButton2) - let payButton3: UIButton = PaymentButton( - colorScheme: .dynamic(lightPalette: .alt, darkPalette: .default), - buttonKind: .payNow - ) - stack.addArrangedSubview(payButton3) - let payButton4: UIButton = PaymentButton( - colorScheme: .dynamic(lightPalette: .darkMono, darkPalette: .lightMono), - buttonKind: .continueWith - ) - stack.addArrangedSubview(payButton4) - - let priceBreakdown1 = PriceBreakdownView() - priceBreakdown1.introText = .payInTitle - priceBreakdown1.totalAmount = 103.50 - priceBreakdown1.delegate = self - priceBreakdown1.moreInfoOptions = MoreInfoOptions(modalTheme: .white) - priceBreakdown1.logoColorScheme = .dynamic(lightPalette: .darkMono, darkPalette: .alt) - - stack.addArrangedSubview(priceBreakdown1) - - let priceBreakdown2 = PriceBreakdownView() - priceBreakdown2.totalAmount = 100 - priceBreakdown2.delegate = self - priceBreakdown2.showWithText = false - priceBreakdown2.showInterestFreeText = false - priceBreakdown2.logoColorScheme = .dynamic(lightPalette: .default, darkPalette: .lightMono) - priceBreakdown2.moreInfoOptions = MoreInfoOptions(modalLinkStyle: .moreInfoText) - priceBreakdown2.logoType = .compactBadge - stack.addArrangedSubview(priceBreakdown2) - - let priceBreakdown3 = PriceBreakdownView() - priceBreakdown3.totalAmount = 3000 - priceBreakdown3.delegate = self - priceBreakdown3.logoColorScheme = .dynamic(lightPalette: .darkMono, darkPalette: .lightMono) - stack.addArrangedSubview(priceBreakdown3) - let b = BadgeView(colorScheme: .dynamic(lightPalette: .alt, darkPalette: .darkMono)) - stack.addArrangedSubview(b) + // MARK: - LockupView + stack.addArrangedSubview(sectionHeader("LockupView")) + + let lockupDefault = LockupView(colorScheme: .static(.default)) + lockupDefault.widthAnchor.constraint(equalToConstant: 64).isActive = true + stack.addArrangedSubview(labeledView(lockupDefault, caption: "LockupView - default")) + + let lockupAlt = LockupView(colorScheme: .static(.alt)) + lockupAlt.widthAnchor.constraint(equalToConstant: 64).isActive = true + stack.addArrangedSubview(labeledView(lockupAlt, caption: "LockupView - alt")) + + let lockupDarkMono = LockupView(colorScheme: .static(.darkMono)) + lockupDarkMono.widthAnchor.constraint(equalToConstant: 64).isActive = true + stack.addArrangedSubview(labeledView(lockupDarkMono, caption: "LockupView - darkMono")) + + let lockupLightMono = LockupView(colorScheme: .static(.lightMono)) + lockupLightMono.widthAnchor.constraint(equalToConstant: 64).isActive = true + stack.addArrangedSubview(labeledView(lockupLightMono, caption: "LockupView - lightMono")) + + // MARK: - PaymentButton + stack.addArrangedSubview(sectionHeader("PaymentButton")) + + // buyNow - all color palettes + let buyNowDefault = PaymentButton(colorScheme: .static(.default), buttonKind: .buyNow) + stack.addArrangedSubview(labeledView(buyNowDefault, caption: "PaymentButton - buyNow, default")) + + let buyNowAlt = PaymentButton(colorScheme: .static(.alt), buttonKind: .buyNow) + stack.addArrangedSubview(labeledView(buyNowAlt, caption: "PaymentButton - buyNow, alt")) + + let buyNowDarkMono = PaymentButton(colorScheme: .static(.darkMono), buttonKind: .buyNow) + stack.addArrangedSubview(labeledView(buyNowDarkMono, caption: "PaymentButton - buyNow, darkMono")) + + let buyNowLightMono = PaymentButton(colorScheme: .static(.lightMono), buttonKind: .buyNow) + stack.addArrangedSubview(labeledView(buyNowLightMono, caption: "PaymentButton - buyNow, lightMono")) + + // checkout - all color palettes + let checkoutDefault = PaymentButton(colorScheme: .static(.default), buttonKind: .checkout) + stack.addArrangedSubview(labeledView(checkoutDefault, caption: "PaymentButton - checkout, default")) + + let checkoutAlt = PaymentButton(colorScheme: .static(.alt), buttonKind: .checkout) + stack.addArrangedSubview(labeledView(checkoutAlt, caption: "PaymentButton - checkout, alt")) + + let checkoutDarkMono = PaymentButton(colorScheme: .static(.darkMono), buttonKind: .checkout) + stack.addArrangedSubview(labeledView(checkoutDarkMono, caption: "PaymentButton - checkout, darkMono")) + + let checkoutLightMono = PaymentButton(colorScheme: .static(.lightMono), buttonKind: .checkout) + stack.addArrangedSubview(labeledView(checkoutLightMono, caption: "PaymentButton - checkout, lightMono")) + + // payNow - all color palettes + let payNowDefault = PaymentButton(colorScheme: .static(.default), buttonKind: .payNow) + stack.addArrangedSubview(labeledView(payNowDefault, caption: "PaymentButton - payNow, default")) + + let payNowAlt = PaymentButton(colorScheme: .static(.alt), buttonKind: .payNow) + stack.addArrangedSubview(labeledView(payNowAlt, caption: "PaymentButton - payNow, alt")) + + let payNowDarkMono = PaymentButton(colorScheme: .static(.darkMono), buttonKind: .payNow) + stack.addArrangedSubview(labeledView(payNowDarkMono, caption: "PaymentButton - payNow, darkMono")) + + let payNowLightMono = PaymentButton(colorScheme: .static(.lightMono), buttonKind: .payNow) + stack.addArrangedSubview(labeledView(payNowLightMono, caption: "PaymentButton - payNow, lightMono")) + + // continueWith - all color palettes + let continueWithDefault = PaymentButton(colorScheme: .static(.default), buttonKind: .continueWith) + stack.addArrangedSubview(labeledView(continueWithDefault, caption: "PaymentButton - continueWith, default")) + + let continueWithAlt = PaymentButton(colorScheme: .static(.alt), buttonKind: .continueWith) + stack.addArrangedSubview(labeledView(continueWithAlt, caption: "PaymentButton - continueWith, alt")) + + let continueWithDarkMono = PaymentButton(colorScheme: .static(.darkMono), buttonKind: .continueWith) + stack.addArrangedSubview(labeledView(continueWithDarkMono, caption: "PaymentButton - continueWith, darkMono")) + + let continueWithLightMono = PaymentButton(colorScheme: .static(.lightMono), buttonKind: .continueWith) + stack.addArrangedSubview(labeledView(continueWithLightMono, caption: "PaymentButton - continueWith, lightMono")) + + // MARK: - PriceBreakdownView + stack.addArrangedSubview(sectionHeader("PriceBreakdownView")) + + // Logo type variations + let priceBreakdownBadge = PriceBreakdownView() + priceBreakdownBadge.totalAmount = 100 + priceBreakdownBadge.delegate = self + priceBreakdownBadge.logoType = .badge + stack.addArrangedSubview(labeledView(priceBreakdownBadge, caption: "PriceBreakdownView - logoType: badge")) + + let priceBreakdownLockup = PriceBreakdownView() + priceBreakdownLockup.totalAmount = 100 + priceBreakdownLockup.delegate = self + priceBreakdownLockup.logoType = .lockup + stack.addArrangedSubview(labeledView(priceBreakdownLockup, caption: "PriceBreakdownView - logoType: lockup")) + + let priceBreakdownCompact = PriceBreakdownView() + priceBreakdownCompact.totalAmount = 100 + priceBreakdownCompact.delegate = self + priceBreakdownCompact.logoType = .compactBadge + stack.addArrangedSubview(labeledView(priceBreakdownCompact, caption: "PriceBreakdownView - logoType: compactBadge")) + + // Intro text variations + let priceBreakdownOr = PriceBreakdownView() + priceBreakdownOr.totalAmount = 100 + priceBreakdownOr.delegate = self + priceBreakdownOr.introText = .or + stack.addArrangedSubview(labeledView(priceBreakdownOr, caption: "PriceBreakdownView - introText: or")) + + let priceBreakdownPayIn = PriceBreakdownView() + priceBreakdownPayIn.totalAmount = 100 + priceBreakdownPayIn.delegate = self + priceBreakdownPayIn.introText = .payIn + stack.addArrangedSubview(labeledView(priceBreakdownPayIn, caption: "PriceBreakdownView - introText: payIn")) + + let priceBreakdownMake = PriceBreakdownView() + priceBreakdownMake.totalAmount = 100 + priceBreakdownMake.delegate = self + priceBreakdownMake.introText = .make + stack.addArrangedSubview(labeledView(priceBreakdownMake, caption: "PriceBreakdownView - introText: make")) + + let priceBreakdownPay = PriceBreakdownView() + priceBreakdownPay.totalAmount = 100 + priceBreakdownPay.delegate = self + priceBreakdownPay.introText = .pay + stack.addArrangedSubview(labeledView(priceBreakdownPay, caption: "PriceBreakdownView - introText: pay")) + + let priceBreakdownIn = PriceBreakdownView() + priceBreakdownIn.totalAmount = 100 + priceBreakdownIn.delegate = self + priceBreakdownIn.introText = .in + stack.addArrangedSubview(labeledView(priceBreakdownIn, caption: "PriceBreakdownView - introText: in")) + + let priceBreakdownEmpty = PriceBreakdownView() + priceBreakdownEmpty.totalAmount = 100 + priceBreakdownEmpty.delegate = self + priceBreakdownEmpty.introText = .empty + stack.addArrangedSubview(labeledView(priceBreakdownEmpty, caption: "PriceBreakdownView - introText: empty")) + + // Display options + let priceBreakdownNoWith = PriceBreakdownView() + priceBreakdownNoWith.totalAmount = 100 + priceBreakdownNoWith.delegate = self + priceBreakdownNoWith.showWithText = false + stack.addArrangedSubview(labeledView(priceBreakdownNoWith, caption: "PriceBreakdownView - showWithText: false")) + + let priceBreakdownNoInterest = PriceBreakdownView() + priceBreakdownNoInterest.totalAmount = 100 + priceBreakdownNoInterest.delegate = self + priceBreakdownNoInterest.showInterestFreeText = false + stack.addArrangedSubview(labeledView(priceBreakdownNoInterest, caption: "PriceBreakdownView - showInterestFreeText: false")) + + let priceBreakdownMinimal = PriceBreakdownView() + priceBreakdownMinimal.totalAmount = 100 + priceBreakdownMinimal.delegate = self + priceBreakdownMinimal.showWithText = false + priceBreakdownMinimal.showInterestFreeText = false + stack.addArrangedSubview(labeledView(priceBreakdownMinimal, caption: "PriceBreakdownView - minimal (no with/interest)")) + + // Amount edge cases + let priceBreakdownOutOfRange = PriceBreakdownView() + priceBreakdownOutOfRange.totalAmount = 5000 + priceBreakdownOutOfRange.delegate = self + stack.addArrangedSubview(labeledView(priceBreakdownOutOfRange, caption: "PriceBreakdownView - amount out of range")) + + // Logo color schemes + let priceBreakdownLogoDefault = PriceBreakdownView() + priceBreakdownLogoDefault.totalAmount = 100 + priceBreakdownLogoDefault.delegate = self + priceBreakdownLogoDefault.logoColorScheme = .static(.default) + stack.addArrangedSubview(labeledView(priceBreakdownLogoDefault, caption: "PriceBreakdownView - logoColorScheme: default")) + + let priceBreakdownLogoAlt = PriceBreakdownView() + priceBreakdownLogoAlt.totalAmount = 100 + priceBreakdownLogoAlt.delegate = self + priceBreakdownLogoAlt.logoColorScheme = .static(.alt) + stack.addArrangedSubview(labeledView(priceBreakdownLogoAlt, caption: "PriceBreakdownView - logoColorScheme: alt")) + + let priceBreakdownLogoDarkMono = PriceBreakdownView() + priceBreakdownLogoDarkMono.totalAmount = 100 + priceBreakdownLogoDarkMono.delegate = self + priceBreakdownLogoDarkMono.logoColorScheme = .static(.darkMono) + stack.addArrangedSubview(labeledView(priceBreakdownLogoDarkMono, caption: "PriceBreakdownView - logoColorScheme: darkMono")) + + let priceBreakdownLogoLightMono = PriceBreakdownView() + priceBreakdownLogoLightMono.totalAmount = 100 + priceBreakdownLogoLightMono.delegate = self + priceBreakdownLogoLightMono.logoColorScheme = .static(.lightMono) + stack.addArrangedSubview(labeledView(priceBreakdownLogoLightMono, caption: "PriceBreakdownView - logoColorScheme: lightMono")) } let stackConstraints = [ - stack.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), - stack.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), + stack.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), stack.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), stack.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8), ] @@ -234,4 +379,43 @@ private final class ContentStackViewController: UIViewController, PriceBreakdown func viewControllerForPresentation() -> UIViewController { self } + private func sectionHeader(_ title: String) -> UIView { + let container = UIView() + + let label = UILabel() + label.text = title + label.font = .preferredFont(forTextStyle: .headline) + label.textColor = .label + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + + container.addSubview(label) + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(equalTo: container.leadingAnchor), + label.trailingAnchor.constraint(equalTo: container.trailingAnchor), + label.topAnchor.constraint(equalTo: container.topAnchor, constant: 16), + label.bottomAnchor.constraint(equalTo: container.bottomAnchor), + ]) + + return container + } + + private func labeledView(_ view: UIView, caption: String) -> UIView { + let container = UIStackView() + container.axis = .vertical + container.alignment = .leading + container.spacing = 4 + + let label = UILabel() + label.text = caption + label.font = .preferredFont(forTextStyle: .caption1) + label.textColor = .secondaryLabel + label.adjustsFontForContentSizeCategory = true + + container.addArrangedSubview(view) + container.addArrangedSubview(label) + + return container + } + }