Closed joaoppedrosa closed 4 years ago
Hi @joaoppedrosa, could you provide any additional steps or code you are using to reproduce? I just tried using the test card number in our Custom Integration sample app, declining the authorization, then trying the same again but didn't hit this behavior.
Also is it just the web content that is not responsive or is the SFSafariViewController UI unresponsive as well (e.g. you are unable to dismiss)?
Hi @csabol-stripe, thanks for the reply. Actually is a very odd behavior, because sometimes at the second, third try everything works fine, and when we are debugging everything works perfectly. Sometimes appear the "Return to Merchant" button, but most of the time at the second try after a failure, appear this white and freeze screen, whit no cancel or dismiss button, just completely freeze.
I think that is the SFSafariViewController itself because we cannot dismiss or go back after this white screen appears, the only solution is to restart the application.
😬 that doesn't sound good! I'll leave this issue open for a while to see if we get any other reports. If you are able to isolate any more steps/setup to reproduce please add them here!
@csabol-stripe I will leave here our implementation, if you can spot or see anything that we are doing wrong or that can be making this bug.
We are using the version 17.0.1 of Stripe SDK.
RNStripeWrapper.swift
//
// RNStripeWrapper.swift
//
import Foundation
import Stripe
// Converts HEX to UIColor used for Stripe themeing
extension UIColor {
convenience init(hexString: String) {
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int = UInt32()
Scanner(string: hex).scanHexInt32(&int)
let a, r, g, b: UInt32
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (255, 0, 0, 0)
}
self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255)
}
}
// Create an AuthContext with a initializer for defining a view controller
// This is probably not needed if we are using self UIViewController
class AuthContext: NSObject, STPAuthenticationContext {
var viewController: UIViewController
init(viewController: UIViewController) {
self.viewController = viewController
}
func authenticationPresentingViewController() -> UIViewController {
return self.viewController
}
}
// Converts PaymentOption to React including label and base64 image of the card
func convertPaymentMethodObject(_ paymentMethod: STPPaymentOption) -> [AnyHashable : Any]? {
var result: [AnyHashable : Any] = [:]
let data: Data = paymentMethod.image.pngData()!
let img = "data:image/png;base64,"
result["label"] = paymentMethod.label
result["image"] = img + (data.base64EncodedString(options: .endLineWithCarriageReturn))
return result
}
@objc(RNStripeWrapper)
class RNStripeWrapper: NSObject, STPPaymentOptionsViewControllerDelegate {
// Configs
var stripePublishableKey = ""
var backendBaseURL: String? = nil
var headers: [String: String] = [:]
var returnUrl: String? = nil
// Themes
var mainTheme: STPTheme = STPTheme.default()
var navigationTheme : STPTheme = STPTheme.default()
// Contexts
var paymentConfiguration: STPPaymentConfiguration! = nil
var customerContext: STPCustomerContext! = nil
var paymentContext: STPPaymentContext! = nil
var companyName = "Company, lda"
var authenticationType = "manual" // TODO: implement automatic integration
// Views
var authenticationView: UIViewController! = nil
// Resolvers
var promiseResolver: RCTPromiseResolveBlock? = nil
var promiseRejector: RCTPromiseRejectBlock? = nil
var paymentInProgress: Bool = false
// User input
var selectedPaymentMethod: STPPaymentMethod? = nil
var paymentAmount: Int? = nil // Should not be used alone! Use payment metadata to check if the amount matches your back-end.
var paymentCurrency: String = "EUR" // Move to config??
var paymentMetadata: [AnyHashable: AnyObject] = [:]
// Setup main queue
@objc static func requiresMainQueueSetup() -> Bool {
return true
}
// Clears promises
func clearPromises() {
self.promiseResolver = nil
self.promiseRejector = nil
}
// Intializes module with options and themes
// Sets the contexts needed and themes
@objc func initialize(_ options: [String: AnyObject], mainTheme: [String: String] = [:], navigationTheme: [String: String] = [:], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
// Initialize class with options
self.stripePublishableKey = (options["publishableKey"] as! String)
self.backendBaseURL = (options["backendBaseURL"] as! String)
self.headers = (options["headers"] as! [String:String])
self.returnUrl = (options["returnUrl"] as! String)
self.companyName = (options["companyName"] as! String)
self.authenticationType = "manual"
// Update APIClient base url
APIClient.sharedClient.baseURLString = self.backendBaseURL
APIClient.sharedClient.headers = self.headers
// Set themes
if (mainTheme.count > 0) {
self.mainTheme = STPTheme()
self.mainTheme.accentColor = UIColor(hexString: mainTheme["accentColor"]!)
self.mainTheme.secondaryBackgroundColor = UIColor(hexString: mainTheme["secondaryBackgroundColor"]!)
self.mainTheme.secondaryForegroundColor = UIColor(hexString: mainTheme["secondaryForegroundColor"]!)
self.mainTheme.primaryBackgroundColor = UIColor(hexString: mainTheme["primaryBackgroundColor"]!)
self.mainTheme.primaryForegroundColor = UIColor(hexString: mainTheme["primaryForegroundColor"]!)
self.mainTheme.emphasisFont = UIFont(name: mainTheme["emphasisFont"]!, size: 16)
}
if (navigationTheme.count > 0) {
self.navigationTheme = STPTheme()
self.navigationTheme.accentColor = UIColor(hexString: navigationTheme["accentColor"]!)
self.navigationTheme.secondaryBackgroundColor = UIColor(hexString: navigationTheme["secondaryBackgroundColor"]!)
self.navigationTheme.secondaryForegroundColor = UIColor(hexString: navigationTheme["secondaryForegroundColor"]!)
self.navigationTheme.primaryBackgroundColor = UIColor(hexString: navigationTheme["primaryBackgroundColor"]!)
self.navigationTheme.primaryForegroundColor = UIColor(hexString: navigationTheme["primaryForegroundColor"]!)
self.navigationTheme.emphasisFont = UIFont(name: navigationTheme["emphasisFont"]!, size: 16)
}
// Setup payment configuration
// TODO: allow changes via options
self.paymentConfiguration = STPPaymentConfiguration.shared()
self.paymentConfiguration.publishableKey = self.stripePublishableKey
self.paymentConfiguration.companyName = self.companyName
self.paymentConfiguration.canDeletePaymentOptions = true
// Setup costumer context
self.customerContext = STPCustomerContext(keyProvider: APIClient.sharedClient)
// Create authentication view
self.authenticationView = UIViewController()
authenticationView.modalPresentationStyle = .overCurrentContext
resolve(nil)
}
/// Opens the paymentMethods selector
///
/// - Parameters:
/// - resolve: JS Promise resolver
/// - reject: JS Promise rejecter
@objc func openPaymentMethods(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
self.promiseResolver = resolve
self.promiseRejector = reject
// Create payment method view controller
let paymentMethodsViewController = STPPaymentOptionsViewController(
configuration: self.paymentConfiguration,
theme: self.mainTheme,
customerContext: self.customerContext,
delegate: self
)
// Set the default selected method to the current selected
paymentMethodsViewController.defaultPaymentMethod = self.selectedPaymentMethod?.stripeId
// Create a navigation controoler and set the navigation theme
let navigationController = UINavigationController(rootViewController: paymentMethodsViewController)
navigationController.navigationBar.stp_theme = self.navigationTheme
// Dispatch the view to the main view controller
DispatchQueue.main.async {
RCTPresentedViewController()?.present(navigationController, animated: true)
}
}
// Updates the payment context
@objc func updatePaymentContext(_ amount: NSInteger, currency: String, metadata: [String: AnyObject] = [:]) {
// Updates payment context
self.paymentAmount = amount
self.paymentCurrency = currency
self.paymentMetadata = metadata
}
/// Requests a payment
///
/// - Parameters:
/// - resolve: JS Promise resolver
/// - reject: JS Promise rejecter
@objc func requestPayment(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
self.promiseResolver = resolve
self.promiseRejector = reject
self.paymentInProgress = true
self.createPaymentIntent()
}
// On error callback
func onAuthenticationError(error: Error) {
self.paymentInProgress = false
self.authenticationView.dismiss(animated: true)
self.promiseRejector!("", error.localizedDescription, error)
self.clearPromises()
}
// On success callback
func onAuthenticationSuccess() {
self.paymentInProgress = false
self.authenticationView.dismiss(animated: true)
self.promiseResolver!(true)
self.clearPromises()
}
// Creates the payment intent
func createPaymentIntent() {
APIClient.sharedClient.createAndConfirmPaymentIntent(
self.selectedPaymentMethod!,
metadata: self.paymentMetadata,
amount: self.paymentAmount!,
returnURL: self.returnUrl!,
shippingAddress: nil,
shippingMethod: nil
) {
(clientSecret, error) in
guard let clientSecret = clientSecret else {
self.onAuthenticationError(error: NSError(domain: StripeDomain, code: 123, userInfo: [NSLocalizedDescriptionKey: "Unable to parse clientSecret from response"]))
return
}
self.authenticatePaymentIntent(clientSecret: clientSecret)
}
}
// Confirms payment intent on the server
@objc func confirmPaymentIntent(handledPaymentIntent: STPPaymentIntent) {
// Confirm again on the backend
APIClient.sharedClient.confirmPaymentIntent(handledPaymentIntent) {
clientSecret, error in
guard let clientSecret = clientSecret else {
self.onAuthenticationError(error: error ?? NSError(domain: StripeDomain, code: 123, userInfo: [NSLocalizedDescriptionKey: "Unable to parse clientSecret from response"]))
return
}
// Retrieve the Payment Intent and check the status for success
STPAPIClient.shared().retrievePaymentIntent(withClientSecret: clientSecret) { (paymentIntent, retrieveError) in
guard let paymentIntent = paymentIntent else {
self.onAuthenticationError(error: retrieveError ?? NSError(domain: StripeDomain, code: 123, userInfo: [NSLocalizedDescriptionKey: "Unable to parse payment intent from response"]) )
return
}
if paymentIntent.status == .succeeded || paymentIntent.status == .requiresCapture {
self.onAuthenticationSuccess()
}
else {
self.onAuthenticationError(error: NSError(domain: StripeDomain, code: 123, userInfo: [NSLocalizedDescriptionKey: "Authentication failed."]))
}
}
}
}
// Authenticates payment
@objc func authenticatePaymentIntent(clientSecret: String) {
// Create payment handler and authcontext with the current view controller
let paymentHandler = STPPaymentHandler.shared()
let authContext = AuthContext(viewController: self.authenticationView)
// Present the authentication view controller
DispatchQueue.main.async {
RCTPresentedViewController()?.present(self.authenticationView, animated: true)
}
paymentHandler.handleNextAction(forPayment: clientSecret, authenticationContext: authContext, returnURL: self.returnUrl) {
(status, handledPaymentIntent, actionError) in
switch (status) {
case .succeeded:
guard let handledPaymentIntent = handledPaymentIntent else {
self.onAuthenticationError(error: actionError ?? NSError(domain: StripeDomain, code: 123, userInfo: [NSLocalizedDescriptionKey: "Unknown failure"]))
return
}
if (handledPaymentIntent.status == .requiresConfirmation) {
self.confirmPaymentIntent(handledPaymentIntent: handledPaymentIntent)
} else {
// Success
self.onAuthenticationSuccess()
}
case .failed:
self.onAuthenticationError(error: actionError!)
case .canceled:
self.onAuthenticationError(error: NSError(domain: StripeDomain, code: 123, userInfo: [NSLocalizedDescriptionKey: "User canceled authentication."]))
}
}
}
// MARK: STPPaymentOptionsViewControllerDelegate
// STPPaymentOptionsViewController fail
func paymentOptionsViewController(_ paymentOptionsViewController: STPPaymentOptionsViewController, didFailToLoadWithError error: Error) {
if (self.promiseRejector != nil) {
self.promiseRejector!("", "", error)
}
self.clearPromises()
}
// STPPaymentOptionsViewController finish
func paymentOptionsViewControllerDidFinish(_ paymentOptionsViewController: STPPaymentOptionsViewController) {
paymentOptionsViewController.dismiss()
}
// STPPaymentOptionsViewController cancel
func paymentOptionsViewControllerDidCancel(_ paymentOptionsViewController: STPPaymentOptionsViewController) {
if (self.promiseRejector != nil) {
self.promiseRejector!("", "User cancelled", NSError(domain: StripeDomain, code: 123, userInfo: [NSLocalizedDescriptionKey: "User canceled selecting payment."]))
}
paymentOptionsViewController.dismiss()
self.clearPromises()
}
// On select Payment Method
func paymentOptionsViewController(_ paymentOptionsViewController: STPPaymentOptionsViewController, didSelect paymentOption: STPPaymentOption) {
// Convert STPPaymentOption option to STPPaymentMethod to obtain the stripeId later
let selectedPaymentMethod = (paymentOption as! STPPaymentMethod)
// For some reason this is called on start so just check if the paymentMethod is different
if (self.selectedPaymentMethod?.stripeId != selectedPaymentMethod.stripeId) {
// Set selected
self.selectedPaymentMethod = selectedPaymentMethod
// Resolve promise with the paymentOption because we don't need any more info
var result: [AnyHashable : Any] = [:]
let selectedPaymentMethod = convertPaymentMethodObject(paymentOption)
result["selectedPaymentMethod"] = selectedPaymentMethod
if (self.promiseResolver != nil) {
self.promiseResolver!(result)
}
self.clearPromises()
}
}
}
AppDelegate.m
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
NSLog( @"STIPRE URL: %@", url.absoluteString );
BOOL stripeHandled = [Stripe handleStripeURLCallbackWithURL:url];
if (stripeHandled) {
return YES;
}
BOOL fbhandled = [[FBSDKApplicationDelegate sharedInstance] application:application
openURL:url
sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]
annotation:options[UIApplicationOpenURLOptionsAnnotationKey]
];
if (fbhandled) {
return YES;
}
return NO;
}
// This method handles opening universal link URLs (e.g., "https://example.com/stripe_ios_callback")
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
if (userActivity.activityType == NSUserActivityTypeBrowsingWeb) {
if (userActivity.webpageURL) {
BOOL stripeHandled = [Stripe handleStripeURLCallbackWithURL:userActivity.webpageURL];
if (stripeHandled) {
return YES;
}
return NO;
}
}
return NO;
}
thanks @joaoppedrosa this is really helpful. Looking at the code, I'm wondering if maybe the view that is left frozen on the screen is actually authenticationView
. Can you confirm the view hierarchy when you hit this bug? (Fastest way would probably be to user Xcodes UI Inspector). Can you also confirm whether your completion handlers onAuthenticationSuccess
and onAuthenticationError
are being called as expected?
I think this is fine, but would also recommend against asynchronously presenting a view before calling handleNextAction
i.e.
DispatchQueue.main.async {
RCTPresentedViewController()?.present(self.authenticationView, animated: true)
}
I don't think it is causing an issue now because the first step of handleNextAction
is a network request but it may cause issues in the future
Hi @csabol-stripe, I check the onAuthenticationSuccess
and onAuthenticationError
and they are being called as expected.
Thanks for the tip about the asynchronously presenting a view, it's already resolved.
About the bug it's still occurring, I use the XCode UI Inspector and the block it's happening in SFSafariViewController
hmm, to help debug could you implement - (void)authenticationContextWillDismissViewController:(UIViewController *)viewController
? This should be called before the Stripe SDK dismisses the safari view controller. I would expect the view hierarchy at that point to be the SFSafariViewController instance on top of self.authenticationView
. By the time the handleNextAction
completion block is called I would expect the SFSafariViewController to be dismissed and [[STPPaymentHandler sharedHandler] safariViewController] == nil
Hi @csabol-stripe , I update the SDK to the latest version (17.0.2) and add the authenticationContextWillDismissViewController
and for now it seems to be correctly working, the screen doesn't freeze anymore.
I will be continuing testing, and if anything comes up I will reach with you again.
Thank you for your support :)
Hi Guys, I'm having a similar issue, but in my case, it's been replacing the view I have in the background. The solution for the issue is to set the SFSafariViewController modal presentation to be "overCurrentContext".
And I'm also having the same issue @csabol-stripe is having with the authenticationContextWillDismissViewController method... But in my case, it's not been called.
I've tried this:
Call Stripe payment intent:
fileprivate func executeStripePaymentIntent(_ paymentIntentClientSecret: String, successCallback: @escaping (_ paymentIntentId:String) -> Void, failureCallback: @escaping () -> Void) {
let paymentIntentParams = STPPaymentIntentParams(clientSecret: paymentIntentClientSecret)
let paymentManager = STPPaymentHandler.shared()
paymentManager.confirmPayment(withParams: paymentIntentParams, authenticationContext: self) { (status, paymentIntent, error) in
switch (status) {
case .failed:
// Handle error
MyAlert.showInfo(subTitle: "\(error!.localizedDescription)")
failureCallback()
break
case .canceled:
// Handle cancel
failureCallback()
break
case .succeeded:
// Payment Intent is confirmed
successCallback(paymentIntent!.stripeId)
break
}
}
}
Extend view to include STPAuthenticationContext delegate methods:
extension WalletTopUpViewController: STPAuthenticationContext {
func authenticationPresentingViewController() -> UIViewController {
return self
}
func prepare(forPresentation completion: @escaping STPVoidBlock) {
MyLogger.log(message: "prepare", event: .debug)
completion()
}
func configureSafariViewController(_ viewController: SFSafariViewController) {
MyLogger.log(message: "configureSafariViewController", event: .debug)
// fix issue with 3d payment replacing previous view
viewController.modalPresentationStyle = .overCurrentContext
}
func authenticationContextWillDismiss(_ viewController: UIViewController) {
MyLogger.log(message: "authenticationContextWillDismiss", event: .debug)
}
}
The "logger" is printing only the "prepare" message, so it means the delegate is fine.. but none of those next two methods were been called.
Am I implementing the method properly? do I have to config something different for them to work?
Stripe SDK version: 18.0.0
Thanks in advance!
@diegoalex Does your completion handler get called? Just looking at the code it seems correct, but we do some checks before calling configureSafariViewController or presenting to make sure the presentation context is valid (see STPPaymentHander#_canPresentWithAuthenticationContext:err:
), but if that's the case you should be getting a completion callback with an error
@diegoalex could you clarify what you mean by "replacing the view I have in the background"? To clarify, sliding/pushing the safari view controller in from the side is the iOS default, but you can override that as you've described :)
Hi @csabol-stripe , the completion handler is working fine, actually, the whole process is working. But for some reason, those two methods (configureSafariViewController and authenticationContextWillDismiss) were not been called.
I've done a pod update and pod install and closed xcode/restart computer and now the app is going into the function configureSafariViewController and setting the modalPresentationStyle.. SO the problem is fixed.. Probably just some Xcode cache issue.
But to clarify about the "replacing view in the background" , my app structure is like this: -Tab view -- payment modal view (modalPresentationStyle = .overCurrentContext) --- stripe safari view
For some reason when the safari view opens, my tab view reloads.. for example, if I'm in tab 3 when I close the safari view it keeps the payment modal, but the tab view refreshes and go back to tab 1. It also happened before with other modal views but setting "modalPresentationStyle = .overCurrentContext" fixed this issue.
But thanks anyway ;)
Summary
At the second try in the safari freezes and doesn't respond anymore. And I'm receiving the URL callback.
Code to reproduce
1 - Insert a 3D Secure card (like 4000 0082 6000 3178), make the confirm and failed the authentication 2 - Request a new confirm and the screen freezes
iOS version
iOS 12.4 | iOS 12.3.1
Installation method
Cocoapods
SDK version
17.0.1
Other information
1-
2-