Skip to content

Commit

Permalink
feature: Add Adyen Blik to Headless (#1005)
Browse files Browse the repository at this point in the history
Implement Adyen Blik on Headless

Co-authored-by: Boris Nikolic <boris.nikolic.dev@gmail.com>
  • Loading branch information
borisprimer and BorisNikolic authored Oct 3, 2024
1 parent 4253347 commit e6f6597
Show file tree
Hide file tree
Showing 8 changed files with 610 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,12 @@ extension MerchantHeadlessCheckoutAvailablePaymentMethodsViewController: UITable
break
#endif
default:
redirectManager = try? PrimerHeadlessUniversalCheckout.NativeUIManager(paymentMethodType: paymentMethodType)
try? redirectManager?.showPaymentMethod(intent: sessionIntent)
do {
redirectManager = try PrimerHeadlessUniversalCheckout.NativeUIManager(paymentMethodType: paymentMethodType)
try redirectManager?.showPaymentMethod(intent: sessionIntent)
} catch {
print("\n\nMERCHANT APP\n\(#function)\nerror: \(error)")
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ extension PrimerHeadlessUniversalCheckout {
case PrimerPaymentMethodType.xenditRetailOutlets.rawValue:
self.rawDataTokenizationBuilder = PrimerRawRetailerDataTokenizationBuilder(paymentMethodType: paymentMethodType)

case PrimerPaymentMethodType.adyenBlik.rawValue:
self.rawDataTokenizationBuilder = PrimerRawOTPDataTokenizationBuilder(paymentMethodType: paymentMethodType)

default:
let err = PrimerError.unsupportedPaymentMethod(paymentMethodType: paymentMethodType, userInfo: .errorUserInfoDictionary(),
diagnosticsId: UUID().uuidString)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
//
// PrimerRawOTPDataTokenizationBuilder.swift
// PrimerSDK
//
// Created by Boris on 26.9.24..
//

// swiftlint:disable function_body_length

import Foundation

class PrimerRawOTPDataTokenizationBuilder: PrimerRawDataTokenizationBuilderProtocol {

var rawData: PrimerRawData? {
didSet {
if let rawOTPInput = self.rawData as? PrimerOTPData {
rawOTPInput.onDataDidChange = {
_ = self.validateRawData(rawOTPInput)
}
}

if let rawData = self.rawData {
_ = self.validateRawData(rawData)
}
}
}

weak var rawDataManager: PrimerHeadlessUniversalCheckout.RawDataManager?
var isDataValid: Bool = false
var paymentMethodType: String
var delegate: PrimerHeadlessUniversalCheckoutRawDataManagerDelegate?

var requiredInputElementTypes: [PrimerInputElementType] {
[.otp]
}

required init(paymentMethodType: String) {
self.paymentMethodType = paymentMethodType
}

func configure(withRawDataManager rawDataManager: PrimerHeadlessUniversalCheckout.RawDataManager) {
self.rawDataManager = rawDataManager
}

func makeRequestBodyWithRawData(_ data: PrimerRawData) -> Promise<Request.Body.Tokenization> {
return Promise { seal in

guard let paymentMethod = PrimerPaymentMethod.getPaymentMethod(withType: paymentMethodType), let paymentMethodId = paymentMethod.id else {
let err = PrimerError.unsupportedPaymentMethod(paymentMethodType: paymentMethodType, userInfo: .errorUserInfoDictionary(),
diagnosticsId: UUID().uuidString)
ErrorHandler.handle(error: err)
seal.reject(err)
return
}

guard let rawData = data as? PrimerOTPData else {
let err = PrimerError.invalidValue(key: "rawData",
value: nil,
userInfo: .errorUserInfoDictionary(),
diagnosticsId: UUID().uuidString)
ErrorHandler.handle(error: err)
seal.reject(err)
return
}

let sessionInfo = BlikSessionInfo(blikCode: rawData.otp,
locale: PrimerSettings.current.localeData.localeCode)

let paymentInstrument = OffSessionPaymentInstrument(
paymentMethodConfigId: paymentMethodId,
paymentMethodType: paymentMethodType,
sessionInfo: sessionInfo)

let requestBody = Request.Body.Tokenization(paymentInstrument: paymentInstrument)
seal.fulfill(requestBody)
}
}

func validateRawData(_ data: PrimerRawData) -> Promise<Void> {
return Promise { seal in
DispatchQueue.global(qos: .userInteractive).async {
var errors: [PrimerValidationError] = []

guard let rawData = data as? PrimerOTPData else {
let err = PrimerValidationError.invalidRawData(
userInfo: .errorUserInfoDictionary(),
diagnosticsId: UUID().uuidString)
errors.append(err)
ErrorHandler.handle(error: err)

self.isDataValid = false

DispatchQueue.main.async {
if let rawDataManager = self.rawDataManager {
self.rawDataManager?.delegate?.primerRawDataManager?(rawDataManager,
dataIsValid: self.isDataValid,
errors: errors.count == 0 ? nil : errors)
}

seal.reject(err)
}
return
}

if !rawData.otp.isValidOTP {
errors.append(PrimerValidationError.invalidOTPCode(
message: "OTP is not valid.",
userInfo: .errorUserInfoDictionary(),
diagnosticsId: UUID().uuidString))
}

if !errors.isEmpty {
let err = PrimerError.underlyingErrors(
errors: errors,
userInfo: .errorUserInfoDictionary(),
diagnosticsId: UUID().uuidString)
ErrorHandler.handle(error: err)

self.isDataValid = false

DispatchQueue.main.async {
if let rawDataManager = self.rawDataManager {
self.rawDataManager?.delegate?.primerRawDataManager?(rawDataManager,
dataIsValid: self.isDataValid,
errors: errors.count == 0 ? nil : errors)
}

seal.reject(err)
}
} else {
self.isDataValid = true

DispatchQueue.main.async {
if let rawDataManager = self.rawDataManager {
self.rawDataManager?.delegate?.primerRawDataManager?(rawDataManager,
dataIsValid: self.isDataValid,
errors: errors.count == 0 ? nil : errors)
}

seal.fulfill()
}
}
}
}
}
}
// swiftlint:enable function_body_length
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,10 @@ class PrimerPaymentMethod: Codable, LogReporter {
case .stripeAch:
categories.append(PrimerPaymentMethodManagerCategory.nativeUI)

default:
break
case .adyenBlik:
categories.append(PrimerPaymentMethodManagerCategory.rawData)

default: break
}

return categories.isEmpty ? nil : categories
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// PrimerOTPData.swift
// PrimerSDK
//
// Created by Boris on 26.9.24..
//

import Foundation

public class PrimerOTPData: PrimerRawData {

public var otp: String {
didSet {
self.onDataDidChange?()
}
}

private enum CodingKeys: String, CodingKey {
case otp
}

public required init(
otp: String
) {
self.otp = otp
super.init()
}
}
43 changes: 43 additions & 0 deletions Tests/Primer/Data Models/PrimerPaymentMethodTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,49 @@ final class PrimerPaymentMethodTests: XCTestCase {
XCTAssertEqual(paymentMethod.logo!, UIImage(systemName: "sun.min"))
}

func testInvertedLogo_NoImage() {
XCTAssertNil(paymentMethod.invertedLogo)
}

func testInvertedLogo_NilImages() {
paymentMethod.baseLogoImage = PrimerTheme.BaseImage(colored: nil, light: nil, dark: nil)
XCTAssertNil(paymentMethod.invertedLogo)
}

func testInvertedLogo_ReturnsCorrectImage() {
// Given
paymentMethod.baseLogoImage = PrimerTheme.BaseImage(
colored: UIImage(systemName: "colored"),
light: UIImage(systemName: "light"),
dark: UIImage(systemName: "dark")
)
let isDarkModeEnabled = UIScreen.isDarkModeEnabled

// When
let invertedLogo = paymentMethod.invertedLogo

// Then
if isDarkModeEnabled {
// When dark mode is enabled, invertedLogo should return the light image if available
if let lightImage = paymentMethod.baseLogoImage?.light {
XCTAssertEqual(invertedLogo, lightImage)
} else if let coloredImage = paymentMethod.baseLogoImage?.colored {
XCTAssertEqual(invertedLogo, coloredImage)
} else {
XCTAssertNil(invertedLogo)
}
} else {
// When dark mode is disabled, invertedLogo should return the dark image if available
if let darkImage = paymentMethod.baseLogoImage?.dark {
XCTAssertEqual(invertedLogo, darkImage)
} else if let coloredImage = paymentMethod.baseLogoImage?.colored {
XCTAssertEqual(invertedLogo, coloredImage)
} else {
XCTAssertNil(invertedLogo)
}
}
}

func testViewModels() {
let nativeSDKPaymentMethod = createPaymentMethod(withImplementationType: .webRedirect)
XCTAssertTrue(nativeSDKPaymentMethod.tokenizationViewModel is WebRedirectPaymentMethodTokenizationViewModel)
Expand Down
47 changes: 47 additions & 0 deletions Tests/Primer/Managers/PrimerOTPDataTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// PrimerOTPDataTests.swift
// PrimerSDK
//
// Created by Boris on 1.10.24..
//

import XCTest
@testable import PrimerSDK

class PrimerOTPDataTests: XCTestCase {

// Test initialization with OTP
func test_initialization_with_otp() {
let otp = "123456"
let otpData = PrimerOTPData(otp: otp)
XCTAssertEqual(otpData.otp, otp, "OTP should be initialized correctly")
}

// Test that onDataDidChange is called when OTP is changed
func test_onDataDidChange_called_when_otp_changes() {
let otpData = PrimerOTPData(otp: "123456")
let exp = expectation(description: "onDataDidChange should be called")

otpData.onDataDidChange = {
exp.fulfill()
}

otpData.otp = "654321" // Change OTP to trigger onDataDidChange

wait(for: [exp], timeout: 1.0)
}

// Test that onDataDidChange is not nil
func test_onDataDidChange_is_not_nil_after_setting() {
let otpData = PrimerOTPData(otp: "123456")
otpData.onDataDidChange = {}

XCTAssertNotNil(otpData.onDataDidChange, "onDataDidChange should not be nil after setting")
}

// Test that onDataDidChange is nil by default
func test_onDataDidChange_is_nil_by_default() {
let otpData = PrimerOTPData(otp: "123456")
XCTAssertNil(otpData.onDataDidChange, "onDataDidChange should be nil by default")
}
}
Loading

0 comments on commit e6f6597

Please sign in to comment.