Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add apple pay integration #312

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions ExampleApp/ExampleApp.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
10 changes: 10 additions & 0 deletions ExampleApp/ExampleAppDebug.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.in-app-payments</key>
<array>
<string>merchant.your_merchant_id</string>
</array>
</dict>
</plist>
2 changes: 2 additions & 0 deletions ExampleApp/Models/LocalConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ struct LocalConfig: Codable {
var devMode: Bool {
devVaultBaseURL != nil && devApiBaseURL != nil
}

var merchantId: String = ""

init() {
devVaultBaseURL = nil
Expand Down
4 changes: 4 additions & 0 deletions ExampleApp/Views/ProductDetailViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ class ProductDetailViewController: BaseViewController {
publicKey: LocalConfig.default.publicKey,
configuration: LocalConfig.default.configuration
)

// Setup merchant ID for ApplePay
omiseSDK.setupApplePay(for: LocalConfig.default.merchantId, requiredBillingAddress: true)

// Setup shared instance to use from other screens if required
OmiseSDK.shared = omiseSDK
return omiseSDK
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Apple_Pay_Mark_RGB_041619-3.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
4 changes: 4 additions & 0 deletions OmiseSDK/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// Source Types
"SourceType.alipay" = "Alipay Online";
"SourceType.alipay.footnote" = "(Alipay+™ Partner)";
"SourceType.applepay" = "Apple Pay";
"SourceType.bill_payment_tesco_lotus" = "Lotus's Bill Payment";
"SourceType.installment_bay" = "Krungsri";
"SourceType.installment_bbl" = "Bangkok Bank";
Expand Down Expand Up @@ -126,3 +127,6 @@

// Authorizing Payment
"AuthorizingPayment.title" = "Authorizing Payment";

// Apple Pay
"ApplePay.not.available.text" = "Apple Pay is not available on this device";
4 changes: 4 additions & 0 deletions OmiseSDK/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// Source Types
"SourceType.alipay" = "アリペイオンライン";
"SourceType.alipay.footnote" = "(アリペイプラス)";
"SourceType.applepay" = "Apple Pay";
"SourceType.bill_payment_tesco_lotus" = "ロータス";
"SourceType.installment_bay" = "クルンシィ";
"SourceType.installment_bbl" = "バンコク銀行";
Expand Down Expand Up @@ -126,3 +127,6 @@

// Authorizing Payment
"AuthorizingPayment.title" = "決済の認可";

// Apple Pay
"ApplePay.not.available.text" = "この端末ではApple Payはご利用いただけません";
4 changes: 4 additions & 0 deletions OmiseSDK/Resources/th.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// Source Types
"SourceType.alipay" = "อาลีเพย์ (ออนไลน์)";
"SourceType.alipay.footnote" = "(อาลีเพย์พลัส)";
"SourceType.applepay" = "Apple Pay";
"SourceType.bill_payment_tesco_lotus" = "โลตัสบิลเพย์เมนต์";
"SourceType.installment_bay" = "ธนาคารกรุงศรี";
"SourceType.installment_bbl" = "ธนาคารกรุงเทพ";
Expand Down Expand Up @@ -123,3 +124,6 @@

// Authorizing Payment
"AuthorizingPayment.title" = "กำลังอนุมัติวงเงิน";

// Apple Pay
"ApplePay.not.available.text" = "ไม่สามารถใช้งาน Apple Pay ได้บนอุปกรณ์นี้";
5 changes: 5 additions & 0 deletions OmiseSDK/Sources/ClientProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ public protocol ClientProtocol {
/// - parameter completion: Returns `Token`object on success and `Error` on request failed
func createToken(payload: CreateTokenPayload, _ completion: @escaping ResponseClosure<Token, Error>)

/// Sends `Create a Token` API request with return ApplePay's PKPayment token
/// - parameter applePayToken: Information required to perform Create a Token API request with ApplePay token Payload
/// - parameter completion: Returns `Token`object on success and `Error` on request failed
func createToken(applePayToken: CreateTokenApplePayPayload, _ completion: @escaping ResponseClosure<Token, Error>)

/// Requests a `Token` from API with given Token ID
/// - parameter tokenID: Token identifier
/// - parameter completion: Returns `Token`object on success and `Error` on request failed
Expand Down
4 changes: 4 additions & 0 deletions OmiseSDK/Sources/OmiseAPI/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ class Client: ClientProtocol {
func createToken(payload: CreateTokenPayload, _ completion: @escaping ResponseClosure<Token, Error>) {
performRequest(api: OmiseAPI.createToken(payload: payload), completion: completion)
}

func createToken(applePayToken: CreateTokenApplePayPayload, _ completion: @escaping ResponseClosure<Token, Error>) {
performRequest(api: OmiseAPI.createTokenFromApplePayToken(token: applePayToken), completion: completion)
}

/// Requests a `Token` from API with given Token ID
/// - parameter tokenID: Token identifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public struct Capability {
public let countryCode: String
public let paymentMethods: [Capability.PaymentMethod]
public let banks: Set<String>
public let tokenizationMethods: Set<String>
}

extension Capability {
Expand Down Expand Up @@ -34,5 +35,6 @@ extension Capability: Codable {
case countryCode = "country"
case paymentMethods = "payment_methods"
case banks
case tokenizationMethods = "tokenization_methods"
}
}
6 changes: 6 additions & 0 deletions OmiseSDK/Sources/OmiseAPI/JSON Models/Token/Token.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,9 @@ extension Token: Decodable {
case chargeStatus = "charge_status"
}
}

extension Token: Equatable {
public static func == (lhs: Token, rhs: Token) -> Bool {
lhs.id == rhs.id
}
}
9 changes: 6 additions & 3 deletions OmiseSDK/Sources/OmiseAPI/OmiseAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum OmiseAPI {
case capability
case token(tokenID: String)
case createToken(payload: CreateTokenPayload)
case createTokenFromApplePayToken(token: CreateTokenApplePayPayload)
case createSource(payload: CreateSourcePayload)
case netceteraConfig(baseUrl: URL)
}
Expand All @@ -38,7 +39,7 @@ extension OmiseAPI: APIProtocol {
switch self {
case .capability, .createSource:
return .api
case .token, .createToken:
case .token, .createToken, .createTokenFromApplePayToken:
return .vault
case .netceteraConfig(let baseURL):
return .other(baseURL: baseURL)
Expand All @@ -51,7 +52,7 @@ extension OmiseAPI: APIProtocol {
return "capability"
case .token(let tokenID):
return "tokens/\(tokenID)"
case .createToken:
case .createToken, .createTokenFromApplePayToken:
return "tokens"
case .createSource:
return "sources"
Expand All @@ -68,7 +69,7 @@ extension OmiseAPI: APIProtocol {
switch self {
case .capability, .token:
return .get
case .createToken, .createSource:
case .createToken, .createSource, .createTokenFromApplePayToken:
return .post
case .netceteraConfig:
return .get
Expand All @@ -83,6 +84,8 @@ extension OmiseAPI: APIProtocol {
switch self {
case .createToken(let payload):
return try? jsonEncoder.encode(payload)
case .createTokenFromApplePayToken(let payload):
return try? jsonEncoder.encode(payload)
case .createSource(let sourcePayment):
return try? jsonEncoder.encode(sourcePayment)
default:
Expand Down
127 changes: 127 additions & 0 deletions OmiseSDK/Sources/OmiseAPI/Payloads/CreateTokenApplePayPayload.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import Foundation
import PassKit

/// Payload to create a token using Apple Pay data.
/// This structure is used as the request body in Omise's createToken API.
/// See: https://docs.opn.ooo/tokens
public struct CreateTokenApplePayPayload: Codable, Equatable {
public static func == (lhs: CreateTokenApplePayPayload, rhs: CreateTokenApplePayPayload) -> Bool {
lhs.tokenization.merchantId == rhs.tokenization.merchantId &&
lhs.tokenization.method == rhs.tokenization.method &&
lhs.tokenization.data == rhs.tokenization.data
}

/// The tokenization information required to create a token.
var tokenization: Tokenization

/// The tokenization data for Apple Pay.
/// This nested structure includes the payment method, payment data, merchant details,
/// and optionally billing address information.
public struct Tokenization: Codable {
/// The method used for tokenization (e.g., "applepay").
let method: String
/// The payment token data as a string.
let data: String
/// The merchant identifier registered for Apple Pay.
let merchantId: String
/// The card brand (e.g., "Visa", "AmEx").
let brand: String

/// Optional billing name.
var billingName: String?
/// Optional billing city.
var billingCity: String?
/// Optional billing country.
var billingCountry: String?
/// Optional billing postal code.
var billingPostalCode: String?
/// Optional billing state.
var billingState: String?
/// Optional billing street line 1.
var billingStreet1: String?
/// Optional billing street line 2.
var billingStreet2: String?
/// Optional billing phone number.
var billingPhoneNumber: String?

/// Initializes a new tokenization object with required and optional billing parameters.
///
/// - Parameters:
/// - method: The tokenization method (e.g., "applepay").
/// - data: The payment token data as a string.
/// - merchantId: The merchant identifier.
/// - brand: The card brand.
/// - billingName: The billing name (optional).
/// - billingCity: The billing city (optional).
/// - billingCountry: The billing country (optional).
/// - billingPostalCode: The billing postal code (optional).
/// - billingState: The billing state (optional).
/// - billingStreet1: The first line of the billing street (optional).
/// - billingStreet2: The second line of the billing street (optional).
/// - billingPhoneNumber: The billing phone number (optional).
public init(
method: String,
data: String,
merchantId: String,
brand: String,
billingName: String? = nil,
billingCity: String? = nil,
billingCountry: String? = nil,
billingPostalCode: String? = nil,
billingState: String? = nil,
billingStreet1: String? = nil,
billingStreet2: String? = nil,
billingPhoneNumber: String? = nil
) {
self.method = method
self.data = data
self.merchantId = merchantId
self.brand = brand
self.billingName = billingName
self.billingCity = billingCity
self.billingCountry = billingCountry
self.billingPostalCode = billingPostalCode
self.billingState = billingState
self.billingStreet1 = billingStreet1
self.billingStreet2 = billingStreet2
self.billingPhoneNumber = billingPhoneNumber
}

// swiftlint:disable:next function_parameter_count
mutating func addAddressDetails(
billingName: String?,
billingCity: String?,
billingCountry: String?,
billingPostalCode: String?,
billingState: String?,
billingStreet1: String?,
billingStreet2: String? = nil,
billingPhoneNumber: String?
) {
self.billingName = billingName
self.billingCity = billingCity
self.billingCountry = billingCountry
self.billingPostalCode = billingPostalCode
self.billingState = billingState
self.billingStreet1 = billingStreet1
self.billingStreet2 = billingStreet2
self.billingPhoneNumber = billingPhoneNumber
}

/// Maps the Swift property names to JSON keys.
enum CodingKeys: String, CodingKey {
case method
case data
case merchantId = "merchant_id"
case brand
case billingName = "billing_name"
case billingCity = "billing_city"
case billingCountry = "billing_country"
case billingPostalCode = "billing_postal_code"
case billingState = "billing_state"
case billingStreet1 = "billing_street1"
case billingStreet2 = "billing_street2"
case billingPhoneNumber = "billing_phone_number"
}
}
}
2 changes: 2 additions & 0 deletions OmiseSDK/Sources/OmiseAPI/SourceType/SourceType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public enum SourceType: String, Codable, CaseIterable {
case alipayHK = "alipay_hk"
/// Atome App Redirection https://docs.opn.ooo/atome
case atome = "atome"
/// ApplePay https://docs.opn.ooo/applepay
case applePay = "applepay"
/// Alipay In-Store https://docs.opn.ooo/alipay-barcode
case barcodeAlipay = "barcode_alipay" // Not available to select in UI
/// Lotus's Bill Payment https://docs.opn.ooo/bill-payment
Expand Down
9 changes: 9 additions & 0 deletions OmiseSDK/Sources/OmiseSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class OmiseSDK {
Country(code: latestLoadedCapability?.countryCode)
}

private(set) var applePayInfo: ApplePayInfo?

public private(set) weak var presentedViewController: UIViewController?

private var expectedReturnURLStrings: [String] = []
Expand Down Expand Up @@ -75,6 +77,7 @@ public class OmiseSDK {
amount: amount,
currency: currency,
currentCountry: country,
applePayInfo: self.applePayInfo,
handleErrors: handleErrors
)

Expand Down Expand Up @@ -121,6 +124,7 @@ public class OmiseSDK {
amount: 0,
currency: "",
currentCountry: Country(code: countryCode) ?? self.country,
applePayInfo: applePayInfo,
handleErrors: handleErrors
)
let viewController = paymentFlow.createCreditCardPaymentController(delegate: delegate)
Expand Down Expand Up @@ -215,6 +219,11 @@ public class OmiseSDK {

return containsURL
}

public func setupApplePay(for merchantId: String, requiredBillingAddress: Bool = false) {
self.applePayInfo = ApplePayInfo(merchantIdentifier: merchantId,
requestBillingAddress: requiredBillingAddress)
}
}

private extension OmiseSDK {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ extension ChoosePaymentCoordinator {
completion()
}
}

func processPayment(applePay payload: CreateTokenApplePayPayload?, completion: @escaping () -> Void) {
guard let delegate = choosePaymentMethodDelegate,
let payload = payload else { return }

client.createToken(applePayToken: payload) { [weak self, weak delegate] result in
switch result {
case .success(let token):
delegate?.choosePaymentMethodDidComplete(with: token)
case .failure(let error):
self?.processError(error)
}
completion()
}
}

func processWhiteLabelInstallmentPayment(_ payment: Source.Payment, card: CreateTokenPayload.Card, completion: @escaping () -> Void) {

Expand Down
Loading
Loading