sumup / sumup-ios-sdk

Other
46 stars 24 forks source link

Use of deprecated UIAlertView causes crash with UIScene based applications (i.e. iOS 13) #93

Closed pierceglennie closed 3 years ago

pierceglennie commented 4 years ago

I'm using the SumUp SDK with a SwiftUI app targeting iOS 13. Mostly it works fine. I use UIViewControllerRepresentable in SwiftUI to incorporate the SumUp UIViewController within SwiftUI.

However, I'm coming across the following error in some situations (steps to reproduce below):

*** Terminating app due to uncaught exception 'NSObjectNotAvailableException', reason: 'UIAlertView is deprecated and unavailable for UIScene based applications, please use UIAlertController!

Presumably, this means that the SumUp SDK is still using UIAlertView to show some alerts, even though it was deprecated in iOS 8. The steps I've used reproduce this bug are as follows, although there may be other situations where the SumUp SDK is trying to use UIAlertView:

1) Initiate payment. Payment view shown on screen 2) Cancel payment by pressing button on side of device. Cancel view shown on screen 3) Initiate payment again. Payment view again shown on screen but after a second or two this error appears in the console. The payment view continues to be shown which blocks the screen. Since there is no cancel button within it, this makes the app completely unusable

The solution seems to be for SumUp to remove all uses of UIAlertView since it is deprecated and completely unavailable for some iOS 13 apps. It should be replaced by UIAlertViewController.

Note that I've tried using version 3.5b1 of the SDK and this does not resolve the issue. Without a fix, it will be impossible to use SumUp within my SwiftUI app and I'll have to find an alternative provider.

pierceglennie commented 4 years ago

I've found the reason why an alert was being triggered when I initiated a payment again. I was using the same foreign transaction ID again. This is presumably is not allowed even if the first payment was cancelled.

However, this does not solve the underlying issue that the SumUp SDK is using UIAlertView which has been deprecated for many years. If it wasn't deprecated, I would presumably have received a helpful error message. Instead, I got a bug that completely crashed the app with no feedback on what was causing the alert to be displayed.

It would be good to see this kind of thing fixed if people are going to have confidence in building an app on top of the SDK.

On a similar note, I get the following warning when I build, due to the SumUp SDK. I've seen others have reported this also. It doesn't seem to be causing any issues but it's irritating to have to ignore this warning every single time.

ld: warning: Incompatible Objective-C category definitions. Some category metadata may be lost. '.../Pods/SumUpSDK/SumUpSDK.embeddedframework/SumUpSDK.framework/SumUpSDK(libSumUpSDK.a-arm64-master.o)' and '/.../Pods/SumUpSDK/SumUpSDK.embeddedframework/SumUpSDK.framework/SumUpSDK(AudioReaderSDK+EMV.o) built with different compilers

A little more regular maintenance on the SDK would be a big help – it's generally good but as I say lack of updates/fixes undermines confidence in relying on it.

andraskadar commented 3 years ago

Hi @pierceglennie!

Sorry for the late reply, we have just recently released version 4.0.1, where all UIAlertViews have been removed from our code and we are using UIAlertControllers now.

I hope this helps! I am closing this issue, but feel free to reach out if you face any more problems.

solutionpark commented 3 years ago

Hi @pierceglennie, I tried to implement the sumupsdk via UIViewControllerRepresentable but it only shows a white page.

I am right that the VC is only to present the Login? Do you have a code snippet of how to handle it? thx much

pierceglennie commented 3 years ago

Hi @solutionpark,

The view controller is used for everything – login screen and things like the payment screen. It's been quite a while since I looked at this and I was quite new to Swift at the time, so my code will be messy. However, I've put various snippets below that may help you.

First, here's a SumUpData observable object I define which contains all settings related to the SumUp view controller:

import Foundation

class SumUpData: ObservableObject {

    @Published var isLoggedIn = false
    @Published var requestLogin = false
    @Published var requestLogout = false
    @Published var prepareForCheckout = false

    /// Setting to non nil value will initiate checkout request for this transaction data
    @Published var requestCheckout: TransactionData?

    /// Should be reset to nil as soon as it has been processed
    @Published var latestCheckoutOutcome: SumUpData.CheckoutOutcome?

    struct TransactionData: Equatable {
        var amountToCharge: Decimal
        /// Will be displayed in the SumUp dashboard and on customer receipts
        var transactionReference = "Example Reference"
        /// Transaction ID available via SumUp API
        var foreignTransactionID = UUID()
        /// Skip showing default SumUp success screen on successful transaction
        var skipSuccessScreen = true
    }

    enum CheckoutOutcome: Equatable {
        case success(TransactionData)
        case cancelled
        /// Handle differently to an error, since another checkout is in progress and will be completed in due course
        case warningCheckoutAlreadyInProgress
        case errorNotLoggedIn
        case errorCheckoutNotStarted
        case errorGeneric
    }

}

Next, I have my UIViewControllerRepresentable which takes this SumUpData observable object.

import SwiftUI
import SumUpSDK
import os.log

struct SumUp: UIViewControllerRepresentable {

    typealias UIViewControllerType = SumUpViewController

    @ObservedObject var sumUpData: SumUpData

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: UIViewControllerRepresentableContext<SumUp>) -> SumUpViewController {
        let sumUpViewController = SumUpViewController(delegate: context.coordinator)
        return sumUpViewController
    }

    func updateUIViewController(_ sumUpViewController: SumUpViewController,
                                context: UIViewControllerRepresentableContext<SumUp>) {
        if sumUpData.requestLogin {
            DispatchQueue.main.async {
                self.sumUpData.requestLogin = false
            }
            sumUpViewController.presentLogin()
        }
        if sumUpData.requestLogout {
            DispatchQueue.main.async {
                self.sumUpData.requestLogout = false
            }
            sumUpViewController.requestLogout()
        }
        if sumUpData.prepareForCheckout {
            DispatchQueue.main.async {
                self.sumUpData.prepareForCheckout = false
            }
            sumUpViewController.prepareForCheckout()
        }
        if let request = sumUpData.requestCheckout {
            DispatchQueue.main.async {
                self.sumUpData.requestCheckout = nil
            }
            if sumUpData.isLoggedIn {
                sumUpViewController.requestCheckout(request)
            } else {
                sumUpViewController.presentLoginThenCheckout(request)
            }
        }
    }

    class Coordinator: NSObject, SumUpViewControllerDelegate {
        var parent: SumUp

        init(_ parent: SumUp) {
            self.parent = parent
        }

        func updatedLoginState(_ isLoggedIn: Bool) {
            DispatchQueue.main.async {
                self.parent.sumUpData.isLoggedIn = isLoggedIn
            }
        }

        func updatedCheckoutOutcome(_ outcome: SumUpData.CheckoutOutcome) {
            DispatchQueue.main.async {
                self.parent.sumUpData.latestCheckoutOutcome = outcome
            }
        }
    }

}

protocol SumUpViewControllerDelegate: class {
    func updatedLoginState(_ isLoggedIn: Bool)
    func updatedCheckoutOutcome(_ outcome: SumUpData.CheckoutOutcome)
}

class SumUpViewController: UIViewController {

    unowned var delegate: SumUpViewControllerDelegate

    init(delegate: SumUpViewControllerDelegate) {
        self.delegate = delegate
        self.delegate.updatedLoginState(SumUpSDK.isLoggedIn)
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    fileprivate func presentLoginThenCheckout(_ transactionData: SumUpData.TransactionData) {
        customLog(.info, "Showing SumUp login then checking out")
        SumUpSDK.presentLogin(from: self, animated: true) { (_, _) in
            self.delegate.updatedLoginState(SumUpSDK.isLoggedIn)
            if SumUpSDK.isLoggedIn {
                self.requestCheckout(transactionData)
            } else {
                self.delegate.updatedCheckoutOutcome(.errorNotLoggedIn)
            }
        }
    }

    fileprivate func presentLogin() {
        SumUpSDK.presentLogin(from: self, animated: true) { (_, _) in
            self.delegate.updatedLoginState(SumUpSDK.isLoggedIn)
        }
    }

    fileprivate func loginWithToken(_ token: String) {
        SumUpSDK.login(withToken: token) { (_, _) in
            self.delegate.updatedLoginState(SumUpSDK.isLoggedIn)
        }
    }

    fileprivate func requestLogout() {
        SumUpSDK.logout { (success: Bool, error: Error?) in
            if success {
                customLog(.info, log: .sumUp,
                          "Logged out of SumUp successfully")
            } else {
                customLog(.error, log: .sumUp,
                          "Tried to logout from SumUp but failed",
                          ["Error": String(describing: error)])
            }
            self.delegate.updatedLoginState(SumUpSDK.isLoggedIn)
        }
    }

    fileprivate func prepareForCheckout() {
        SumUpSDK.prepareForCheckout()
    }

    fileprivate func requestCheckout(_ transactionData: SumUpData.TransactionData) {

        customLog(.info, log: .sumUp, "Starting SumUp checkout")

        // Ensure that we have a valid merchant and set correct currency
        guard let merchantCurrencyCode = SumUpSDK.currentMerchant?.currencyCode else {
            self.delegate.updatedCheckoutOutcome(.errorNotLoggedIn)
            return
        }

        // Setup payment request
        let request = CheckoutRequest(total: NSDecimalNumber(decimal: transactionData.amountToCharge),
                                      title: transactionData.transactionReference,
                                      currencyCode: merchantCurrencyCode)

        request.foreignTransactionID = transactionData.foreignTransactionID.uuidString

        if transactionData.skipSuccessScreen {
            request.skipScreenOptions = .success
        }

        SumUpSDK.checkout(with: request, from: self) { (result: CheckoutResult?, error: Error?) in

            guard error == nil  else {
                if let safeError = error as NSError? {
                    if safeError.domain == SumUpSDKErrorDomain
                        && safeError.code == SumUpSDKError.checkoutInProgress.rawValue {
                        customLog(.error, log: .sumUp,
                                  "SumUp checkout already in progress. Cancelling checkout attempt")
                        self.delegate.updatedCheckoutOutcome(.warningCheckoutAlreadyInProgress)
                    } else if safeError.domain == SumUpSDKErrorDomain
                                && safeError.code == SumUpSDKError.accountNotLoggedIn.rawValue {
                        customLog(.error, log: .sumUp,
                                  "SumUp not logged in. Cancelling checkout attempt")
                        self.delegate.updatedCheckoutOutcome(.errorNotLoggedIn)
                    } else {
                        customLog(.error, log: .sumUp,
                                  "Other SumUp error encountered. Cancelling checkout attempt")
                        self.delegate.updatedCheckoutOutcome(.errorGeneric)
                    }
                } else {
                    customLog(.error, log: .sumUp,
                              "Unknown SumUp error encountered. Cancelling checkout attempt")
                    self.delegate.updatedCheckoutOutcome(.errorGeneric)
                }
                return
            }

            guard let result = result else {
                customLog(.error, log: .sumUp,
                          "SumUp didn't return checkout error or result. Cancelling checkout attempt")
                self.delegate.updatedCheckoutOutcome(.errorGeneric)
                return
            }

            if result.success {
                customLog(.info, log: .sumUp,
                          "SumUp checkout completed successfully")
                self.delegate.updatedCheckoutOutcome(.success(transactionData))
            } else {
                customLog(.info, log: .sumUp,
                          "SumUp checkout cancelled")
                self.delegate.updatedCheckoutOutcome(.cancelled)
            }
        }

    }

}

Finally, to use SumUp inside a SwiftUI view I add a SumUpData StateObject inside my view:

@StateObject var sumUpData = SumUpData()

Further down, inside the body of the view I do a ZStack with my main view content and the SumUp UIViewControllerRepresentable. Specially, I add this view like the following – note the onChange that's not necessary to get something basic working but is used to respond to a payment going through.

SumUp(sumUpData: sumUpData)
    .frame(width: 0, height: 0)
    .onChange(of: sumUpData.latestCheckoutOutcome) { outcome in
        if outcome != nil {
            sumUpData.latestCheckoutOutcome = nil
            if case let .success(transactionData) = outcome {
                trip.order.paymentData = PaymentData(id: transactionData.foreignTransactionID,
                                                     amount: transactionData.amountToCharge)
                attemptToCompleteOrder()
            }
        }
    }

Finally, to actually make it all work my "Pay Now" button within the SwiftUI view just does the following:

sumUpData.requestCheckout = SumUpData.TransactionData(amountToCharge: amount)

This triggers the SumUp UIViewControllerRepresentable to show the relevant screen – login if necessary, otherwise payment screen – and take the payment.

Sorry this is so long and messy but I thought it was more helpful to provide lots of example code rather than accidentally missing the crucial part. Let me know if you have any questions (or ideas about cleaning it up – I'm sure there are improvements that could be made if it was refactored, I kind of stopped once it was working).

solutionpark commented 3 years ago

@@pierceglennie, you saved my day! (month). Copy-Paste and it worked! Thank you so much. I do not see many changes for now. Like you said, if it works, lets spend time somewhere else. But in 3 month the project will be refactored from prototyping. After I will copy my changes. Also I hope Sumup will change his approach to a more "Model Like SDK" without dependencies in UI.

Thanks again and I wish you a productive 2021!