๐Ÿ’ณ PassKit & Apple Pay

์ง€๊ฐ‘ ์•ฑ ํ†ตํ•ฉ๊ณผ Apple Pay ๊ฒฐ์ œ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ํƒญ ํ•œ ๋ฒˆ์œผ๋กœ ๊ฒฐ์ œ ์™„๋ฃŒ!

โœจ PassKit์ด๋ž€?

PassKit์€ ๋‘ ๊ฐ€์ง€ ์ฃผ์š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค: (1) Wallet Pass ์ƒ์„ฑ (๋ฉค๋ฒ„์‹ญ ์นด๋“œ, ์ฟ ํฐ, ํ‹ฐ์ผ“ ๋“ฑ), (2) Apple Pay ๊ฒฐ์ œ ์ฒ˜๋ฆฌ

๐Ÿ’ณ Apple Pay ๊ฒฐ์ œ ๊ตฌํ˜„

ApplePayManager.swift
import PassKit

class ApplePayManager: NSObject, PKPaymentAuthorizationViewControllerDelegate {
    static let shared = ApplePayManager()

    // Apple Pay ์ง€์› ์—ฌ๋ถ€ ํ™•์ธ
    func canMakePayments() -> Bool {
        return PKPaymentAuthorizationViewController.canMakePayments()
    }

    // ๊ฒฐ์ œ ์š”์ฒญ ์ƒ์„ฑ
    func createPaymentRequest() -> PKPaymentRequest {
        let request = PKPaymentRequest()
        request.merchantIdentifier = "merchant.com.yourcompany.app"
        request.supportedNetworks = [.visa, .masterCard, .amex]
        request.merchantCapabilities = .capability3DS
        request.countryCode = "KR"
        request.currencyCode = "KRW"

        // ๊ฒฐ์ œ ํ•ญ๋ชฉ
        let item = PKPaymentSummaryItem(
            label: "ํ”„๋ฆฌ๋ฏธ์—„ ๋ฉค๋ฒ„์‹ญ",
            amount: NSDecimalNumber(value: 9900)
        )

        request.paymentSummaryItems = [item]
        return request
    }

    // ๊ฒฐ์ œ ์‹œํŠธ ํ‘œ์‹œ
    func startPayment(from viewController: UIViewController) {
        let request = createPaymentRequest()
        guard let paymentVC = PKPaymentAuthorizationViewController(paymentRequest: request) else {
            return
        }

        paymentVC.delegate = self
        viewController.present(paymentVC, animated: true)
    }

    // ๊ฒฐ์ œ ์™„๋ฃŒ ์ฒ˜๋ฆฌ
    func paymentAuthorizationViewController(
        _ controller: PKPaymentAuthorizationViewController,
        didAuthorizePayment payment: PKPayment,
        handler completion: @escaping (PKPaymentAuthorizationResult) -> Void
    ) {
        // ๋ฐฑ์—”๋“œ๋กœ payment.token ์ „์†ก
        processPayment(payment.token) { success in
            if success {
                completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
            } else {
                completion(PKPaymentAuthorizationResult(status: .failure, errors: nil))
            }
        }
    }

    func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
        controller.dismiss(animated: true)
    }

    func processPayment(_ token: PKPaymentToken, completion: @escaping (Bool) -> Void) {
        // ์‹ค์ œ ๊ฒฐ์ œ ์ฒ˜๋ฆฌ ๋กœ์ง
        completion(true)
    }
}

๐ŸŽจ SwiftUI์—์„œ Apple Pay ๋ฒ„ํŠผ

ApplePayButton.swift
import SwiftUI
import PassKit

struct ApplePayButtonView: UIViewRepresentable {
    var action: () -> Void

    func makeUIView(context: Context) -> PKPaymentButton {
        let button = PKPaymentButton(
            paymentButtonType: .buy,
            paymentButtonStyle: .black
        )
        button.addTarget(
            context.coordinator,
            action: #selector(Coordinator.buttonTapped),
            for: .touchUpInside
        )
        return button
    }

    func updateUIView(_ uiView: PKPaymentButton, context: Context) {}

    func makeCoordinator() -> Coordinator {
        Coordinator(action: action)
    }

    class Coordinator: NSObject {
        var action: () -> Void

        init(action: @escaping () -> Void) {
            self.action = action
        }

        @objc func buttonTapped() {
            action()
        }
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
struct CheckoutView: View {
    var body: some View {
        VStack {
            Text("์ด 9,900์›")
                .font(.title)

            ApplePayButtonView {
                // Apple Pay ๊ฒฐ์ œ ์‹œ์ž‘
                ApplePayManager.shared.startPayment(from: UIApplication.shared.windows.first!.rootViewController!)
            }
            .frame(height: 50)
            .padding()
        }
    }
}

๐ŸŽซ Wallet Pass ์ƒ์„ฑ

๋ฉค๋ฒ„์‹ญ ์นด๋“œ, ์ฟ ํฐ, ํ‹ฐ์ผ“ ๋“ฑ์„ Wallet ์•ฑ์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

CreatePass.swift
import PassKit

func addPassToWallet(passURL: URL) {
    guard let passData = try? Data(contentsOf: passURL) else { return }
    guard let pass = try? PKPass(data: passData) else { return }

    let addPassVC = PKAddPassesViewController(pass: pass)
    // present addPassVC
}

// Pass ์—…๋ฐ์ดํŠธ ํ‘ธ์‹œ ์•Œ๋ฆผ
func sendPassUpdateNotification(passTypeIdentifier: String, serialNumber: String) {
    // ์„œ๋ฒ„์—์„œ Apple Push Notification ์ „์†ก
    // ์‚ฌ์šฉ์ž Wallet์˜ Pass๊ฐ€ ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธ๋จ
}

๐Ÿ“ฑ Pass Library ์ ‘๊ทผ

PassLibrary.swift
import PassKit

class PassManager {
    let library = PKPassLibrary()

    // ์‚ฌ์šฉ์ž์˜ Wallet์— ํŠน์ • Pass๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ
    func hasPass(passTypeIdentifier: String, serialNumber: String) -> Bool {
        return library.containsPass(
            withPassTypeIdentifier: passTypeIdentifier,
            serialNumber: serialNumber
        )
    }

    // ํŠน์ • Pass ๊ฐ€์ ธ์˜ค๊ธฐ
    func getPass(passTypeIdentifier: String, serialNumber: String) -> PKPass? {
        return library.pass(
            withPassTypeIdentifier: passTypeIdentifier,
            serialNumber: serialNumber
        )
    }

    // ๋ชจ๋“  Pass ๊ฐ€์ ธ์˜ค๊ธฐ
    func getAllPasses() -> [PKPass] {
        return library.passes
    }

    // Pass ์‚ญ์ œ
    func removePass(_ pass: PKPass) {
        library.removePass(pass)
    }
}

๐Ÿ”„ Pass ์—…๋ฐ์ดํŠธ ๊ฐ์ง€

PassNotifications.swift
import PassKit

class PassObserver {
    let library = PKPassLibrary()

    func observePassChanges() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(passLibraryDidChange),
            name: .PKPassLibraryDidChange,
            object: library
        )

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(passLibraryRemoteDidChange),
            name: .PKPassLibraryRemotePaymentPassesDidChange,
            object: library
        )
    }

    @objc func passLibraryDidChange() {
        print("Pass ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ณ€๊ฒฝ๋จ")
        // UI ์—…๋ฐ์ดํŠธ
    }

    @objc func passLibraryRemoteDidChange() {
        print("์›๊ฒฉ ๊ฒฐ์ œ Pass ๋ณ€๊ฒฝ๋จ")
    }
}

๐Ÿ’ฐ ๋ฐฐ์†ก ์ •๋ณด ์š”์ฒญ

ShippingMethods.swift
func createPaymentRequest() -> PKPaymentRequest {
    let request = PKPaymentRequest()
    // ... ๊ธฐ๋ณธ ์„ค์ •

    // ๋ฐฐ์†ก ์ •๋ณด ์š”์ฒญ
    request.requiredShippingContactFields = [.name, .postalAddress, .phoneNumber]

    // ๋ฐฐ์†ก ๋ฐฉ๋ฒ•
    let standard = PKShippingMethod(
        label: "์ผ๋ฐ˜ ๋ฐฐ์†ก",
        amount: NSDecimalNumber(value: 3000)
    )
    standard.identifier = "standard"
    standard.detail = "3-5์ผ ์†Œ์š”"

    let express = PKShippingMethod(
        label: "๋น ๋ฅธ ๋ฐฐ์†ก",
        amount: NSDecimalNumber(value: 5000)
    )
    express.identifier = "express"
    express.detail = "1-2์ผ ์†Œ์š”"

    request.shippingMethods = [standard, express]

    return request
}

// ๋ฐฐ์†ก ์ •๋ณด ๋ณ€๊ฒฝ ์‹œ
func paymentAuthorizationViewController(
    _ controller: PKPaymentAuthorizationViewController,
    didSelect shippingMethod: PKShippingMethod,
    handler completion: @escaping (PKPaymentRequestShippingMethodUpdate) -> Void
) {
    // ๋ฐฐ์†ก ๋ฐฉ๋ฒ•์— ๋”ฐ๋ผ ์ด์•ก ์—…๋ฐ์ดํŠธ
    let item = PKPaymentSummaryItem(label: "์ƒํ’ˆ", amount: 9900)
    let shipping = PKPaymentSummaryItem(label: shippingMethod.label, amount: shippingMethod.amount)
    let total = PKPaymentSummaryItem(
        label: "์ด ํ•ฉ๊ณ„",
        amount: item.amount.adding(shipping.amount)
    )

    let update = PKPaymentRequestShippingMethodUpdate(paymentSummaryItems: [item, shipping, total])
    completion(update)
}

๐ŸŽฏ ์ฟ ํฐ ์ ์šฉ

CouponCode.swift
func createPaymentRequest() -> PKPaymentRequest {
    let request = PKPaymentRequest()
    // ... ๊ธฐ๋ณธ ์„ค์ •

    // ์ฟ ํฐ ์ฝ”๋“œ ์ž…๋ ฅ ์ง€์› (iOS 15+)
    request.supportsCouponCode = true

    return request
}

func paymentAuthorizationViewController(
    _ controller: PKPaymentAuthorizationViewController,
    didChangeCouponCode couponCode: String,
    handler completion: @escaping (PKPaymentRequestCouponCodeUpdate) -> Void
) {
    // ์ฟ ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
    if couponCode == "WELCOME10" {
        let discount = PKPaymentSummaryItem(
            label: "ํ• ์ธ (-10%)",
            amount: NSDecimalNumber(value: -990)
        )
        let item = PKPaymentSummaryItem(label: "์ƒํ’ˆ", amount: 9900)
        let total = PKPaymentSummaryItem(label: "์ด ํ•ฉ๊ณ„", amount: 8910)

        let update = PKPaymentRequestCouponCodeUpdate(
            paymentSummaryItems: [item, discount, total]
        )
        completion(update)
    } else {
        let error = PKPaymentRequest.Error(
            .couponCodeInvalid,
            localizedDescription: "์œ ํšจํ•˜์ง€ ์•Š์€ ์ฟ ํฐ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค"
        )
        let update = PKPaymentRequestCouponCodeUpdate(errors: [error], paymentSummaryItems: [], shippingMethods: [])
        completion(update)
    }
}

๐Ÿ’ก HIG ์ฒดํฌ๋ฆฌ์ŠคํŠธ
โœ… ๊ณต์‹ Apple Pay ๋ฒ„ํŠผ ์‚ฌ์šฉ (์ง์ ‘ ๋””์ž์ธ ๊ธˆ์ง€)
โœ… ๊ฒฐ์ œ ์ „ ์ด์•ก ๋ช…ํ™•ํžˆ ํ‘œ์‹œ
โœ… ๋ฐฐ์†ก๋น„, ์„ธ๊ธˆ ๋“ฑ ์ถ”๊ฐ€ ๋น„์šฉ ๋ช…์‹œ
โœ… ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋Š” ์‚ฌ์šฉ์ž ์นœํ™”์ ์œผ๋กœ
โœ… ๊ฒฐ์ œ ์™„๋ฃŒ ํ›„ ๋ช…ํ™•ํ•œ ํ”ผ๋“œ๋ฐฑ

๐Ÿ“ฆ ํ•™์Šต ์ž๋ฃŒ

๐Ÿ’ป
GitHub ํ”„๋กœ์ ํŠธ
๐ŸŽ
Apple HIG ์›๋ฌธ
๐Ÿ“–
Apple ๊ณต์‹ ๋ฌธ์„œ