Skip to content

Commit

Permalink
feat(POM-433): extend UI customization (#377)
Browse files Browse the repository at this point in the history
* Support customizing text field prompt and icon.
* Support customizing button icon.
* Fix nAPM field validation error animation.
* Fix stale information when changing payment method during DC.
* Refactor UI modules configurations.
  • Loading branch information
andrii-vysotskyi-cko authored Nov 6, 2024
1 parent eb08a01 commit abb8616
Show file tree
Hide file tree
Showing 40 changed files with 1,191 additions and 568 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,11 @@ final class AlternativePaymentsViewModel: ObservableObject {
let configuration = PONativeAlternativePaymentConfiguration(
invoiceId: invoice.id,
gatewayConfigurationId: gatewayConfigurationId,
secondaryAction: .cancel(
cancelButton: .init(
confirmation: .init()
),
paymentConfirmation: .init(
showProgressIndicatorAfter: 5,
confirmButton: .init(),
secondaryAction: .cancel(disabledFor: 10)
showProgressViewAfter: 5, confirmButton: .init(), cancelButton: .init(disabledFor: 10)
)
)
let nativePaymentItem = AlternativePaymentsViewModelState.NativePayment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ final class CardPaymentViewModel: ObservableObject {

private func setCardTokenizationItem() {
let configuration = POCardTokenizationConfiguration(
isCardholderNameInputVisible: false, isSavingAllowed: true
cardholderName: nil, isSavingAllowed: true
)
let cardTokenizationItem = CardPaymentViewModelState.CardTokenization(
id: UUID().uuidString,
Expand Down
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 5.10

import PackageDescription

Expand Down Expand Up @@ -45,6 +45,9 @@ let package = Package(
],
resources: [
.process("Resources")
],
swiftSettings: [
.enableUpcomingFeature("IsolatedDefaultValues")
]
),
.target(
Expand Down
2 changes: 1 addition & 1 deletion ProcessOutCheckout3DS.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = 'ProcessOutCheckout3DS'
s.version = '4.21.1'
s.swift_versions = ['5.9']
s.swift_versions = ['5.10']
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.homepage = 'https://github.com/processout/processout-ios'
s.author = 'ProcessOut'
Expand Down
2 changes: 1 addition & 1 deletion ProcessOutCoreUI.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = 'ProcessOutCoreUI'
s.version = '4.21.1'
s.swift_versions = ['5.9']
s.swift_versions = ['5.10']
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.homepage = 'https://github.com/processout/processout-ios'
s.author = 'ProcessOut'
Expand Down
4 changes: 2 additions & 2 deletions ProcessOutUI.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = 'ProcessOutUI'
s.version = '4.21.1'
s.swift_versions = ['5.9']
s.swift_versions = ['5.10']
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.homepage = 'https://github.com/processout/processout-ios'
s.author = 'ProcessOut'
Expand All @@ -11,7 +11,7 @@ Pod::Spec.new do |s|
s.ios.deployment_target = '14.0'
s.ios.resources = 'Sources/ProcessOutUI/Resources/**/*'
s.source_files = 'Sources/ProcessOutUI/**/*.swift'
s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-Xfrontend -module-interface-preserve-types-as-written' }
s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-Xfrontend -module-interface-preserve-types-as-written -enable-upcoming-feature IsolatedDefaultValues' }
s.dependency 'ProcessOut', s.version.to_s
s.dependency 'ProcessOutCoreUI', s.version.to_s
end
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ public struct POActionsContainerView: View {
if !actions.isEmpty {
VStack(spacing: POSpacing.small) {
ForEach(actions) { element in
Button(element.title, action: element.action)
Button.create(with: element)
.buttonStyle(forPrimaryRole: style.primary, fallback: style.secondary)
.buttonViewModel(element)
}
.modify(when: style.axis == .horizontal) { content in
// The implementation considers that benign action that people are likely to
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// POButtonRoleStyleProvider.swift
// View+RoleButtonStyle.swift.swift
// ProcessOut
//
// Created by Andrii Vysotskyi on 19.10.2024.
Expand All @@ -20,24 +20,24 @@ extension View {
public func buttonStyle(
forPrimaryRole primaryStyle: any ButtonStyle, fallback fallbackStyle: any ButtonStyle
) -> some View {
modifier(ContentModifier(primaryStyle: primaryStyle, fallbackStyle: fallbackStyle))
self.buttonStyle(RoleButtonStyle(primaryStyle: primaryStyle, fallbackStyle: fallbackStyle))
}
}

private struct ContentModifier: ViewModifier {
private struct RoleButtonStyle: ButtonStyle {

let primaryStyle, fallbackStyle: any ButtonStyle

// MARK: - ViewModifier
// MARK: - ButtonStyle

func body(content: Content) -> some View {
func makeBody(configuration: Configuration) -> some View {
let resolvedStyle = switch poButtonRole {
case .primary:
primaryStyle
default:
fallbackStyle
}
content.buttonStyle(POAnyButtonStyle(erasing: resolvedStyle))
AnyView(resolvedStyle.makeBody(configuration: configuration))
}

// MARK: - Private Properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,55 @@
//

import Foundation
import SwiftUI

@_spi(PO)
@MainActor
public struct POButtonViewModel: Identifiable {

/// Creates view model with given parameters.
public init(
id: String,
title: String,
isEnabled: Bool = true,
isLoading: Bool = false,
role: POButtonRole? = nil,
action: @escaping () -> Void
) {
self.id = id
self.title = title
self.isEnabled = isEnabled
self.isLoading = isLoading
self.role = role
self.action = action
/// Confirmation dialog configuration.
@MainActor
public struct Confirmation {

/// Confirmation title. Use empty string to hide title.
public let title: String

/// Message. Use empty string to hide message.
public let message: String?

/// Button that confirms action.
public let confirmButtonTitle: String

/// Button that aborts action.
public let cancelButtonTitle: String

/// Action to invoke when confirmation appears.
public let onAppear: (() -> Void)?

public init(
title: String,
message: String?,
confirmButtonTitle: String,
cancelButtonTitle: String,
onAppear: (() -> Void)?
) {
self.title = title
self.message = message
self.confirmButtonTitle = confirmButtonTitle
self.cancelButtonTitle = cancelButtonTitle
self.onAppear = onAppear
}
}

public let id: String
/// Identifier.
public nonisolated let id: String

/// Action title.
public let title: String

/// Icon view.
public let icon: AnyView?

/// Boolean value indicating whether action is enabled.
public let isEnabled: Bool

Expand All @@ -41,6 +64,30 @@ public struct POButtonViewModel: Identifiable {
/// A value that describes the purpose of a button.
public let role: POButtonRole?

/// Confirmation dialog to present to user before invoking action.
public let confirmation: Confirmation?

/// Action handler.
public let action: () -> Void
public let action: @MainActor () -> Void

/// Creates view model with given parameters.
public init(
id: String,
title: String,
icon: AnyView? = nil,
isEnabled: Bool = true,
isLoading: Bool = false,
role: POButtonRole? = nil,
confirmation: Confirmation? = nil,
action: @escaping @MainActor () -> Void
) {
self.id = id
self.title = title
self.icon = icon
self.isEnabled = isEnabled
self.isLoading = isLoading
self.role = role
self.confirmation = confirmation
self.action = action
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,59 @@

import SwiftUI

extension View {
extension Button where Label == AnyView {

/// Configures a button view using provided view model.
///
/// - Parameters:
/// - viewModel: The view model containing button properties.
/// - styleProvider: An object that provides the button style based on the role.
@_spi(PO)
@ViewBuilder
public func buttonViewModel(_ viewModel: POButtonViewModel) -> some View {
self
@available(iOS 14, *)
public static func create(with viewModel: POButtonViewModel) -> some View {
ButtonWrapper(viewModel: viewModel)
}
}

@available(iOS 14, *)
private struct ButtonWrapper: View {

let viewModel: POButtonViewModel

// MARK: - View

var body: some View {
Button(action: action, label: { buttonLabel })
.accessibility(identifier: viewModel.id)
.disabled(!viewModel.isEnabled)
.buttonLoading(viewModel.isLoading)
.poButtonRole(viewModel.role)
.accessibility(identifier: viewModel.id)
.poConfirmationDialog(item: $confirmationDialog)
}

// MARK: - Private Properties

@State
private var confirmationDialog: POConfirmationDialog?

// MARK: - Private Methods

private var buttonLabel: Label<Text, some View> {
Label {
Text(viewModel.title)
} icon: {
viewModel.icon
}
}

private func action() {
if let confirmation = viewModel.confirmation {
confirmation.onAppear?()
confirmationDialog = .init(
title: confirmation.title,
message: confirmation.message,
primaryButton: .init(
title: confirmation.confirmButtonTitle, action: viewModel.action
),
secondaryButton: .init(title: confirmation.cancelButtonTitle, role: .cancel)
)
} else {
viewModel.action()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// POTextFieldViewModel.swift
// ProcessOut
//
// Created by Andrii Vysotskyi on 04.11.2024.
//

import SwiftUI

@_spi(PO)
public struct POTextFieldViewModel: Identifiable {

/// Item identifier.
public let id: AnyHashable

/// Current parameter's value text.
@Binding
public var value: String

/// Parameter's placeholder.
public let placeholder: String

/// Input icon.
public let icon: AnyView?

/// Boolean value indicating whether value is valid.
public let isInvalid: Bool

/// Boolean value indicating whether input is currently enabled.
public let isEnabled: Bool

/// Formatter to use to format value if any.
public let formatter: Formatter?

/// Keyboard type.
public let keyboard: UIKeyboardType

/// Text content type.
public let contentType: UITextContentType?

/// Submit label.
public let submitLabel: POBackport<Any>.SubmitLabel

/// Action to perform when the user submits a value to this input.
public let onSubmit: () -> Void

public init(
id: AnyHashable,
value: Binding<String>,
placeholder: String,
icon: AnyView?,
isInvalid: Bool,
isEnabled: Bool,
formatter: Formatter?,
keyboard: UIKeyboardType,
contentType: UITextContentType?,
submitLabel: POBackport<Any>.SubmitLabel,
onSubmit: @escaping () -> Void
) {
self.id = id
self._value = value
self.placeholder = placeholder
self.icon = icon
self.isInvalid = isInvalid
self.isEnabled = isEnabled
self.formatter = formatter
self.keyboard = keyboard
self.contentType = contentType
self.submitLabel = submitLabel
self.onSubmit = onSubmit
}
}
Loading

0 comments on commit abb8616

Please sign in to comment.