| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679 |
- #import "BTPayPalDriver_Internal.h"
- #import "BTPayPalAccountNonce_Internal.h"
- #import "BTPayPalCreditFinancing_Internal.h"
- #import "BTPayPalCreditFinancingAmount_Internal.h"
- #import "BTPayPalRequest_Internal.h"
- #import "BTPayPalCheckoutRequest_Internal.h"
- #if __has_include(<Braintree/BraintreePayPal.h>) // CocoaPods
- #import <Braintree/BraintreeCore.h>
- #import <Braintree/BTAPIClient_Internal.h>
- #import <Braintree/BTPaymentMethodNonceParser.h>
- #import <Braintree/BTLogger_Internal.h>
- #import <Braintree/BTConfiguration+PayPal.h>
- #import <Braintree/BTPayPalLineItem.h>
- #elif SWIFT_PACKAGE // SPM
- #import <BraintreeCore/BraintreeCore.h>
- #import "../BraintreeCore/BTAPIClient_Internal.h"
- #import "../BraintreeCore/BTPaymentMethodNonceParser.h"
- #import "../BraintreeCore/BTLogger_Internal.h"
- #import <BraintreePayPal/BTConfiguration+PayPal.h>
- #import <BraintreePayPal/BTPayPalLineItem.h>
- #else // Carthage
- #import <BraintreeCore/BraintreeCore.h>
- #import <BraintreeCore/BTAPIClient_Internal.h>
- #import <BraintreeCore/BTPaymentMethodNonceParser.h>
- #import <BraintreeCore/BTLogger_Internal.h>
- #import <BraintreePayPal/BTConfiguration+PayPal.h>
- #import <BraintreePayPal/BTPayPalLineItem.h>
- #endif
- #if __has_include(<Braintree/Braintree-Swift.h>) // CocoaPods
- #import <Braintree/Braintree-Swift.h>
- #elif SWIFT_PACKAGE // SPM
- /* Use @import for SPM support
- * See https://forums.swift.org/t/using-a-swift-package-in-a-mixed-swift-and-objective-c-project/27348
- */
- @import PayPalDataCollector;
- #elif __has_include("Braintree-Swift.h") // CocoaPods for ReactNative
- /* Use quoted style when importing Swift headers for ReactNative support
- * See https://github.com/braintree/braintree_ios/issues/671
- */
- #import "Braintree-Swift.h"
- #else // Carthage
- #import <PayPalDataCollector/PayPalDataCollector-Swift.h>
- #endif
- NSString *const BTPayPalDriverErrorDomain = @"com.braintreepayments.BTPayPalDriverErrorDomain";
- /**
- This environment MUST be used for App Store submissions.
- */
- NSString * _Nonnull const PayPalEnvironmentProduction = @"live";
- /**
- Sandbox: Uses the PayPal sandbox for transactions. Useful for development.
- */
- NSString * _Nonnull const PayPalEnvironmentSandbox = @"sandbox";
- /**
- Mock: Mock mode. Does not submit transactions to PayPal. Fakes successful responses. Useful for unit tests.
- */
- NSString * _Nonnull const PayPalEnvironmentMock = @"mock";
- @interface BTPayPalDriver () <ASWebAuthenticationPresentationContextProviding>
- @property (nonatomic, assign) BOOL returnedToAppAfterPermissionAlert;
- @end
- @implementation BTPayPalDriver
- + (void)load {
- if (self == [BTPayPalDriver class]) {
- [[BTPaymentMethodNonceParser sharedParser] registerType:@"PayPalAccount" withParsingBlock:^BTPaymentMethodNonce * _Nullable(BTJSON * _Nonnull payPalAccount) {
- return [self payPalAccountFromJSON:payPalAccount];
- }];
- }
- }
- - (instancetype)initWithAPIClient:(BTAPIClient *)apiClient {
- if (self = [super init]) {
- _apiClient = apiClient;
- [NSNotificationCenter.defaultCenter addObserver:self
- selector:@selector(applicationDidBecomeActive:)
- name:UIApplicationDidBecomeActiveNotification
- object:nil];
- }
- return self;
- }
- - (instancetype)init {
- return nil;
- }
- - (void)applicationDidBecomeActive:(__unused NSNotification *)notification {
- if (self.isAuthenticationSessionStarted) {
- self.returnedToAppAfterPermissionAlert = YES;
- }
- }
- - (void)dealloc {
- [NSNotificationCenter.defaultCenter removeObserver:self];
- }
- #pragma mark - Billing Agreement (Vault)
- - (void)requestBillingAgreement:(BTPayPalVaultRequest *)request
- completion:(void (^)(BTPayPalAccountNonce *tokenizedCheckout, NSError *error))completionBlock {
- [self tokenizePayPalAccountWithPayPalRequest:request completion:completionBlock];
- }
- #pragma mark - One-Time Payment (Checkout)
- - (void)requestOneTimePayment:(BTPayPalCheckoutRequest *)request
- completion:(void (^)(BTPayPalAccountNonce *tokenizedCheckout, NSError *error))completionBlock {
- [self tokenizePayPalAccountWithPayPalRequest:request completion:completionBlock];
- }
- #pragma mark - Helpers
- - (void)tokenizePayPalAccountWithPayPalRequest:(BTPayPalRequest *)request completion:(void (^)(BTPayPalAccountNonce *, NSError *))completionBlock {
- if (!self.apiClient) {
- NSError *error = [NSError errorWithDomain:BTPayPalDriverErrorDomain
- code:BTPayPalDriverErrorTypeIntegration
- userInfo:@{NSLocalizedDescriptionKey: @"BTPayPalDriver failed because BTAPIClient is nil."}];
- completionBlock(nil, error);
- return;
- }
- if (!request) {
- completionBlock(nil, [NSError errorWithDomain:BTPayPalDriverErrorDomain code:BTPayPalDriverErrorTypeInvalidRequest userInfo:nil]);
- return;
- }
- if (!([request isKindOfClass:BTPayPalCheckoutRequest.class] || [request isKindOfClass:BTPayPalVaultRequest.class])) {
- NSError *error = [NSError errorWithDomain:BTPayPalDriverErrorDomain
- code:BTPayPalDriverErrorTypeIntegration
- userInfo:@{NSLocalizedDescriptionKey: @"BTPayPalDriver failed because request is not of type BTPayPalCheckoutRequest or BTPayPalVaultRequest."}];
- completionBlock(nil, error);
- return;
- }
- [self.apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) {
- if (error) {
- if (completionBlock) {
- completionBlock(nil, error);
- }
- return;
- }
- if (![self verifyAppSwitchWithRemoteConfiguration:configuration.json error:&error]) {
- if (completionBlock) {
- completionBlock(nil, error);
- }
- return;
- }
- self.payPalRequest = request;
- [self.apiClient POST:request.hermesPath
- parameters:[request parametersWithConfiguration:configuration]
- completion:^(BTJSON *body, __unused NSHTTPURLResponse *response, NSError *error) {
- if (error) {
- if (error.code == NETWORK_CONNECTION_LOST_CODE) {
- [self.apiClient sendAnalyticsEvent:@"ios.paypal.tokenize.network-connection.failure"];
- }
- NSString *errorDetailsIssue = ((BTJSON *)error.userInfo[BTHTTPJSONResponseBodyKey][@"paymentResource"][@"errorDetails"][0][@"issue"]).asString;
- if (error.userInfo[NSLocalizedDescriptionKey] == nil && errorDetailsIssue != nil) {
- NSMutableDictionary *dictionary = [error.userInfo mutableCopy];
- dictionary[NSLocalizedDescriptionKey] = errorDetailsIssue;
- error = [NSError errorWithDomain:error.domain code:error.code userInfo:dictionary];
- }
- if (completionBlock) {
- completionBlock(nil, error);
- }
- return;
- }
- NSURL *approvalUrl = [body[@"paymentResource"][@"redirectUrl"] asURL];
- if (approvalUrl == nil) {
- approvalUrl = [body[@"agreementSetup"][@"approvalUrl"] asURL];
- }
- approvalUrl = [self decorateApprovalURL:approvalUrl forRequest:request];
- NSString *pairingID = [self.class tokenFromApprovalURL:approvalUrl];
- self.clientMetadataID = self.payPalRequest.riskCorrelationId ? self.payPalRequest.riskCorrelationId : [PPDataCollector clientMetadataID:pairingID isSandbox:[configuration.environment isEqualToString:@"sandbox"]];
- BOOL analyticsSuccess = error ? NO : YES;
- [self sendAnalyticsEventForInitiatingOneTouchForPaymentType:request.paymentType withSuccess:analyticsSuccess];
- [self handlePayPalRequestWithURL:approvalUrl
- error:error
- paymentType:request.paymentType
- completion:completionBlock];
- }];
- }];
- }
- - (NSDictionary *)dictionaryFromResponseURL:(NSURL *)url {
- if ([[self.class actionFromURLAction: url] isEqualToString:@"cancel"]) {
- return nil;
- }
- NSDictionary *resultDictionary = @{
- @"client": @{
- @"platform": @"iOS",
- @"product_name": @"PayPal",
- @"paypal_sdk_version": @"version"
- },
- @"response": @{
- @"webURL": url.absoluteString
- },
- @"response_type": @"web"
- };
- return resultDictionary;
- }
- - (void)handlePayPalRequestWithURL:(NSURL *)url
- error:(NSError *)error
- paymentType:(BTPayPalPaymentType)paymentType
- completion:(void (^)(BTPayPalAccountNonce *, NSError *))completionBlock {
- if (!error) {
- // Defensive programming in case PayPal One Touch returns a non-HTTP URL so that ASWebAuthenticationSession doesn't crash
- if (![url.scheme.lowercaseString hasPrefix:@"http"]) {
- NSError *urlError = [NSError errorWithDomain:BTPayPalDriverErrorDomain
- code:BTPayPalDriverErrorTypeUnknown
- userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Attempted to open an invalid URL in ASWebAuthenticationSession: %@://", url.scheme],
- NSLocalizedRecoverySuggestionErrorKey: @"Try again or contact Braintree Support." }];
- NSString *eventName = [NSString stringWithFormat:@"ios.%@.webswitch.error.safariviewcontrollerbadscheme.%@", [self.class eventStringForPaymentType:paymentType], url.scheme];
- [self.apiClient sendAnalyticsEvent:eventName];
- if (completionBlock) {
- completionBlock(nil, urlError);
- }
- return;
- }
- [self performSwitchRequest:url paymentType:paymentType completion:completionBlock];
- } else if (completionBlock) {
- completionBlock(nil, error);
- }
- }
- - (void)performSwitchRequest:(NSURL *)appSwitchURL paymentType:(BTPayPalPaymentType)paymentType completion:(void (^)(BTPayPalAccountNonce *, NSError *))completionBlock {
- self.approvalUrl = appSwitchURL; // exposed for testing
- self.authenticationSession = [[ASWebAuthenticationSession alloc] initWithURL:appSwitchURL
- callbackURLScheme:BTPayPalCallbackURLScheme
- completionHandler:^(NSURL * _Nullable callbackURL, NSError * _Nullable error) {
- // Required to avoid memory leak for BTPayPalDriver
- self.authenticationSession = nil;
- if (error) {
- if (error.domain == ASWebAuthenticationSessionErrorDomain && error.code == ASWebAuthenticationSessionErrorCodeCanceledLogin) {
- if (self.returnedToAppAfterPermissionAlert) {
- // User tapped system cancel button in browser
- NSString *eventName = [NSString stringWithFormat:@"ios.%@.authsession.browser.cancel", [self.class eventStringForPaymentType:paymentType]];
- [self.apiClient sendAnalyticsEvent:eventName];
- } else {
- // User tapped system cancel button on permission alert
- NSString *eventName = [NSString stringWithFormat:@"ios.%@.authsession.alert.cancel", [self.class eventStringForPaymentType:paymentType]];
- [self.apiClient sendAnalyticsEvent:eventName];
- }
- }
- // User canceled by breaking out of the PayPal browser switch flow
- // (e.g. System "Cancel" button on permission alert or browser during ASWebAuthenticationSession)
- NSError *err = [NSError errorWithDomain:BTPayPalDriverErrorDomain
- code:BTPayPalDriverErrorTypeCanceled
- userInfo:@{NSLocalizedDescriptionKey: @"PayPal flow was canceled by the user."}];
- if (completionBlock) {
- completionBlock(nil, err);
- }
- return;
- }
- [self handleBrowserSwitchReturnURL:callbackURL
- paymentType:paymentType
- completion:completionBlock];
- }];
- if (@available(iOS 13, *)) {
- self.authenticationSession.presentationContextProvider = self;
- }
- self.returnedToAppAfterPermissionAlert = NO;
- self.isAuthenticationSessionStarted = [self.authenticationSession start];
- if (self.isAuthenticationSessionStarted) {
- NSString *eventName = [NSString stringWithFormat:@"ios.%@.authsession.start.succeeded", [self.class eventStringForPaymentType:paymentType]];
- [self.apiClient sendAnalyticsEvent:eventName];
- } else {
- NSString *eventName = [NSString stringWithFormat:@"ios.%@.authsession.start.failed", [self.class eventStringForPaymentType:paymentType]];
- [self.apiClient sendAnalyticsEvent:eventName];
- }
- }
- - (BTClientMetadata *)clientMetadata {
- BTMutableClientMetadata *metadata = [self.apiClient.metadata mutableCopy];
- metadata.source = BTClientMetadataSourcePayPalBrowser;
- return [metadata copy];
- }
- + (BTPostalAddress *)accountAddressFromJSON:(BTJSON *)addressJSON {
- if (!addressJSON.isObject) {
- return nil;
- }
-
- BTPostalAddress *address = [[BTPostalAddress alloc] init];
- address.recipientName = [addressJSON[@"recipientName"] asString]; // Likely to be nil
- address.streetAddress = [addressJSON[@"street1"] asString];
- address.extendedAddress = [addressJSON[@"street2"] asString];
- address.locality = [addressJSON[@"city"] asString];
- address.region = [addressJSON[@"state"] asString];
- address.postalCode = [addressJSON[@"postalCode"] asString];
- address.countryCodeAlpha2 = [addressJSON[@"country"] asString];
-
- return address;
- }
- + (BTPostalAddress *)shippingOrBillingAddressFromJSON:(BTJSON *)addressJSON {
- if (!addressJSON.isObject) {
- return nil;
- }
-
- BTPostalAddress *address = [[BTPostalAddress alloc] init];
- address.recipientName = [addressJSON[@"recipientName"] asString]; // Likely to be nil
- address.streetAddress = [addressJSON[@"line1"] asString];
- address.extendedAddress = [addressJSON[@"line2"] asString];
- address.locality = [addressJSON[@"city"] asString];
- address.region = [addressJSON[@"state"] asString];
- address.postalCode = [addressJSON[@"postalCode"] asString];
- address.countryCodeAlpha2 = [addressJSON[@"countryCode"] asString];
-
- return address;
- }
- + (BTPayPalCreditFinancingAmount *)creditFinancingAmountFromJSON:(BTJSON *)amountJSON {
- if (!amountJSON.isObject) {
- return nil;
- }
- NSString *currency = [amountJSON[@"currency"] asString];
- NSString *value = [amountJSON[@"value"] asString];
- return [[BTPayPalCreditFinancingAmount alloc] initWithCurrency:currency value:value];
- }
- + (BTPayPalCreditFinancing *)creditFinancingFromJSON:(BTJSON *)creditFinancingOfferedJSON {
- if (!creditFinancingOfferedJSON.isObject) {
- return nil;
- }
- BOOL isCardAmountImmutable = [creditFinancingOfferedJSON[@"cardAmountImmutable"] isTrue];
- BTPayPalCreditFinancingAmount *monthlyPayment = [self.class creditFinancingAmountFromJSON:creditFinancingOfferedJSON[@"monthlyPayment"]];
- BOOL payerAcceptance = [creditFinancingOfferedJSON[@"payerAcceptance"] isTrue];
- NSInteger term = [creditFinancingOfferedJSON[@"term"] asIntegerOrZero];
- BTPayPalCreditFinancingAmount *totalCost = [self.class creditFinancingAmountFromJSON:creditFinancingOfferedJSON[@"totalCost"]];
- BTPayPalCreditFinancingAmount *totalInterest = [self.class creditFinancingAmountFromJSON:creditFinancingOfferedJSON[@"totalInterest"]];
- return [[BTPayPalCreditFinancing alloc] initWithCardAmountImmutable:isCardAmountImmutable
- monthlyPayment:monthlyPayment
- payerAcceptance:payerAcceptance
- term:term
- totalCost:totalCost
- totalInterest:totalInterest];
- }
- + (BTPayPalAccountNonce *)payPalAccountFromJSON:(BTJSON *)payPalAccount {
- NSString *nonce = [payPalAccount[@"nonce"] asString];
-
- BTJSON *details = payPalAccount[@"details"];
-
- NSString *email = [details[@"email"] asString];
- NSString *clientMetadataID = [details[@"correlationId"] asString];
- // Allow email to be under payerInfo
- if ([details[@"payerInfo"][@"email"] isString]) {
- email = [details[@"payerInfo"][@"email"] asString];
- }
-
- NSString *firstName = [details[@"payerInfo"][@"firstName"] asString];
- NSString *lastName = [details[@"payerInfo"][@"lastName"] asString];
- NSString *phone = [details[@"payerInfo"][@"phone"] asString];
- NSString *payerID = [details[@"payerInfo"][@"payerId"] asString];
- BOOL isDefault = [payPalAccount[@"default"] isTrue];
-
- BTPostalAddress *shippingAddress = [self.class shippingOrBillingAddressFromJSON:details[@"payerInfo"][@"shippingAddress"]];
- BTPostalAddress *billingAddress = [self.class shippingOrBillingAddressFromJSON:details[@"payerInfo"][@"billingAddress"]];
- if (!shippingAddress) {
- shippingAddress = [self.class accountAddressFromJSON:details[@"payerInfo"][@"accountAddress"]];
- }
- BTPayPalCreditFinancing *creditFinancing = [self.class creditFinancingFromJSON:details[@"creditFinancingOffered"]];
- BTPayPalAccountNonce *tokenizedPayPalAccount = [[BTPayPalAccountNonce alloc] initWithNonce:nonce
- email:email
- firstName:firstName
- lastName:lastName
- phone:phone
- billingAddress:billingAddress
- shippingAddress:shippingAddress
- clientMetadataID:clientMetadataID
- payerID:payerID
- isDefault:isDefault
- creditFinancing:creditFinancing];
-
- return tokenizedPayPalAccount;
- }
- #pragma mark - ASWebAuthenticationPresentationContextProviding protocol
- - (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session API_AVAILABLE(ios(13)) NS_EXTENSION_UNAVAILABLE("Uses APIs (i.e UIApplication.sharedApplication) not available for use in App Extensions.") {
- if (self.payPalRequest.activeWindow) {
- return self.payPalRequest.activeWindow;
- }
- for (UIScene* scene in UIApplication.sharedApplication.connectedScenes) {
- if (scene.activationState == UISceneActivationStateForegroundActive) {
- UIWindowScene *windowScene = (UIWindowScene *)scene;
- return windowScene.windows.firstObject;
- }
- }
- if (@available(iOS 15, *)) {
- return ((UIWindowScene *)UIApplication.sharedApplication.connectedScenes.allObjects.firstObject).windows.firstObject;
- } else {
- return UIApplication.sharedApplication.windows.firstObject;
- }
- }
- #pragma mark - Preflight check
- - (BOOL)verifyAppSwitchWithRemoteConfiguration:(BTJSON *)configuration error:(NSError * __autoreleasing *)error {
- if (![configuration[@"paypalEnabled"] isTrue]) {
- [self.apiClient sendAnalyticsEvent:@"ios.paypal-otc.preflight.disabled"];
- if (error != NULL) {
- *error = [NSError errorWithDomain:BTPayPalDriverErrorDomain
- code:BTPayPalDriverErrorTypeDisabled
- userInfo:@{ NSLocalizedDescriptionKey: @"PayPal is not enabled for this merchant",
- NSLocalizedRecoverySuggestionErrorKey: @"Enable PayPal for this merchant in the Braintree Control Panel" }];
- }
- return NO;
- }
- return YES;
- }
- #pragma mark - Analytics Helpers
- + (NSString *)eventStringForPaymentType:(BTPayPalPaymentType)paymentType {
- switch (paymentType) {
- case BTPayPalPaymentTypeVault:
- return @"paypal-ba";
- case BTPayPalPaymentTypeCheckout:
- return @"paypal-single-payment";
- default:
- return nil;
- }
- }
- - (void)sendAnalyticsEventForInitiatingOneTouchForPaymentType:(BTPayPalPaymentType)paymentType
- withSuccess:(BOOL)success {
- NSString *eventName = [NSString stringWithFormat:@"ios.%@.webswitch.initiate.%@", [self.class eventStringForPaymentType:paymentType], success ? @"started" : @"failed"];
- [self.apiClient sendAnalyticsEvent:eventName];
- if ([self.payPalRequest isKindOfClass:BTPayPalCheckoutRequest.class] && ((BTPayPalCheckoutRequest *)self.payPalRequest).offerPayLater) {
- NSString *eventName = [NSString stringWithFormat:@"ios.%@.webswitch.paylater.offered.%@", [self.class eventStringForPaymentType:paymentType], success ? @"started" : @"failed"];
- [self.apiClient sendAnalyticsEvent:eventName];
- }
- if ([self.payPalRequest isKindOfClass:BTPayPalVaultRequest.class] && ((BTPayPalVaultRequest *)self.payPalRequest).offerCredit) {
- NSString *eventName = [NSString stringWithFormat:@"ios.%@.webswitch.credit.offered.%@", [self.class eventStringForPaymentType:paymentType], success ? @"started" : @"failed"];
- [self.apiClient sendAnalyticsEvent:eventName];
- }
- }
- - (void)sendAnalyticsEventIfCreditFinancingInNonce:(BTPayPalAccountNonce *)payPalAccountNonce forPaymentType:(BTPayPalPaymentType)paymentType {
- if (payPalAccountNonce.creditFinancing) {
- NSString *eventName = [NSString stringWithFormat:@"ios.%@.credit.accepted", [self.class eventStringForPaymentType:paymentType]];
- [self.apiClient sendAnalyticsEvent:eventName];
- }
- }
- - (void)sendAnalyticsEventForTokenizationSuccessForPaymentType:(BTPayPalPaymentType)paymentType {
- NSString *eventName = [NSString stringWithFormat:@"ios.%@.tokenize.succeeded", [self.class eventStringForPaymentType:paymentType]];
- [self.apiClient sendAnalyticsEvent:eventName];
- }
- - (void)sendAnalyticsEventForTokenizationFailureForPaymentType:(BTPayPalPaymentType)paymentType {
- NSString *eventName = [NSString stringWithFormat:@"ios.%@.tokenize.failed", [self.class eventStringForPaymentType:paymentType]];
- [self.apiClient sendAnalyticsEvent:eventName];
- }
- #pragma mark - Internal
- - (NSURL *)decorateApprovalURL:(NSURL*)approvalURL forRequest:(BTPayPalRequest *)paypalRequest {
- if (approvalURL != nil && [paypalRequest isKindOfClass:BTPayPalCheckoutRequest.class]) {
- NSURLComponents* approvalURLComponents = [[NSURLComponents alloc] initWithURL:approvalURL resolvingAgainstBaseURL:NO];
- if (approvalURLComponents != nil) {
- NSString *userActionValue = ((BTPayPalCheckoutRequest *)paypalRequest).userActionAsString;
- if (userActionValue.length > 0) {
- NSURLQueryItem *userActionQueryItem = [[NSURLQueryItem alloc] initWithName:@"useraction" value:userActionValue];
- NSArray<NSURLQueryItem *> *queryItems = approvalURLComponents.queryItems ?: @[];
- approvalURLComponents.queryItems = [queryItems arrayByAddingObject:userActionQueryItem];
- }
- return approvalURLComponents.URL;
- }
- }
- return approvalURL;
- }
- #pragma mark - Browser Switch handling
- - (void)handleBrowserSwitchReturnURL:(NSURL *)url
- paymentType:(BTPayPalPaymentType)paymentType
- completion:(void (^)(BTPayPalAccountNonce *tokenizedCheckout, NSError *error))completionBlock {
- if (![self.class isValidURLAction: url]) {
- NSError *responseError = [NSError errorWithDomain:BTPayPalDriverErrorDomain
- code:BTPayPalDriverErrorTypeUnknown
- userInfo:@{ NSLocalizedDescriptionKey: @"Unexpected response" }];
- if (completionBlock) {
- completionBlock(nil, responseError);
- }
- return;
- }
- NSDictionary *response = [self dictionaryFromResponseURL:url];
- if (!response) {
- if (completionBlock) {
- // If there's no response, the user canceled out of the flow using the cancel link
- // on the PayPal website
- NSError *err = [NSError errorWithDomain:BTPayPalDriverErrorDomain
- code:BTPayPalDriverErrorTypeCanceled
- userInfo:@{NSLocalizedDescriptionKey: @"PayPal flow was canceled by the user."}];
- completionBlock(nil, err);
- }
- return;
- }
- NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
- parameters[@"paypal_account"] = [response mutableCopy];
- if (paymentType == BTPayPalPaymentTypeCheckout) {
- parameters[@"paypal_account"][@"options"] = @{ @"validate": @NO };
- if ([self.payPalRequest isKindOfClass:BTPayPalCheckoutRequest.class] && ((BTPayPalCheckoutRequest *) self.payPalRequest).intentAsString) {
- parameters[@"paypal_account"][@"intent"] = ((BTPayPalCheckoutRequest *) self.payPalRequest).intentAsString;
- }
- }
- if (self.clientMetadataID) {
- parameters[@"paypal_account"][@"correlation_id"] = self.clientMetadataID;
- }
- if (self.payPalRequest != nil && self.payPalRequest.merchantAccountID != nil) {
- parameters[@"merchant_account_id"] = self.payPalRequest.merchantAccountID;
- }
- BTClientMetadata *metadata = [self clientMetadata];
- parameters[@"_meta"] = @{
- @"source" : metadata.sourceString,
- @"integration" : metadata.integrationString,
- @"sessionId" : metadata.sessionID,
- };
- [self.apiClient POST:@"/v1/payment_methods/paypal_accounts"
- parameters:parameters
- completion:^(BTJSON *body, __unused NSHTTPURLResponse *response, NSError *error) {
- if (error) {
- if (error.code == NETWORK_CONNECTION_LOST_CODE) {
- [self.apiClient sendAnalyticsEvent:@"ios.paypal.handle-browser-switch.network-connection.failure"];
- }
- [self sendAnalyticsEventForTokenizationFailureForPaymentType:paymentType];
- if (completionBlock) {
- completionBlock(nil, error);
- }
- return;
- }
- [self sendAnalyticsEventForTokenizationSuccessForPaymentType:paymentType];
- BTJSON *payPalAccount = body[@"paypalAccounts"][0];
- BTPayPalAccountNonce *tokenizedAccount = [self.class payPalAccountFromJSON:payPalAccount];
- [self sendAnalyticsEventIfCreditFinancingInNonce:tokenizedAccount forPaymentType:paymentType];
- if (completionBlock) {
- completionBlock(tokenizedAccount, nil);
- }
- }];
- }
- #pragma mark - Class Methods
- + (NSString *)tokenFromApprovalURL:(NSURL *)approvalURL {
- NSDictionary *queryDictionary = [self parseQueryString:[approvalURL query]];
- return queryDictionary[@"token"] ?: queryDictionary[@"ba_token"];
- }
- + (NSDictionary *)parseQueryString:(NSString *)query {
- NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:6];
- NSArray *pairs = [query componentsSeparatedByString:@"&"];
- for (NSString *pair in pairs) {
- NSArray *elements = [pair componentsSeparatedByString:@"="];
- if (elements.count > 1) {
- NSString *key = [[elements objectAtIndex:0] stringByRemovingPercentEncoding];
- NSString *val = [[elements objectAtIndex:1] stringByRemovingPercentEncoding];
- if (key.length && val.length) {
- dict[key] = val;
- }
- }
- }
- return dict;
- }
- + (BOOL)isValidURLAction:(NSURL *)url {
- NSString *scheme = url.scheme;
- if (!scheme.length) {
- return NO;
- }
- NSString *hostAndPath = [url.host stringByAppendingString:url.path];
- NSMutableArray *pathComponents = [[hostAndPath componentsSeparatedByString:@"/"] mutableCopy];
- [pathComponents removeLastObject]; // remove the action (`success`, `cancel`, etc)
- hostAndPath = [pathComponents componentsJoinedByString:@"/"];
- if ([hostAndPath length]) {
- hostAndPath = [hostAndPath stringByAppendingString:@"/"];
- }
- if (![hostAndPath isEqualToString:BTPayPalCallbackURLHostAndPath]) {
- return NO;
- }
- NSString *action = [self actionFromURLAction:url];
- if (!action.length) {
- return NO;
- }
- NSArray *validActions = @[@"success", @"cancel", @"authenticate"];
- if (![validActions containsObject:action]) {
- return NO;
- }
- NSString *query = [url query];
- if (!query.length) {
- // should always have at least a payload or else a Hermes token (even if the action is "cancel")
- return NO;
- }
- return YES;
- }
- + (NSString *)actionFromURLAction:(NSURL *)url {
- NSString *action = [url.lastPathComponent componentsSeparatedByString:@"?"][0];
- if (![action length]) {
- action = url.host;
- }
- return action;
- }
- @end
|