ios-branch-deep-linking-attribution
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
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
:
(2) the initSafetyCheck
method adds an extra event into the queue:
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.
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
- 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()
}
- Implement payment flows with a Credit Card and ApplePay. Make sure the code calls
event.logEvent()
only once per each payment. - Try to perform payment with Credit Card and check Liveview - only one purchase event is reported.
- 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).
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 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 , 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 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
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.