Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Example/Example.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
<dict>
<key>com.apple.developer.in-app-payments</key>
<array>
<string>merchant.com.payline.prod</string>
<string>merchant.homo.sdk.app.monext.com</string>
<string>merchant.homo.sdk.monext.com</string>
<string>merchant.prod.sdk.app.monext.com</string>
</array>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -26,110 +26,16 @@ struct CardForm: View {
var onFieldFocused: ((CardField?) -> Void)?

var body: some View {

VStack(spacing: 10) {

FormFieldView<CardField>(
label: "Card number",
textValue: $viewModel.cardNumber,
errorMessage: viewModel.cardNumberError,
formatter: CardNumberFormatter(),
keyboardType: .numberPad,
focusedState: $focusedField,
focusedField: .cardNumber
)
.id(CardField.cardNumber)

HStack {

if viewModel.showExpirationDate {
FormFieldView<CardField>(
label: "Expiry",
textValue: $viewModel.cardExpiration,
errorMessage: viewModel.cardExpirationError,
formatter: CardDateFormatter(),
keyboardType: .numberPad,
focusedState: $focusedField,
focusedField: .expiration
)
.id(CardField.expiration)
}

if viewModel.showCardCvv {
FormFieldView<CardField>(
label: "CVV",
textValue: $viewModel.cardCvv,
errorMessage: viewModel.cardCvvError,
formatter: CardCvvFormatter(),
keyboardType: .numberPad,
focusedState: $focusedField,
focusedField: .cvv,
onTappedInfoAccessory: {
isPresentedCvvInfo = true
}
)
.id(CardField.cvv)
.modifier(CvvInfoDialog(isPresented: $isPresentedCvvInfo))
}
}

if viewModel.showCardHolderName {
FormFieldView<CardField>(
label: "Name on card",
textValue: $viewModel.cardHolderName,
errorMessage: viewModel.cardHolderNameError,
formatter: nil,
focusedState: $focusedField,
focusedField: .holder
)
.id(CardField.holder)
.onSubmit {
focusedField = nil
}
.submitLabel(.done)
}

if let availableNetworks = viewModel.availableNetworks,
let defaultNetwork = availableNetworks.defaultCardNetwork,
let altNetwork = availableNetworks.altCardNetwork,
viewModel.showNetworkPicker {
CardNetworkSelector(
defaultNetwork: defaultNetwork,
altNetwork: altNetwork,
selectedNetwork: $viewModel.selectedNetwork
)
}

if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
CardNetworkSelector(
defaultNetwork: .init(network: "CB", code: "1"),
altNetwork: .init(network: "VISA", code: "2"),
selectedNetwork: .constant(.init(network: "CB", code: "1"))
)
}

if viewModel.showSaveCard {

ToggleButton(
"I want to save my credit card information for later.",
isOn: $viewModel.saveCard
)
}

cardNumberField
expirationAndCvvRow
holderField
networkPickerSection
saveCardToggle
CompliancyNotice()
}
.background(sessionStore.appearance.backgroundColor)
.toolbar {
if [CardField.cardNumber, CardField.expiration, CardField.cvv].contains(focusedField) {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button(action: nextFocus) {
Text("Next")
.foregroundStyle(colorScheme == .dark ? .white : .black)
}
}
}
}
.toolbar { keyboardToolbar }
.onChange(of: focusedField) { field in
viewModel.focusedField = field
onFieldFocused?(field)
Expand All @@ -138,20 +44,129 @@ struct CardForm: View {
focusedField = nil
}
.onChange(of: viewModel.selectedNetwork) { [oldValue = viewModel.selectedNetwork] _ in
if oldValue != nil {
focusedField = nil
}
if oldValue != nil { focusedField = nil }
}
.onChange(of: viewModel.formValid) { isValid in
formValid = isValid
}
.onChange(of: viewModel.cardExpiration) {
if DateFormatter.isValidCardExpiration($0) {
nextFocus()
if DateFormatter.isValidCardExpiration($0) { nextFocus() }
}
}

// MARK: - Subviews

private var cardNumberField: some View {
FormFieldView<CardField>(
label: "Card number",
textValue: $viewModel.cardNumber,
errorMessage: viewModel.cardNumberError,
formatter: CardNumberFormatter(),
keyboardType: .numberPad,
focusedState: $focusedField,
focusedField: .cardNumber,
isScreenshotProtected: true
)
.id(CardField.cardNumber)
}

private var expirationAndCvvRow: some View {
HStack {
if viewModel.showExpirationDate {
FormFieldView<CardField>(
label: "Expiry",
textValue: $viewModel.cardExpiration,
errorMessage: viewModel.cardExpirationError,
formatter: CardDateFormatter(),
keyboardType: .numberPad,
focusedState: $focusedField,
focusedField: .expiration,
isScreenshotProtected: true
)
.id(CardField.expiration)
}
if viewModel.showCardCvv {
FormFieldView<CardField>(
label: "CVV",
textValue: $viewModel.cardCvv,
errorMessage: viewModel.cardCvvError,
formatter: CardCvvFormatter(),
keyboardType: .numberPad,
focusedState: $focusedField,
focusedField: .cvv,
onTappedInfoAccessory: { isPresentedCvvInfo = true },
isScreenshotProtected: true
)
.id(CardField.cvv)
.modifier(CvvInfoDialog(isPresented: $isPresentedCvvInfo))
}
}
}

@ViewBuilder
private var holderField: some View {
if viewModel.showCardHolderName {
FormFieldView<CardField>(
label: "Name on card",
textValue: $viewModel.cardHolderName,
errorMessage: viewModel.cardHolderNameError,
formatter: nil,
focusedState: $focusedField,
focusedField: .holder,
isScreenshotProtected: true
)
.id(CardField.holder)
.onSubmit { focusedField = nil }
.submitLabel(.done)
}
}

@ViewBuilder
private var networkPickerSection: some View {
if let availableNetworks = viewModel.availableNetworks,
let defaultNetwork = availableNetworks.defaultCardNetwork,
let altNetwork = availableNetworks.altCardNetwork,
viewModel.showNetworkPicker {
CardNetworkSelector(
defaultNetwork: defaultNetwork,
altNetwork: altNetwork,
selectedNetwork: $viewModel.selectedNetwork
)
}
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
CardNetworkSelector(
defaultNetwork: .init(network: "CB", code: "1"),
altNetwork: .init(network: "VISA", code: "2"),
selectedNetwork: .constant(.init(network: "CB", code: "1"))
)
}
}

@ViewBuilder
private var saveCardToggle: some View {
if viewModel.showSaveCard {
ToggleButton(
"I want to save my credit card information for later.",
isOn: $viewModel.saveCard
)
}
}

@ToolbarContentBuilder
private var keyboardToolbar: some ToolbarContent {
if [CardField.cardNumber, CardField.expiration, CardField.cvv].contains(focusedField) {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button(action: nextFocus) {
Text("Next")
.foregroundStyle(colorScheme == .dark ? .white : .black)
}
}
}
}

// MARK: - Actions

private func nextFocus() {
guard let focusedField else { return }
self.focusedField = viewModel.nextFocus(focusedField)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ struct AlternativePaymentMethodForm: View {
keyboardType: getKeyboardType(for: field),
focusedState: $focusedField,
focusedField: field.id,
placeholder: field.placeholder
placeholder: field.placeholder,
isScreenshotProtected: field.secured ?? false
)
.padding(.vertical, 8)
default:
Expand Down
19 changes: 17 additions & 2 deletions Sources/Monext/Presentation/Views/FormField/FormFieldView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ struct FormFieldView<ID: Hashable>: View {

@EnvironmentObject var sessionStore: SessionStateStore


/// Active la protection contre les screenshots et screen recordings iOS natifs.
/// À utiliser sur les champs sensibles : PAN, CVV, etc.
var isScreenshotProtected: Bool = false
private var shouldProtectSensitiveData: Bool {
isScreenshotProtected && (sessionStore.env == .production || sessionStore.env == .sandbox)
}

private var config: Appearance {
sessionStore.appearance
}
Expand Down Expand Up @@ -117,6 +125,11 @@ struct FormFieldView<ID: Hashable>: View {
.tint(config.textfieldTextColor)
.keyboardType(keyboardType)
.textFieldStyle(MonextTextFieldStyle(config: config))
// Cache les champs uniquement en Production et Homologation
.privacySensitive(shouldProtectSensitiveData)
.if(shouldProtectSensitiveData) {
$0.screenshotProtected()
}

if onTappedInfoAccessory != nil {
Image(moduleImage: "ic.i.circle.filled")
Expand Down Expand Up @@ -191,7 +204,8 @@ struct FormFieldView<ID: Hashable>: View {
keyboardType: .numberPad,
focusedState: FocusState<CardField?>().projectedValue,
focusedField: .cardNumber,
placeholder: "0000 0000 0000 0000"
placeholder: "0000 0000 0000 0000",
isScreenshotProtected: true
)
.padding()

Expand All @@ -205,7 +219,8 @@ struct FormFieldView<ID: Hashable>: View {
keyboardType: .numberPad,
focusedState: FocusState<CardField?>().projectedValue,
focusedField: .cvv,
placeholder: "123"
placeholder: "123",
isScreenshotProtected: true
)
.padding()

Expand Down
Loading
Loading