🇺🇸 EN

💳 PassKit & Apple Pay

지갑 앱 통합과 Apple Pay 결제를 구현합니다. 탭 한 번으로 결제 완료!

⭐ 난이도: ⭐⭐⭐ ⏱️ 예상 시간: 2-3h 📂 App Services

✨ 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 공식 문서

📎 Apple 공식 자료

📘 공식 문서 🎬 WWDC 세션