ios-branch-deep-linking-attribution icon indicating copy to clipboard operation
ios-branch-deep-linking-attribution copied to clipboard

Commerce event of kind .purchase is reported twice when user pays with ApplePay, resulting in two-fold revenue digits shown in dashboard for ApplePay payments

Open vitalii-tym opened this issue 3 months ago • 2 comments

Describe the bug

While reporting an event of type .purchase the SDK sends two almost identical API calls if ApplePay is used to perform the payment.

This happens because: (1) when the ApplePay dialog appears the SDK sets initializationStatus = BNCInitStatusUninitialized:

Screenshot 2024-05-01 at 15 48 14

(2) the initSafetyCheck method adds an extra event into the queue:

Screenshot 2024-05-01 at 15 53 39 Screenshot 2024-05-01 at 16 09 36

This extra event is an OpenRequest, which could be tolerated if it was the only issue. (3) But for some unknown reason the SDK sends two .purchase events to the server instead, which are almost identical.

Screenshot 2024-05-01 at 16 17 25 Screenshot 2024-05-01 at 16 17 40

The Dashboard, in its turn, shows these two events as if it was one event with an x2 revenue in Summary, while in LiveView and export those are shown as 2 events.

Everything goes well if Credit Card is used to make this purchase - correct revenue is reported.

Steps to reproduce

  1. Implement reporting of .purchase event as described in the docs (step 1 - generate a BUO, step 2 - create a standard event of type .purchase, add BUO into this event and report it).

Example:

    func generateBUO(itemId: String, frpId: String, title: String, description: String, feeCheckout: ServiceFeeCheckout?, tokensCheckout: TokensCheckout?) -> BranchUniversalObject {
        let buo: BranchUniversalObject = BranchUniversalObject(canonicalIdentifier: itemId) // this is the bookingId, which is surely unique one (while frpId can repeat if a booking is cancelled)
        buo.title = title
        buo.contentDescription = description
        buo.locallyIndex = false
        buo.publiclyIndex = false
        buo.contentMetadata.contentSchema = .commerceService
        if let knownFeeCheck = feeCheckout?.forPayment {
            buo.contentMetadata.quantity = 1.0
            buo.contentMetadata.price = Decimal(knownFeeCheck.amount/100) as NSDecimalNumber // amount is in "cents"
            buo.contentMetadata.currency = BNCCurrency(rawValue: knownFeeCheck.currency.rawValue)
            buo.contentMetadata.productName = "Private Jet Charter service"
            buo.contentMetadata.productBrand = "TEST"
            buo.contentMetadata.productCategory = .software
        } else if let knownTokensCheck = tokensCheckout {
            buo.contentMetadata.quantity = 1.0 // Double(knownTokensCheck.totalAmount)  // TODO: Add correct amount here
            if let coinsRate = knownTokensCheck.rates["COINRATE"], let currencyRate = knownTokensCheck.rates[knownTokensCheck.currency.rawValue] {
                buo.contentMetadata.price = (Decimal(currencyRate) / Decimal(coinsRate)) as NSDecimalNumber
            }
            buo.contentMetadata.currency = BNCCurrency(rawValue: knownTokensCheck.currency.rawValue)
            buo.contentMetadata.productName = "TEST Tokens"
            buo.contentMetadata.productBrand = "TEST"
            buo.contentMetadata.productCategory = .software
        }
        buo.contentMetadata.customMetadata["Flight ID"] = frpId.prefix(6)
        return buo
    }

    func logPurchase(buo: BranchUniversalObject, alias: String, bookingId: String, transactionID: String, provider: PaymentProvider, means: PaymentMeans, feeCheckout: ServiceFeeCheckout?, coinsCheckout: TokensCheckout?) {
        let event = BranchEvent.standardEvent(.purchase)
        event.contentItems = [buo]
        event.alias = alias
        event.transactionID = transactionID
        if let knownCheck = feeCheckout?.forPayment {
            event.eventDescription = "A Service Fee payment"
            event.revenue = Decimal(knownCheck.amount/100) as NSDecimalNumber
            event.currency = BNCCurrency(rawValue: knownCheck.currency.rawValue)
        } else if let knownCheck = coinsCheckout {
            event.eventDescription = "Tokens purchase"
            event.revenue = Decimal(knownCheck.amount/100) as NSDecimalNumber
            event.currency = BNCCurrency(rawValue: knownCheck.currency.rawValue)
        }
        event.customData = [
            "Payment_Provider": provider.rawValue,
            "Payment_Means": means.rawValue,
            "Booking_ID": bookingId
        ]
        event.logEvent()
    }
  1. Implement payment flows with a Credit Card and ApplePay. Make sure the code calls event.logEvent() only once per each payment.
  2. Try to perform payment with Credit Card and check Liveview - only one purchase event is reported.
  3. Try to perform payment with ApplePay and check it - you will see two events reported in the Liveview, which are less than a second from each other. Watch also the network to see that the SDK performs 2 API calls (see screenshots in description).
Screenshot 2024-05-01 at 16 36 06

Expected behavior

I expect the same number of events reported when payment is made with ApplePay and Credit Card, as well as same values in Revenue field shown in these two cases in the Branch Dashboard.

SDK Version

3.3.0

XCode Version

15.0

Device

iOS simulator

OS

17.0

Additional Information/Context

I will also share access to our TestFlight build with Yashwanthi, with whom I'm currently in contact via email.

vitalii-tym avatar May 01 '24 13:05 vitalii-tym

@vitalii-tym Can you provide some more context on when you call your logPurchase() method and how you're bringing up ApplePay vs when using credit card?

nsingh-branch avatar May 08 '24 20:05 nsingh-branch

@nsingh-branch , the ApplePay is implemented using Stripe's Payment Intents API: First I create a payment request as described here, then I ask Stripe SDK to present the ApplePay payment sheet as described here, then I get client secret from my server and perform completion block so that Stripe completes the payment and dismisses the Apple Pay sheet as described here. The logPurchase() method is called inside applePayContext(_:didCompleteWithStatus:error:) when the returned status is .success.

In terms of code, this is how I ask Stripe to present the Apple Pay sheet:

private func requestStripeApplePayAuthorization(currency: Currency, purchaseSummaryList: [(String, Double)]) {
    guard let merchantIdentifier = STPPaymentConfiguration.shared.appleMerchantIdentifier else {  return }
    let paymentRequest = StripeAPI.paymentRequest(withMerchantIdentifier: merchantIdentifier, country: "GB", currency: currency.rawValue)
    for item in self.purchaseSummaryList {
        let name = item.0
        let cost: NSDecimalNumber = NSDecimalNumber(decimal: Decimal(item.1))
        paymentRequest.paymentSummaryItems.append(PKPaymentSummaryItem(label: name, amount: cost))
        }
    if StripeAPI.canSubmitPaymentRequest(paymentRequest), let applePayContext = STPApplePayContext(paymentRequest: paymentRequest, delegate: self) {
        applePayContext.presentApplePay()
    } else {
        // show alert to user
    }
}

and this part asks Stripe to complete the payment and close the Apple Pay sheet, this is where I report the event to Branch:

extension PaymentMethodViewController: STPApplePayContextDelegate {
    func applePayContext(_ context: STPApplePayContext, didCreatePaymentMethod paymentMethod: STPPaymentMethod, paymentInformation: PKPayment, completion: @escaping STPIntentClientSecretCompletionBlock) {
        createClientSecret(success: { secret in completion(secret, nil) }, failure: { error in completion(nil, error) })
    }

    func applePayContext(_ context: STPApplePayContext, didCompleteWith status: STPPaymentStatus, error: Error?) {
        switch status {
        case .success:
            AppUtil.shared.showThankYouView(paymentMode: self.mode)
            if let knownProduct: BranchUniversalObject = self.currentProduct {
                BranchManager.shared.logPurchase(buo: knownProduct, alias: UserStateManager.shared.clientInfo?.branch == .usBranch ? "Tokens US" : "Tokens EU", bookingId: "N/A", transactionID: "", provider: .stripe, means: .applePay, feeCheckout: nil, coinsCheckout: self.coinsCheckout)
            }
            self.dismiss(animated: true)
        case .error:
            () // show error to user
        case .userCancellation: () // no purchase request happened, usually when Apple Pay dialog was cancelled
        @unknown default:
            fatalError()
        }
    }
}

Payment with Credit Card also uses Payment Intents, though it doesn't involve opening any extra sheets. The code uses a previously tokenized/saved card. It is implemented this way:

private func payWithStripe(cardId: String) {
    createClientSecret(success: { secret in
        StripeService.shared.payWithCard(cardId, secret: secret, authenticationContext: self) { status, paymentIntent, error in
            switch status {
            case .failed:
                // show alert to user
                self.dismiss(animated: true)
            case .canceled:
                self.dismiss(animated: true)
            case .succeeded:
                AppUtil.shared.showThankYouView(paymentMode: self.mode)
                if let knownProduct: BranchUniversalObject = self.currentProduct, let knownPaymentIntent = paymentIntent {
                    BranchManager.shared.logPurchase(buo: knownProduct, alias: UserStateManager.shared.clientInfo?.branch == .usBranch ? "Tokens US" : "Tokens EU",
                                                     bookingId: "N/A", transactionID: knownPaymentIntent.stripeId, provider: .stripe, means: .creditCard, feeCheckout: nil, coinsCheckout: self.coinsCheckout)
                }
                self.dismiss(animated: true)
            @unknown default:
                fatalError()
            }
        }
    }, failure: { error in
        // show alert to user
    })
}

vitalii-tym avatar May 13 '24 17:05 vitalii-tym

@vitalii-tym Thanks for above info. Can you provide SDK logs as well? Also, you mentioned about extra OpenRequest event added by initSafetyCheck . I dont see that in above screenshot. I see only https://api-safetrack.branch.io/v2/event/standard. Can you share OpenRequest POST params also ?

NidhiDixit09 avatar May 23 '24 22:05 NidhiDixit09

@NidhiDixit09

you mentioned about extra OpenRequest event added by initSafetyCheck . I dont see that in above screenshot

The .open request is visible in the requestQueue after Apple's payment sheet appears. However, there is no POST message about it seen anywhere. It feels as if the extra .purchase event is sent instead of the .open request.

Can you provide SDK logs as well?

Will try later.

vitalii-tym avatar May 24 '24 07:05 vitalii-tym