yoomoney / yookassa-payments-swift

This library allows implementing payment acceptance into mobile apps on iOS and works as an extension to the YooMoney API
https://yookassa.ru/developers
MIT License
41 stars 41 forks source link

SwiftUI реализация #94

Open ssitdikov opened 3 years ago

ssitdikov commented 3 years ago

Планируется ли реализация для SwiftUI?

ask9rov commented 3 years ago

Презентуем ViewController через обычный sheet, используя UIViewControllerRepresentable. Ниже полный код с подтверждением 3дс. Много всяких биндингов, еле привел к работающему варианту. Где-то можно было и проще.

К слову о прямой реализации, пусть лучше хотя бы библиотеку в менеджер пакетов нормально интегрируют. На М1, последнем Xcode и SwiftUI завести все это дело было непросто.

//
//  PaymentScreen.swift
//  Dbz
//
//  Created by Elvin Askyarov on 18.02.2021.
//

import Foundation
import SwiftUI
import YooKassaPayments
import YooKassaPaymentsApi

struct PaymentScreen: View {
    var initAmount: Int
    @State var amount: Int = 0
    @State var showingPaymentSheet = false
    @State var status: Int = 0
    @State var url = ""
    @ObservedObject var textBindingManager = TextBindingManager(limit: 5)
    @State var vc: (UIViewController & TokenizationModuleInput)? = nil
    @Environment(\.presentationMode) var presentation

    init(amount: Int) {
        self.initAmount = amount
        textBindingManager.text = amount > 0 ? "\(amount)" : "500"
    }

    var body: some View {
        VStack {
            if (initAmount>0) {
                Text("Пополните баланс на недостающую сумму.")
                    .multilineTextAlignment(.center)
                    .font(.system(size: 17))
                    .foregroundColor(.secondary)
            }
            HStack(alignment: .center) {
                CustomTextField(text: $textBindingManager.text, isFirstResponder: true)
                    .keyboardType(.numberPad) .frame(width: 90, height: 40)
                .defaultTextFieldStyle()
                Text("₽")
                    .font(.system(size: 36))
                    .foregroundColor(.accentColor)
            }.padding(.top, 16)
            Text("Минимальная сумма пополнения 150 рублей")
                .multilineTextAlignment(.center)
                .padding(.horizontal, 32)
                .padding(.top, 12)
                .foregroundColor(.secondary)

            Spacer()
        }.sheet(isPresented: $showingPaymentSheet) {
            PaymentController(amount: $amount, vc: $vc, url: $url, showingPaymentSheet: $showingPaymentSheet)
        }.padding()
        .onChange(of: url, perform: { value in
            //если 3дс пустое, то закрываем и viewcontroller сдк и view оплаты, иначе запускаем 3дс
            if (url.isEmpty) {
                showingPaymentSheet = false
                dismiss()
            } else {
                vc?.start3dsProcess(requestUrl: value)
            }
        })
        .navigationBarTitle(Strings.PROFILE_TITLE_PAYMENT, displayMode: .inline)
        .navigationBarItems(trailing:
                                HStack {
                                    if (Int(textBindingManager.text) ?? 0>0) {
                                        Button(Strings.NEXT) {
                                                amount = Int(textBindingManager.text) ?? 0
                                                showingPaymentSheet = true
                                        }.tabBar()
                                    }
                                })
    }

    private func dismiss() {
        self.presentation.wrappedValue.dismiss()
    }
}

class PaymentOutput: TokenizationModuleOutput {
    var amount: Int
    var url: Binding<String>
    var showingPaymentSheet: Binding<Bool>

    init(amount: Int, url: Binding<String>, showingPaymentSheet: Binding<Bool>) {
        self.amount = amount
        self.url = url
        self.showingPaymentSheet = showingPaymentSheet
    }

    func set(amount: Int, url: Binding<String>, showingPaymentSheet: Binding<Bool>) {
        self.amount = amount
        self.url = url
        self.showingPaymentSheet = showingPaymentSheet
    }

    func tokenizationModule(_ module: TokenizationModuleInput,
                            didTokenize token: Tokens,
                            paymentMethodType: PaymentMethodType) {
        //отправка в апи токена для получения 3дс
        Api.get(
            message: .constant(nil),
            type: ConfirmPayment.R.self, request: Get3Ds(token: token.paymentToken, amount: amount),
            success: {r in
                self.url.wrappedValue = r.url
            },
            failure: {_ in
                self.showingPaymentSheet.wrappedValue = false
            },
            any: {

            }
        )
    }

    func didFinish(on module: TokenizationModuleInput,
                   with error: YooKassaPaymentsError?) {

        //print("didFinish Error " + e)
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            //self.dismiss(animated: true)
        }
    }

    func didSuccessfullyPassedCardSec(on module: TokenizationModuleInput) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            print("3DS SUCCESS")
            self.url.wrappedValue = ""
            // Создать экран успеха после прохождения 3DS
            //self.dismiss(animated: true)
            // Показать экран успеха
        }
    }
}

struct PaymentController: UIViewControllerRepresentable {
    @Binding var amount: Int
    @Binding var vc: (UIViewController & TokenizationModuleInput)?
    @Binding var url: String
    @Binding var showingPaymentSheet: Bool

    @State var output: PaymentOutput = PaymentOutput(amount: 0, url: .constant(""), showingPaymentSheet: .constant(false))

    func makeUIViewController(context: Context) -> UIViewController {
        let clientApplicationKey = "#"
        let a = Amount(value: Decimal(amount), currency: .rub)

        let tokenizationSettings = TokenizationSettings(paymentMethodTypes: [.bankCard])
        let tokenizationModuleInputData =
                  TokenizationModuleInputData(clientApplicationKey: clientApplicationKey,
                                              shopName: "#",
                                              purchaseDescription: "\(Main.user.id)",
                                              amount: a,
                                              tokenizationSettings: tokenizationSettings,
                                              savePaymentMethod: .off)

        let inputData: TokenizationFlow = .tokenization(tokenizationModuleInputData)
        output.set(amount: amount, url: $url, showingPaymentSheet: $showingPaymentSheet)
        let vc = TokenizationAssembly.makeModule(inputData: inputData, moduleOutput: output)
        self.vc = vc
        return vc
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
    }
}