stripe-ios icon indicating copy to clipboard operation
stripe-ios copied to clipboard

SwiftUI redrawing view when payment sheet is presented and on sheet dismissed completion block is written out inline causing leak.

Open stshelton opened this issue 2 years ago • 1 comments

Summary

When updating/redrawing view when payment sheet is presented and on sheet dismissed completion block is written out. Causes payment sheet to be dismissed and presented quickly than when you close payment sheet, grey overlay stays and memory starts to leak CPU and memory continues to climb until app crashs.

Code to reproduce

When onSheetDismissed is written like this it works as intended

PaymentSheet.FlowController.PaymentOptionsButton(
                    paymentSheetFlowController: paymentSheetFlowController,
                    onSheetDismissed: model.onOptionsCompletion) {
                    ExamplePaymentOptionView(
                        paymentOptionDisplayData: paymentSheetFlowController.paymentOption)
 }

When onSheetDismissed completion block is written inline and view is updated while payment sheet is presented causes undesired behavior

PaymentSheet.FlowController.PaymentOptionsButton(
                    paymentSheetFlowController: paymentSheetFlowController,
                    onSheetDismissed: {
                        model.onOptionsCompletion()
                    }
                ) {
                    ExamplePaymentOptionView(
                        paymentOptionDisplayData: paymentSheetFlowController.paymentOption)
                }

Full Code including the banner view that will update/redraw the view when it becomes dismissed which causes leak

struct ExampleSwiftUICustomPaymentFlow: View {
    @StateObject var model = MyCustomBackendModel()
    @State var isConfirmingPayment = false
    
   

    var body: some View {
        VStack {
            if model.showBanner{
                Text("make entire view reload")
            }
            
            if let paymentSheetFlowController = model.paymentSheetFlowController {
                PaymentSheet.FlowController.PaymentOptionsButton(
                    paymentSheetFlowController: paymentSheetFlowController,
                    onSheetDismissed: {
                        model.onOptionsCompletion()
                    }
                ) {
                    ExamplePaymentOptionView(
                        paymentOptionDisplayData: paymentSheetFlowController.paymentOption)
                }
                Button(action: {
                    // If you need to update the PaymentIntent's amount, you should do it here and
                    // set the `isConfirmingPayment` binding after your update completes.
                    isConfirmingPayment = true
                }) {
                    if isConfirmingPayment {
                        ExampleLoadingView()
                    } else {
                        ExamplePaymentButtonView()
                    }
                }.paymentConfirmationSheet(
                    isConfirming: $isConfirmingPayment,
                    paymentSheetFlowController: paymentSheetFlowController,
                    onCompletion: model.onCompletion
                )
                .disabled(paymentSheetFlowController.paymentOption == nil || isConfirmingPayment)
            } else {
                ExampleLoadingView()
            }
            
            Button {
                model.showBanner = true
            } label: {
                Text("Test Banner")
            }

            if let result = model.paymentResult {
                ExamplePaymentStatusView(result: result)
            }
        }.onAppear { model.preparePaymentSheet() }.banner(data: .constant(BannerData(title: "Test", detail: "Test", bannerType: .Error)), show: $model.showBanner)
    }

}

Banner Code:

extension View {
    func banner(data: Binding<BannerData>, show: Binding<Bool>) -> some View {
        self.modifier(BannerModifier(bannerData: data, show: show))
    }
}


struct BannerData {
    var title:String
    var detail:String
    var bannerType: BannerType
}
enum BannerType {
    case Info
    case Warning
    case Success
    case Error
    
    var tintColor: Color {
          switch self {
          case .Info:
              return Color(red: 67/255, green: 154/255, blue: 215/255)
          case .Success:
              return Color.green
          case .Warning:
              return Color.yellow
          case .Error:
              return Color.red
          }
      }
}
struct BannerModifier: ViewModifier{

    
    @Binding var bannerData:BannerData
    @Binding var show:Bool

    func body(content: Content) -> some View {
        ZStack {
            content
            if show {
                VStack {
                    HStack {
                        VStack(alignment: .leading, spacing: 2) {
                            Text(bannerData.title)
                                .bold()
                            Text(bannerData.detail)
                                .font(Font.system(size: 15, weight: Font.Weight.light, design: Font.Design.default))
                        }
                        Spacer()
                    }
                    .foregroundColor(Color.white)
                    .padding(12)
                    .background(bannerData.bannerType.tintColor)
                    .cornerRadius(8)
                    Spacer()
                }.zIndex(2.0)
                .padding()
                .animation(.easeInOut)
                .transition(AnyTransition.move(edge: .top).combined(with: .opacity))
                .onTapGesture {
                    withAnimation {
                        self.show = false
                    }
                }.onAppear(perform: {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                        withAnimation {
                            self.show = false
                        }
                    }
                })
            }
        }
    }

}

iOS version

Tested on IOS 15 and IOS 14.4 (same behavior on both)

Installation method

Swift Package Manager

SDK version

21.9.0

Other information

Example Video: https://user-images.githubusercontent.com/11301401/142065214-34b0534c-1e32-4133-b196-50c0af89e35d.MP4

stshelton avatar Nov 16 '21 21:11 stshelton

Thanks for filing this! We've been investigating a few other issues with SwiftUI redrawing, so we'll track this along with those.

davidme-stripe avatar Jan 13 '22 22:01 davidme-stripe