Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Payment sheet leaking memory in swiftui #1881

Closed
stshelton opened this issue Oct 3, 2021 · 5 comments
Closed

Payment sheet leaking memory in swiftui #1881

stshelton opened this issue Oct 3, 2021 · 5 comments
Assignees
Labels
fixed in master Fixed in master, awaiting next update kind:bug triaged Issue has been reviewed by Stripe and is being tracked internally

Comments

@stshelton
Copy link

stshelton commented Oct 3, 2021

Summary

Having an issue when presenting the payment sheet. The payment sheet doesn't get deallocated when leaving view that it was presented on. When I view the memory debug memory graph I'm seeing this
Screen Shot 2021-10-03 at 3 39 31 PM.

I am presenting the payment sheet in a view that was presented with ".fullScreenCover" which contains a navigation view. Then the payment sheet is presented in the second view on the nav stack. I also notice that when I present the payment sheet it makes all my text go bold while its presented then back to normal size when it dismisses. Once this navview that was presented by fullScreenCover is dismissed. if I view the memory debug graph all STPClasses still exists. if I do this multiple times in the same session there are multiple instances of multiple stpclasses still in memory.

Picture of memory graph multiple stpClasses still in memory after view as dismissed
Screen Shot 2021-10-03 at 4 13 13 PM

Code to reproduce

View for payment methods:

struct PaymentMethodView: View{
    
    var theme: CheckoutTheme = Themes.PaymentMethodTheme
    @ObservedObject var stripeBuilder: CheckoutViewModel
    
    
    var body: some View{
        
        BaseView(addPadding: true){
            VStack(alignment: .leading){
                HStack{
                    Text("Payment Method").font(theme.headerTextSize).bold().frame(maxWidth: .infinity,alignment: .leading)
                    
                }
                Text("Add or choose a payment method").foregroundColor(theme.secondaryTextColor)
                if let paymentSheetFlowController = stripeBuilder.paymentSheetFlowController{
                    PaymentSheet.FlowController.PaymentOptionsButton(paymentSheetFlowController: paymentSheetFlowController) {
                        stripeBuilder.doesUserHavePayment = paymentSheetFlowController.paymentOption != nil ? true : false
                        stripeBuilder.onOptionsCompletion()
                    } content: {
                        HStack (alignment: .bottom){
                            HStack{
                                Image(uiImage: paymentSheetFlowController.paymentOption?.image ?? UIImage(systemName: "creditcard")!)
                                Text(paymentSheetFlowController.paymentOption?.label ?? "Select a payment method")
                            }.padding(.leading, 10).padding(.trailing,10).padding(.top, 5).padding(.bottom, 5).frame( height: 50).modifier(theme.paymentMethodImageModifier)
                            if paymentSheetFlowController.paymentOption?.label != nil{
                                Rectangle().frame(width: 1, height: 1)
                                Text("Change Payment").foregroundColor(Color.tiffanyBlue)
                            }
                        }
                    }.paymentConfirmationSheet(isConfirming: $stripeBuilder.isCheckingPayment,
                                               paymentSheetFlowController: paymentSheetFlowController,
                                               onCompletion: stripeBuilder.onPaymentCompletion).disabled(stripeBuilder.isCheckingPayment)
                }else{
                    HStack(alignment: .bottom){
                        
                        HStack{
                            Image(uiImage: UIImage(systemName: "creditcard")!)
                            Text("Loading...")
                        }.padding(.leading, 10).padding(.trailing,10).padding(.top, 5).padding(.bottom, 5).frame( height: 50).modifier(theme.paymentMethodImageModifier)
                        
                        
                    }
                }
            }
        }
    }
}

View that payment methods view is shown:

struct CheckoutView: View{
    @Binding var isFullSheetOpen: Bool
    @EnvironmentObject var cartDataModel: CartDataModel
    @StateObject var stripeBuilder: CheckoutViewModel = CheckoutViewModel()

    //@Binding var popUpFlag: Bool
    @State var tabBar: UITabBarController?
    @State var goToCheckoutComplete = false
    @State var openSummary = false
    
   //@State var showOrderPoolingPopup: Bool = false
  
  
    
    var body: some View{
        ZStack{
            if let orderPlacedInfo = stripeBuilder.orderPlacedInfo{
                NavigationLink(
                    destination: CheckoutComplete(orderPlacedInfo: orderPlacedInfo, isFullSheetOpen: $isFullSheetOpen),
                    isActive: $goToCheckoutComplete,
                    label: {
                       EmptyView()
                    })
            }
        
        VStack(spacing: 0){
         .......
            ScrollView {
                VStack{
                    
           PaymentMethodView(stripeBuilder: stripeBuilder)
                }
            }
        }.banner(data: $stripeBuilder.bannerInfo, show: $stripeBuilder.showBanner).onAppear {
            if let checkoutStripeInfo = cartDataModel.checkoutProductCartInfo?.checkoutStripeInfo{
                stripeBuilder.preparePaymentSheet(stripePaymentInfo: checkoutStripeInfo)
            }
            self.tabBar?.tabBar.isHidden = true
        }.onDisappear(perform: {
            if !goToCheckoutComplete{
                self.tabBar?.tabBar.isHidden = false
            }
            
            stripeBuilder.paymentSheetFlowController = nil
          
        }).introspectTabBarController { tabBar in
            self.tabBar = tabBar
            self.tabBar?.tabBar.isHidden = true
        }.onAppear(perform: {
            stripeBuilder.totalPriceOfOrder = cartDataModel.checkoutProductCartInfo?.cartTotalPriceInfo.totalFinalPrice
        })
        .popUpCover(show: $stripeBuilder.isCheckingOut ,image: "img",  textView:
                VStack(spacing: 5){
                    Text(stripeBuilder.loadingPopupTitle).bold().font(.title).frame(maxWidth: .infinity).lineLimit(1).minimumScaleFactor(0.5).transition(.opacity).id("MyTitleComponent" + stripeBuilder.loadingPopupTitle)
                    if let errorMessage = stripeBuilder.errorMessage{
                        Text(errorMessage).foregroundColor(Color.red).frame(maxWidth: .infinity).lineLimit(1).minimumScaleFactor(0.5).animation(.easeInOut)
                    }
                }
               , buttonView:
                VStack{
                    switch stripeBuilder.viewModelLoadingStatus{
                    case .loading:
                        LoadingIndicatorView(loadingStatus: $stripeBuilder.viewModelLoadingStatus, style: .init(height: 60, strokeWidth: 4, strokeColor: Color.tiffanyBlue)).padding(.top)
                    case .successfull(_,_):
                        Button(action: {
                            stripeBuilder.isCheckingOut = false
                            self.goToCheckoutComplete = true
                        }, label: {
                            Text("View Order")
                        }).buttonStyle(ButtonStyles.defaultButtonStyle).padding(.top)
                    case .error(_,_):
                        Button(action: {
                            stripeBuilder.isCheckingOut = false
                            stripeBuilder.viewModelLoadingStatus = .loading
                        }, label: {
                            Text("OK")
                        }).buttonStyle(ButtonStyles.defaultSecondaryButtonStyle).padding(.top)
                    default:
                        EmptyView()
                    }
                }
            )
            
        }

    }
    
   

}

Checkout ViewModel:

class CheckoutViewModel: ObservableObject{
    @Published var paymentSheetFlowController: PaymentSheet.FlowController?
    @Published var paymentResult: PaymentSheetResult?
    @Published var isCheckingPayment: Bool = false
    @Published var isCheckingOut: Bool = false
    @Published var didPaymentReturnResult: Bool = false
    
    @Published var loadingPopupTitle: String = "Processing Order..."
    @Published var errorMessage: String?
    @Published var didCheckoutComplete: Bool = false
    
    @Published var bannerInfo: BannerData = BannerData(title: "No Internet", detail: "The Internet connection appears to be offline.", bannerType: .Error)
    @Published var showBanner: Bool = false
    @Published var doesUserHavePayment: Bool = false
    
 
    @Published var viewModelLoadingStatus: ViewModelLoadingStatus = .loading{
    .......
    }
    
    /// create orderinteractor
    var createOrderInteractor = OrderCreateInteractor()

    var stripePaymentInfo: CheckoutStripeInfo?
    
    var orderPlacedInfo: OrderData?
    var totalPriceOfOrder: Double?
    
    init(){
    createOrderInteractor.mutationResponseDelegate = self
        STPAPIClient.shared.publishableKey = BuildConfiguration.shared.stripePublishableKey
    }
    
    func preparePaymentSheet(stripePaymentInfo: CheckoutStripeInfo){
        self.stripePaymentInfo = stripePaymentInfo
        var configuration = PaymentSheet.Configuration()
         configuration.merchantDisplayName = ""
    configuration.customer = .init(id: stripePaymentInfo.cutstomerID, ephemeralKeySecret: stripePaymentInfo.emphemeralKey)
        configuration.primaryButtonColor = UIColor(Color.tiffanyBlue)
        
        PaymentSheet.FlowController.create(paymentIntentClientSecret: stripePaymentInfo.clientSecret, configuration: configuration) { [weak self] result in
            
            switch result{
            case .success(let paymentSheetFlowController):
                DispatchQueue.main.async {
                    self?.paymentSheetFlowController = paymentSheetFlowController
                    self?.doesUserHavePayment =  paymentSheetFlowController.paymentOption != nil  ? true : false
                }
            case .failure(let error):
                self?.showBanner = true
                //TODO deal with errror
                self?.doesUserHavePayment = false
            }
        }
    }
    
    func onOptionsCompletion() {
      // Tell our observer to refresh
      objectWillChange.send()
    }
    
    func onPaymentCompletion(result: PaymentSheetResult) {
       self.paymentResult = result 

self.paymentSheetFlowController = nil
     }
    

}

iOS version

IOS 14.6

Installation method

Swift package manger

SDK version

21.8.1

Other information

Not sure if my pop up view modifier would effect anything can include that code if needed to.

@stshelton stshelton changed the title Payment sheet leaking in swiftui Payment sheet leaking memory in swiftui Oct 3, 2021
@csabol-stripe csabol-stripe added the triaged Issue has been reviewed by Stripe and is being tracked internally label Oct 4, 2021
@csabol-stripe
Copy link
Contributor

Thanks for the detailed report @stshelton! We are currently working on PaymentSheet and will take a look at this memory issue

@ramont-stripe
Copy link
Contributor

Hi @stshelton,

We just released a new version of the SDK last week that includes fixes to SwiftUI memory leaks. I recommend giving it a try.

I would also like to suggest this small change to PaymentMethodView:

 if let paymentSheetFlowController = stripeBuilder.paymentSheetFlowController {
     PaymentSheet.FlowController.PaymentOptionsButton(paymentSheetFlowController: paymentSheetFlowController) {
+        [weak paymentSheetFlowController] in
-        stripeBuilder.doesUserHavePayment = paymentSheetFlowController.paymentOption != nil ? true : false
+        stripeBuilder.doesUserHavePayment = paymentSheetFlowController?.paymentOption != nil ? true : false
         stripeBuilder.onOptionsCompletion()
     }  ...
 } ...

Regarding the text underneath becoming bold, I'm not able to reproduce the issue in our test app. Could it be related to this SwiftUI bug?

@ramont-stripe ramont-stripe self-assigned this Oct 25, 2021
@stshelton
Copy link
Author

Thanks, give me a day or two to update the sdk and add the change you suggested. Also, you may be right on that swifui bug will also look into that. I'll update you guys once I implement changes. Thanks for the help!

@stshelton
Copy link
Author

Looks like this is fixed. I have found another small bug with the latest version. Gonna try to reproduce on example project and create issue when I do. Thanks again

@ramont-stripe
Copy link
Contributor

Awesome! Thank you for the update. I'm closing this issue for now. Feel free to create a new issue once you are able to reproduce the other bug. We are happy to help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
fixed in master Fixed in master, awaiting next update kind:bug triaged Issue has been reviewed by Stripe and is being tracked internally
Projects
None yet
Development

No branches or pull requests

4 participants