BTPayPalDriver.m 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152
  1. #import "BTPayPalDriver_Internal.h"
  2. #if __has_include("PayPalOneTouch.h")
  3. #import "PPOTRequest.h"
  4. #import "PPOTCore.h"
  5. #else
  6. #import <PayPalOneTouch/PPOTRequest.h>
  7. #import <PayPalOneTouch/PPOTCore.h>
  8. #endif
  9. #if __has_include("BraintreeCore.h")
  10. #import "BTAPIClient_Internal.h"
  11. #import "BTPayPalAccountNonce_Internal.h"
  12. #import "BTPostalAddress.h"
  13. #import "BTLogger_Internal.h"
  14. #else
  15. #import <BraintreeCore/BTAPIClient_Internal.h>
  16. #import <BraintreeCore/BTPostalAddress.h>
  17. #import <BraintreeCore/BTLogger_Internal.h>
  18. #endif
  19. #if __has_include("BTPayPalAccountNonce_Internal.h")
  20. #import "BTPayPalAccountNonce_Internal.h"
  21. #else
  22. #import <BraintreePayPal/BTPayPalAccountNonce_Internal.h>
  23. #endif
  24. #import <SafariServices/SafariServices.h>
  25. #import "BTConfiguration+PayPal.h"
  26. #import "BTPayPalLineItem.h"
  27. NSString *const BTPayPalDriverErrorDomain = @"com.braintreepayments.BTPayPalDriverErrorDomain";
  28. NSString *const BTSFAuthenticationSessionDisabled = @"sfAuthenticationSessionDisabled";
  29. static void (^appSwitchReturnBlock)(NSURL *url);
  30. typedef NS_ENUM(NSUInteger, BTPayPalPaymentType) {
  31. BTPayPalPaymentTypeUnknown = 0,
  32. BTPayPalPaymentTypeFuturePayments,
  33. BTPayPalPaymentTypeCheckout,
  34. BTPayPalPaymentTypeBillingAgreement,
  35. };
  36. @interface BTPayPalDriver () <SFSafariViewControllerDelegate, UIViewControllerTransitioningDelegate>
  37. @property (nonatomic, assign) BOOL becameActiveAfterSFAuthenticationSessionModal;
  38. @end
  39. @implementation BTPayPalDriver
  40. + (void)load {
  41. if (self == [BTPayPalDriver class]) {
  42. PayPalClass = [PPOTCore class];
  43. [[BTAppSwitch sharedInstance] registerAppSwitchHandler:self];
  44. [[BTTokenizationService sharedService] registerType:@"PayPal" withTokenizationBlock:^(BTAPIClient *apiClient, __unused NSDictionary *options, void (^completionBlock)(BTPaymentMethodNonce *paymentMethodNonce, NSError *error)) {
  45. BTPayPalDriver *driver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient];
  46. driver.viewControllerPresentingDelegate = options[BTTokenizationServiceViewPresentingDelegateOption];
  47. driver.appSwitchDelegate = options[BTTokenizationServiceAppSwitchDelegateOption];
  48. [driver authorizeAccountWithAdditionalScopes:options[BTTokenizationServicePayPalScopesOption] completion:completionBlock];
  49. }];
  50. [[BTPaymentMethodNonceParser sharedParser] registerType:@"PayPalAccount" withParsingBlock:^BTPaymentMethodNonce * _Nullable(BTJSON * _Nonnull payPalAccount) {
  51. return [self payPalAccountFromJSON:payPalAccount];
  52. }];
  53. }
  54. }
  55. - (instancetype)initWithAPIClient:(BTAPIClient *)apiClient {
  56. if (self = [super init]) {
  57. BTClientMetadataSourceType source = [self isiOSAppAvailableForAppSwitch] ? BTClientMetadataSourcePayPalApp : BTClientMetadataSourcePayPalBrowser;
  58. _apiClient = [apiClient copyWithSource:source integration:apiClient.metadata.integration];
  59. [[NSNotificationCenter defaultCenter] addObserver:self
  60. selector:@selector(applicationDidBecomeActive:)
  61. name:UIApplicationDidBecomeActiveNotification
  62. object:nil];
  63. }
  64. return self;
  65. }
  66. - (instancetype)init {
  67. return nil;
  68. }
  69. - (void)applicationDidBecomeActive:(__unused NSNotification *)notification
  70. {
  71. if (self.isSFAuthenticationSessionStarted) {
  72. self.becameActiveAfterSFAuthenticationSessionModal = YES;
  73. }
  74. }
  75. - (void)dealloc {
  76. [[NSNotificationCenter defaultCenter] removeObserver:self];
  77. }
  78. #pragma mark - Authorization (Future Payments)
  79. - (void)authorizeAccountWithCompletion:(void (^)(BTPayPalAccountNonce *paymentMethod, NSError *error))completionBlock {
  80. [self authorizeAccountWithAdditionalScopes:[NSSet set] completion:completionBlock];
  81. }
  82. - (void)authorizeAccountWithAdditionalScopes:(NSSet<NSString *> *)additionalScopes completion:(void (^)(BTPayPalAccountNonce *, NSError *))completionBlock {
  83. [self authorizeAccountWithAdditionalScopes:additionalScopes
  84. forceFuturePaymentFlow:false
  85. completion:completionBlock];
  86. }
  87. - (void)authorizeAccountWithAdditionalScopes:(NSSet<NSString *> *)additionalScopes
  88. forceFuturePaymentFlow:(BOOL)forceFuturePaymentFlow
  89. completion:(void (^)(BTPayPalAccountNonce *, NSError *))completionBlock {
  90. if (!self.apiClient) {
  91. NSError *error = [NSError errorWithDomain:BTPayPalDriverErrorDomain
  92. code:BTPayPalDriverErrorTypeIntegration
  93. userInfo:@{NSLocalizedDescriptionKey: @"BTPayPalDriver failed because BTAPIClient is nil."}];
  94. completionBlock(nil, error);
  95. return;
  96. }
  97. [self setAuthorizationAppSwitchReturnBlock:completionBlock];
  98. [self.apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) {
  99. if (error) {
  100. if (completionBlock) {
  101. completionBlock(nil, error);
  102. }
  103. return;
  104. }
  105. self.disableSFAuthenticationSession = [configuration.json[BTSFAuthenticationSessionDisabled] isTrue] || self.disableSFAuthenticationSession;
  106. if (configuration.isBillingAgreementsEnabled && !forceFuturePaymentFlow) {
  107. // Switch to Billing Agreements flow
  108. BTPayPalRequest *payPalRequest = [[BTPayPalRequest alloc] init]; // Drop-in only supports Vault flow, which does not use currency code or amount
  109. [self requestBillingAgreement:payPalRequest completion:completionBlock];
  110. return;
  111. }
  112. if (![self verifyAppSwitchWithRemoteConfiguration:configuration.json error:&error]) {
  113. if (completionBlock) {
  114. completionBlock(nil, error);
  115. }
  116. return;
  117. }
  118. PPOTAuthorizationRequest *request =
  119. [self.requestFactory requestWithScopeValues:[self.defaultOAuth2Scopes setByAddingObjectsFromSet:(additionalScopes ? additionalScopes : [NSSet set])]
  120. privacyURL:[configuration.json[@"paypal"][@"privacyUrl"] asURL]
  121. agreementURL:[configuration.json[@"paypal"][@"userAgreementUrl"] asURL]
  122. clientID:[self paypalClientIdWithRemoteConfiguration:configuration.json]
  123. environment:[self payPalEnvironmentForRemoteConfiguration:configuration.json]
  124. callbackURLScheme:self.returnURLScheme];
  125. if (self.apiClient.clientToken) {
  126. request.additionalPayloadAttributes = @{ @"client_token": self.apiClient.clientToken.originalValue };
  127. } else if (self.apiClient.tokenizationKey) {
  128. request.additionalPayloadAttributes = @{ @"client_key": self.apiClient.tokenizationKey };
  129. }
  130. if (@available(iOS 9.0, *)) {
  131. // will use in-app browser
  132. } else {
  133. [self informDelegateWillPerformAppSwitch];
  134. }
  135. [request performWithAdapterBlock:^(BOOL success, NSURL *url, PPOTRequestTarget target, NSString *clientMetadataId, NSError *error) {
  136. self.clientMetadataId = clientMetadataId;
  137. [self sendAnalyticsEventForInitiatingOneTouchForPaymentType:BTPayPalPaymentTypeFuturePayments
  138. withSuccess:success
  139. target:target];
  140. [self handlePayPalRequestWithSuccess:success
  141. error:error
  142. requestURL:url
  143. target:target
  144. paymentType:BTPayPalPaymentTypeFuturePayments
  145. completion:completionBlock];
  146. }];
  147. }];
  148. }
  149. - (void)setAuthorizationAppSwitchReturnBlock:(void (^)(BTPayPalAccountNonce *account, NSError *error))completionBlock {
  150. [self setAppSwitchReturnBlock:completionBlock forPaymentType:BTPayPalPaymentTypeFuturePayments];
  151. }
  152. #pragma mark - Billing Agreement
  153. - (void)requestBillingAgreement:(BTPayPalRequest *)request completion:(void (^)(BTPayPalAccountNonce *tokenizedCheckout, NSError *error))completionBlock {
  154. [self requestExpressCheckout:request
  155. isBillingAgreement:YES
  156. handler:nil
  157. completion:completionBlock];
  158. }
  159. - (void)requestBillingAgreement:(BTPayPalRequest *)request
  160. handler:(id<BTPayPalApprovalHandler>)handler
  161. completion:(void (^)(BTPayPalAccountNonce * _Nullable, NSError * _Nullable))completionBlock {
  162. [self requestExpressCheckout:request
  163. isBillingAgreement:YES
  164. handler:handler
  165. completion:completionBlock];
  166. }
  167. - (void)setBillingAgreementAppSwitchReturnBlock:(void (^)(BTPayPalAccountNonce *tokenizedAccount, NSError *error))completionBlock {
  168. [self setAppSwitchReturnBlock:completionBlock forPaymentType:BTPayPalPaymentTypeBillingAgreement];
  169. }
  170. #pragma mark - Express Checkout (One-Time Payments)
  171. - (void)requestOneTimePayment:(BTPayPalRequest *)request completion:(void (^)(BTPayPalAccountNonce *tokenizedCheckout, NSError *error))completionBlock {
  172. [self requestExpressCheckout:request
  173. isBillingAgreement:NO
  174. handler:nil
  175. completion:completionBlock];
  176. }
  177. - (void)requestOneTimePayment:(BTPayPalRequest *)request
  178. handler:(id<BTPayPalApprovalHandler>)handler
  179. completion:(void (^)(BTPayPalAccountNonce *tokenizedCheckout, NSError *error))completionBlock {
  180. [self requestExpressCheckout:request
  181. isBillingAgreement:NO
  182. handler:handler
  183. completion:completionBlock];
  184. }
  185. - (void)setOneTimePaymentAppSwitchReturnBlock:(void (^)(BTPayPalAccountNonce *tokenizedAccount, NSError *error))completionBlock {
  186. [self setAppSwitchReturnBlock:completionBlock forPaymentType:BTPayPalPaymentTypeCheckout];
  187. }
  188. #pragma mark - Helpers
  189. /// A "Hermes checkout" is used by both Billing Agreements and Express Checkout
  190. - (void)requestExpressCheckout:(BTPayPalRequest *)request
  191. isBillingAgreement:(BOOL)isBillingAgreement
  192. handler:(id<BTPayPalApprovalHandler>)handler
  193. completion:(void (^)(BTPayPalAccountNonce *tokenizedCheckout, NSError *error))completionBlock {
  194. if (!self.apiClient) {
  195. NSError *error = [NSError errorWithDomain:BTPayPalDriverErrorDomain
  196. code:BTPayPalDriverErrorTypeIntegration
  197. userInfo:@{NSLocalizedDescriptionKey: @"BTPayPalDriver failed because BTAPIClient is nil."}];
  198. completionBlock(nil, error);
  199. return;
  200. }
  201. if (!request || (!isBillingAgreement && !request.amount)) {
  202. completionBlock(nil, [NSError errorWithDomain:BTPayPalDriverErrorDomain code:BTPayPalDriverErrorTypeInvalidRequest userInfo:nil]);
  203. return;
  204. }
  205. [self.apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) {
  206. if (error) {
  207. if (completionBlock) completionBlock(nil, error);
  208. return;
  209. }
  210. if (![self verifyAppSwitchWithRemoteConfiguration:configuration.json error:&error]) {
  211. if (completionBlock) completionBlock(nil, error);
  212. return;
  213. }
  214. self.disableSFAuthenticationSession = [configuration.json[BTSFAuthenticationSessionDisabled] isTrue] || self.disableSFAuthenticationSession;
  215. NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
  216. NSMutableDictionary *experienceProfile = [NSMutableDictionary dictionary];
  217. if (!isBillingAgreement) {
  218. parameters[@"intent"] = [self.class intentTypeToString:request.intent];
  219. if (request.amount != nil) {
  220. parameters[@"amount"] = request.amount;
  221. }
  222. } else {
  223. if (request.billingAgreementDescription.length > 0) {
  224. parameters[@"description"] = request.billingAgreementDescription;
  225. }
  226. }
  227. parameters[@"offer_paypal_credit"] = @(request.offerCredit);
  228. experienceProfile[@"no_shipping"] = @(!request.isShippingAddressRequired);
  229. experienceProfile[@"brand_name"] = request.displayName ?: [configuration.json[@"paypal"][@"displayName"] asString];
  230. NSString *landingPageTypeValue = [self.class landingPageTypeToString:request.landingPageType];
  231. if (landingPageTypeValue != nil) {
  232. experienceProfile[@"landing_page_type"] = landingPageTypeValue;
  233. }
  234. if (request.localeCode != nil) {
  235. experienceProfile[@"locale_code"] = request.localeCode;
  236. }
  237. if (request.merchantAccountId != nil) {
  238. parameters[@"merchant_account_id"] = request.merchantAccountId;
  239. }
  240. // Currency code should only be used for Hermes Checkout (one-time payment).
  241. // For BA, currency should not be used.
  242. NSString *currencyCode = request.currencyCode ?: [configuration.json[@"paypal"][@"currencyIsoCode"] asString];
  243. if (!isBillingAgreement && currencyCode) {
  244. parameters[@"currency_iso_code"] = currencyCode;
  245. }
  246. if (request.shippingAddressOverride != nil) {
  247. experienceProfile[@"address_override"] = @(!request.isShippingAddressEditable);
  248. BTPostalAddress *shippingAddress = request.shippingAddressOverride;
  249. if (isBillingAgreement) {
  250. NSMutableDictionary *shippingAddressParams = [NSMutableDictionary dictionary];
  251. shippingAddressParams[@"line1"] = shippingAddress.streetAddress;
  252. shippingAddressParams[@"line2"] = shippingAddress.extendedAddress;
  253. shippingAddressParams[@"city"] = shippingAddress.locality;
  254. shippingAddressParams[@"state"] = shippingAddress.region;
  255. shippingAddressParams[@"postal_code"] = shippingAddress.postalCode;
  256. shippingAddressParams[@"country_code"] = shippingAddress.countryCodeAlpha2;
  257. shippingAddressParams[@"recipient_name"] = shippingAddress.recipientName;
  258. parameters[@"shipping_address"] = shippingAddressParams;
  259. } else {
  260. parameters[@"line1"] = shippingAddress.streetAddress;
  261. parameters[@"line2"] = shippingAddress.extendedAddress;
  262. parameters[@"city"] = shippingAddress.locality;
  263. parameters[@"state"] = shippingAddress.region;
  264. parameters[@"postal_code"] = shippingAddress.postalCode;
  265. parameters[@"country_code"] = shippingAddress.countryCodeAlpha2;
  266. parameters[@"recipient_name"] = shippingAddress.recipientName;
  267. }
  268. } else {
  269. experienceProfile[@"address_override"] = @NO;
  270. }
  271. if (request.lineItems.count > 0) {
  272. NSMutableArray *lineItemsArray = [NSMutableArray arrayWithCapacity:request.lineItems.count];
  273. for (BTPayPalLineItem *lineItem in request.lineItems) {
  274. [lineItemsArray addObject:[lineItem requestParameters]];
  275. }
  276. parameters[@"line_items"] = lineItemsArray;
  277. }
  278. NSString *returnURI;
  279. NSString *cancelURI;
  280. [[self.class payPalClass] redirectURLsForCallbackURLScheme:self.returnURLScheme
  281. withReturnURL:&returnURI
  282. withCancelURL:&cancelURI];
  283. if (!returnURI || !cancelURI) {
  284. completionBlock(nil, [NSError errorWithDomain:BTPayPalDriverErrorDomain
  285. code:BTPayPalDriverErrorTypeIntegrationReturnURLScheme
  286. userInfo:@{NSLocalizedFailureReasonErrorKey: @"Application may not support One Touch callback URL scheme.",
  287. NSLocalizedRecoverySuggestionErrorKey: @"Check the return URL scheme" }]);
  288. return;
  289. }
  290. if (returnURI) {
  291. parameters[@"return_url"] = returnURI;
  292. }
  293. if (cancelURI) {
  294. parameters[@"cancel_url"] = cancelURI;
  295. }
  296. parameters[@"experience_profile"] = experienceProfile;
  297. self.payPalRequest = request;
  298. NSString *url = isBillingAgreement ? @"setup_billing_agreement" : @"create_payment_resource";
  299. [self.apiClient POST:[NSString stringWithFormat:@"v1/paypal_hermes/%@",url]
  300. parameters:parameters
  301. completion:^(BTJSON *body, __unused NSHTTPURLResponse *response, NSError *error) {
  302. if (error) {
  303. NSString *errorDetailsIssue = ((BTJSON *)error.userInfo[BTHTTPJSONResponseBodyKey][@"paymentResource"][@"errorDetails"][0][@"issue"]).asString;
  304. if (error.userInfo[NSLocalizedDescriptionKey] == nil && errorDetailsIssue != nil) {
  305. NSMutableDictionary *dictionary = [error.userInfo mutableCopy];
  306. dictionary[NSLocalizedDescriptionKey] = errorDetailsIssue;
  307. error = [NSError errorWithDomain:error.domain code:error.code userInfo:dictionary];
  308. }
  309. if (completionBlock) {
  310. completionBlock(nil, error);
  311. }
  312. return;
  313. }
  314. if (isBillingAgreement) {
  315. [self setBillingAgreementAppSwitchReturnBlock:completionBlock];
  316. } else {
  317. [self setOneTimePaymentAppSwitchReturnBlock:completionBlock];
  318. }
  319. NSString *payPalClientID = [configuration.json[@"paypal"][@"clientId"] asString];
  320. if (!payPalClientID && [self payPalEnvironmentForRemoteConfiguration:configuration.json] == PayPalEnvironmentMock) {
  321. payPalClientID = @"FAKE-PAYPAL-CLIENT-ID";
  322. }
  323. NSURL *approvalUrl = [body[@"paymentResource"][@"redirectUrl"] asURL];
  324. if (approvalUrl == nil) {
  325. approvalUrl = [body[@"agreementSetup"][@"approvalUrl"] asURL];
  326. }
  327. approvalUrl = [self decorateApprovalURL:approvalUrl forRequest:request];
  328. PPOTCheckoutRequest *request = nil;
  329. if (isBillingAgreement) {
  330. request = [self.requestFactory billingAgreementRequestWithApprovalURL:approvalUrl
  331. clientID:payPalClientID
  332. environment:[self payPalEnvironmentForRemoteConfiguration:configuration.json]
  333. callbackURLScheme:self.returnURLScheme];
  334. } else {
  335. request = [self.requestFactory checkoutRequestWithApprovalURL:approvalUrl
  336. clientID:payPalClientID
  337. environment:[self payPalEnvironmentForRemoteConfiguration:configuration.json]
  338. callbackURLScheme:self.returnURLScheme];
  339. }
  340. // Call custom handler and return before beginning the default approval process
  341. if (handler != nil) {
  342. [handler handleApproval:request paypalApprovalDelegate:self];
  343. return;
  344. }
  345. if (@available(iOS 9.0, *)) {
  346. // will use in-app browser
  347. } else {
  348. [self informDelegateWillPerformAppSwitch];
  349. }
  350. [request performWithAdapterBlock:^(BOOL success, NSURL *url, PPOTRequestTarget target, NSString *clientMetadataId, NSError *error) {
  351. self.clientMetadataId = clientMetadataId;
  352. if (isBillingAgreement) {
  353. [self sendAnalyticsEventForInitiatingOneTouchForPaymentType:BTPayPalPaymentTypeBillingAgreement
  354. withSuccess:success
  355. target:target];
  356. } else {
  357. [self sendAnalyticsEventForInitiatingOneTouchForPaymentType:BTPayPalPaymentTypeCheckout
  358. withSuccess:success
  359. target:target];
  360. }
  361. [self handlePayPalRequestWithSuccess:success
  362. error:error
  363. requestURL:url
  364. target:target
  365. paymentType:isBillingAgreement ? BTPayPalPaymentTypeBillingAgreement : BTPayPalPaymentTypeCheckout
  366. completion:completionBlock];
  367. }];
  368. }];
  369. }];
  370. }
  371. - (void)setAppSwitchReturnBlock:(void (^)(BTPayPalAccountNonce *tokenizedAccount, NSError *error))completionBlock
  372. forPaymentType:(BTPayPalPaymentType)paymentType {
  373. appSwitchReturnBlock = ^(NSURL *url) {
  374. [self informDelegateAppContextDidReturn];
  375. if (@available(iOS 11.0, *)) {
  376. if (self.safariAuthenticationSession) {
  377. // do nothing
  378. } else if (self.safariViewController) {
  379. [self informDelegatePresentingViewControllerNeedsDismissal];
  380. } else {
  381. [self informDelegateWillProcessAppSwitchReturn];
  382. }
  383. } else if (@available(iOS 9.0, *)) {
  384. [self informDelegatePresentingViewControllerNeedsDismissal];
  385. } else {
  386. [self informDelegateWillProcessAppSwitchReturn];
  387. }
  388. // Before parsing the return URL, check whether the user cancelled by breaking
  389. // out of the PayPal app switch flow (e.g. "Done" button in SFSafariViewController)
  390. if ([url.absoluteString isEqualToString:SFSafariViewControllerFinishedURL]) {
  391. if (completionBlock) completionBlock(nil, nil);
  392. appSwitchReturnBlock = nil;
  393. return;
  394. }
  395. [[self.class payPalClass] parseResponseURL:url completionBlock:^(PPOTResult *result) {
  396. [self sendAnalyticsEventForHandlingOneTouchResult:result forPaymentType:paymentType];
  397. switch (result.type) {
  398. case PPOTResultTypeError:
  399. if (completionBlock) completionBlock(nil, result.error);
  400. break;
  401. case PPOTResultTypeCancel:
  402. if (result.error) {
  403. [[BTLogger sharedLogger] error:@"PayPal error: %@", result.error];
  404. }
  405. if (completionBlock) completionBlock(nil, nil);
  406. break;
  407. case PPOTResultTypeSuccess: {
  408. NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
  409. parameters[@"paypal_account"] = [result.response mutableCopy];
  410. if (paymentType == BTPayPalPaymentTypeCheckout) {
  411. parameters[@"paypal_account"][@"options"] = @{ @"validate": @NO };
  412. if (self.payPalRequest) {
  413. parameters[@"paypal_account"][@"intent"] = [self.class intentTypeToString:self.payPalRequest.intent];
  414. }
  415. }
  416. if (self.clientMetadataId) {
  417. parameters[@"paypal_account"][@"correlation_id"] = self.clientMetadataId;
  418. }
  419. if (self.payPalRequest != nil && self.payPalRequest.merchantAccountId != nil) {
  420. parameters[@"merchant_account_id"] = self.payPalRequest.merchantAccountId;
  421. }
  422. BTClientMetadata *metadata = [self clientMetadata];
  423. parameters[@"_meta"] = @{
  424. @"source" : metadata.sourceString,
  425. @"integration" : metadata.integrationString,
  426. @"sessionId" : metadata.sessionId,
  427. };
  428. [self.apiClient POST:@"/v1/payment_methods/paypal_accounts"
  429. parameters:parameters
  430. completion:^(BTJSON *body, __unused NSHTTPURLResponse *response, NSError *error)
  431. {
  432. if (error) {
  433. [self sendAnalyticsEventForTokenizationFailureForPaymentType:paymentType];
  434. if (completionBlock) completionBlock(nil, error);
  435. return;
  436. }
  437. [self sendAnalyticsEventForTokenizationSuccessForPaymentType:paymentType];
  438. BTJSON *payPalAccount = body[@"paypalAccounts"][0];
  439. BTPayPalAccountNonce *tokenizedAccount = [self.class payPalAccountFromJSON:payPalAccount];
  440. [self sendAnalyticsEventIfCreditFinancingInNonce:tokenizedAccount forPaymentType:paymentType];
  441. if (completionBlock) {
  442. completionBlock(tokenizedAccount, nil);
  443. }
  444. }];
  445. break;
  446. }
  447. }
  448. appSwitchReturnBlock = nil;
  449. }];
  450. };
  451. }
  452. - (void)handlePayPalRequestWithSuccess:(BOOL)success
  453. error:(NSError *)error
  454. requestURL:(NSURL *)url
  455. target:(PPOTRequestTarget)target
  456. paymentType:(BTPayPalPaymentType)paymentType
  457. completion:(void (^)(BTPayPalAccountNonce *, NSError *))completionBlock {
  458. if (success) {
  459. // Defensive programming in case PayPal One Touch returns a non-HTTP URL so that SFSafariViewController doesn't crash
  460. if (@available(iOS 9.0, *)) {
  461. if (![url.scheme.lowercaseString hasPrefix:@"http"]) {
  462. NSError *urlError = [NSError errorWithDomain:BTPayPalDriverErrorDomain
  463. code:BTPayPalDriverErrorTypeUnknown
  464. userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Attempted to open an invalid URL in SFSafariViewController: %@://", url.scheme],
  465. NSLocalizedRecoverySuggestionErrorKey: @"Try again or contact Braintree Support." }];
  466. if (completionBlock) {
  467. completionBlock(nil, urlError);
  468. }
  469. NSString *eventName = [NSString stringWithFormat:@"ios.%@.%@.error.safariviewcontrollerbadscheme.%@", [self.class eventStringForPaymentType:paymentType], [self.class eventStringForRequestTarget:target], url.scheme];
  470. [self.apiClient sendAnalyticsEvent:eventName];
  471. return;
  472. }
  473. }
  474. [self performSwitchRequest:url];
  475. if (@available(iOS 9.0, *)) {
  476. // use in-app browser
  477. } else {
  478. [self informDelegateDidPerformAppSwitchToTarget:target];
  479. }
  480. } else {
  481. if (completionBlock) {
  482. completionBlock(nil, error);
  483. }
  484. }
  485. }
  486. - (void)performSwitchRequest:(NSURL *)appSwitchURL {
  487. [self informDelegateAppContextWillSwitch];
  488. if (@available(iOS 11.0, *)) {
  489. NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:appSwitchURL resolvingAgainstBaseURL:NO];
  490. if (self.disableSFAuthenticationSession) {
  491. // Append "force-one-touch" query param when One Touch functions correctly
  492. NSString *queryForAuthSession = [urlComponents.query stringByAppendingString:@"&bt_int_type=1"];
  493. urlComponents.query = queryForAuthSession;
  494. [self informDelegatePresentingViewControllerRequestPresent:urlComponents.URL];
  495. } else {
  496. NSString *queryForAuthSession = [urlComponents.query stringByAppendingString:@"&bt_int_type=2"];
  497. urlComponents.query = queryForAuthSession;
  498. self.safariAuthenticationSession = [[SFAuthenticationSession alloc] initWithURL:urlComponents.URL
  499. callbackURLScheme:self.returnURLScheme
  500. completionHandler:^(NSURL * _Nullable callbackURL, NSError * _Nullable error)
  501. {
  502. if (error) {
  503. if (error.domain == SFAuthenticationErrorDomain && error.code == SFAuthenticationErrorCanceledLogin) {
  504. if (self.becameActiveAfterSFAuthenticationSessionModal) {
  505. [self.apiClient sendAnalyticsEvent:@"ios.sfauthsession.cancel.web"];
  506. } else {
  507. [self.apiClient sendAnalyticsEvent:@"ios.sfauthsession.cancel.modal"];
  508. }
  509. }
  510. [self.class handleAppSwitchReturnURL:[NSURL URLWithString:SFSafariViewControllerFinishedURL]];
  511. return;
  512. }
  513. [BTAppSwitch handleOpenURL:callbackURL sourceApplication:@"com.apple.safariviewservice"];
  514. self.safariAuthenticationSession = nil;
  515. }];
  516. if (self.safariAuthenticationSession != nil) {
  517. self.becameActiveAfterSFAuthenticationSessionModal = NO;
  518. self.isSFAuthenticationSessionStarted = [self.safariAuthenticationSession start];
  519. if (self.isSFAuthenticationSessionStarted) {
  520. [self.apiClient sendAnalyticsEvent:@"ios.sfauthsession.start.succeeded"];
  521. } else {
  522. [self.apiClient sendAnalyticsEvent:@"ios.sfauthsession.start.failed"];
  523. }
  524. }
  525. }
  526. } else if (@available(iOS 9.0, *)) {
  527. NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:appSwitchURL resolvingAgainstBaseURL:NO];
  528. NSString *queryForAuthSession = [urlComponents.query stringByAppendingString:@"&bt_int_type=1"];
  529. urlComponents.query = queryForAuthSession;
  530. [self informDelegatePresentingViewControllerRequestPresent:urlComponents.URL];
  531. } else {
  532. UIApplication *application = [UIApplication sharedApplication];
  533. if (@available(iOS 10.0, *)) {
  534. [application openURL:appSwitchURL options:[NSDictionary dictionary] completionHandler:nil];
  535. } else {
  536. #pragma clang diagnostic push
  537. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  538. [application openURL:appSwitchURL];
  539. #pragma clang diagnostic pop
  540. }
  541. }
  542. }
  543. - (NSString *)payPalEnvironmentForRemoteConfiguration:(BTJSON *)configuration {
  544. NSString *btPayPalEnvironmentName = [configuration[@"paypal"][@"environment"] asString];
  545. if ([btPayPalEnvironmentName isEqualToString:@"offline"]) {
  546. return PayPalEnvironmentMock;
  547. } else if ([btPayPalEnvironmentName isEqualToString:@"live"]) {
  548. return PayPalEnvironmentProduction;
  549. } else {
  550. // Fall back to mock when configuration has an unsupported value for environment, e.g. "custom"
  551. // Instead of returning btPayPalEnvironmentName
  552. return PayPalEnvironmentMock;
  553. }
  554. }
  555. - (NSString *)paypalClientIdWithRemoteConfiguration:(BTJSON *)configuration {
  556. if ([[configuration[@"paypal"][@"environment"] asString] isEqualToString:@"offline"] && ![configuration[@"paypal"][@"clientId"] isString]) {
  557. return @"mock-paypal-client-id";
  558. } else {
  559. return [configuration[@"paypal"][@"clientId"] asString];
  560. }
  561. }
  562. - (BTClientMetadata *)clientMetadata {
  563. BTMutableClientMetadata *metadata = [self.apiClient.metadata mutableCopy];
  564. if ([self isiOSAppAvailableForAppSwitch]) {
  565. metadata.source = BTClientMetadataSourcePayPalApp;
  566. } else {
  567. metadata.source = BTClientMetadataSourcePayPalBrowser;
  568. }
  569. return [metadata copy];
  570. }
  571. - (NSSet *)defaultOAuth2Scopes {
  572. return [NSSet setWithObjects:@"https://uri.paypal.com/services/payments/futurepayments", @"email", nil];
  573. }
  574. + (BTPostalAddress *)accountAddressFromJSON:(BTJSON *)addressJSON {
  575. if (!addressJSON.isObject) {
  576. return nil;
  577. }
  578. BTPostalAddress *address = [[BTPostalAddress alloc] init];
  579. address.recipientName = [addressJSON[@"recipientName"] asString]; // Likely to be nil
  580. address.streetAddress = [addressJSON[@"street1"] asString];
  581. address.extendedAddress = [addressJSON[@"street2"] asString];
  582. address.locality = [addressJSON[@"city"] asString];
  583. address.region = [addressJSON[@"state"] asString];
  584. address.postalCode = [addressJSON[@"postalCode"] asString];
  585. address.countryCodeAlpha2 = [addressJSON[@"country"] asString];
  586. return address;
  587. }
  588. + (BTPostalAddress *)shippingOrBillingAddressFromJSON:(BTJSON *)addressJSON {
  589. if (!addressJSON.isObject) {
  590. return nil;
  591. }
  592. BTPostalAddress *address = [[BTPostalAddress alloc] init];
  593. address.recipientName = [addressJSON[@"recipientName"] asString]; // Likely to be nil
  594. address.streetAddress = [addressJSON[@"line1"] asString];
  595. address.extendedAddress = [addressJSON[@"line2"] asString];
  596. address.locality = [addressJSON[@"city"] asString];
  597. address.region = [addressJSON[@"state"] asString];
  598. address.postalCode = [addressJSON[@"postalCode"] asString];
  599. address.countryCodeAlpha2 = [addressJSON[@"countryCode"] asString];
  600. return address;
  601. }
  602. + (BTPayPalCreditFinancingAmount *)creditFinancingAmountFromJSON:(BTJSON *)amountJSON {
  603. if (!amountJSON.isObject) {
  604. return nil;
  605. }
  606. NSString *currency = [amountJSON[@"currency"] asString];
  607. NSString *value = [amountJSON[@"value"] asString];
  608. return [[BTPayPalCreditFinancingAmount alloc] initWithCurrency:currency value:value];
  609. }
  610. + (BTPayPalCreditFinancing *)creditFinancingFromJSON:(BTJSON *)creditFinancingOfferedJSON {
  611. if (!creditFinancingOfferedJSON.isObject) {
  612. return nil;
  613. }
  614. BOOL isCardAmountImmutable = [creditFinancingOfferedJSON[@"cardAmountImmutable"] isTrue];
  615. BTPayPalCreditFinancingAmount *monthlyPayment = [self.class creditFinancingAmountFromJSON:creditFinancingOfferedJSON[@"monthlyPayment"]];
  616. BOOL payerAcceptance = [creditFinancingOfferedJSON[@"payerAcceptance"] isTrue];
  617. NSInteger term = [creditFinancingOfferedJSON[@"term"] asIntegerOrZero];
  618. BTPayPalCreditFinancingAmount *totalCost = [self.class creditFinancingAmountFromJSON:creditFinancingOfferedJSON[@"totalCost"]];
  619. BTPayPalCreditFinancingAmount *totalInterest = [self.class creditFinancingAmountFromJSON:creditFinancingOfferedJSON[@"totalInterest"]];
  620. return [[BTPayPalCreditFinancing alloc] initWithCardAmountImmutable:isCardAmountImmutable
  621. monthlyPayment:monthlyPayment
  622. payerAcceptance:payerAcceptance
  623. term:term
  624. totalCost:totalCost
  625. totalInterest:totalInterest];
  626. }
  627. + (BTPayPalAccountNonce *)payPalAccountFromJSON:(BTJSON *)payPalAccount {
  628. NSString *nonce = [payPalAccount[@"nonce"] asString];
  629. NSString *description = [payPalAccount[@"description"] asString];
  630. BTJSON *details = payPalAccount[@"details"];
  631. NSString *email = [details[@"email"] asString];
  632. NSString *clientMetadataId = [details[@"correlationId"] asString];
  633. // Allow email to be under payerInfo
  634. if ([details[@"payerInfo"][@"email"] isString]) {
  635. email = [details[@"payerInfo"][@"email"] asString];
  636. }
  637. NSString *firstName = [details[@"payerInfo"][@"firstName"] asString];
  638. NSString *lastName = [details[@"payerInfo"][@"lastName"] asString];
  639. NSString *phone = [details[@"payerInfo"][@"phone"] asString];
  640. NSString *payerId = [details[@"payerInfo"][@"payerId"] asString];
  641. BOOL isDefault = [payPalAccount[@"default"] isTrue];
  642. BTPostalAddress *shippingAddress = [self.class shippingOrBillingAddressFromJSON:details[@"payerInfo"][@"shippingAddress"]];
  643. BTPostalAddress *billingAddress = [self.class shippingOrBillingAddressFromJSON:details[@"payerInfo"][@"billingAddress"]];
  644. if (!shippingAddress) {
  645. shippingAddress = [self.class accountAddressFromJSON:details[@"payerInfo"][@"accountAddress"]];
  646. }
  647. // Braintree gateway has some inconsistent behavior depending on
  648. // the type of nonce, and sometimes returns "PayPal" for description,
  649. // and sometimes returns a real identifying string. The former is not
  650. // desirable for display. The latter is.
  651. // As a workaround, we ignore descriptions that look like "PayPal".
  652. if ([description caseInsensitiveCompare:@"PayPal"] == NSOrderedSame) {
  653. description = email;
  654. }
  655. BTPayPalCreditFinancing *creditFinancing = [self.class creditFinancingFromJSON:details[@"creditFinancingOffered"]];
  656. BTPayPalAccountNonce *tokenizedPayPalAccount = [[BTPayPalAccountNonce alloc] initWithNonce:nonce
  657. description:description
  658. email:email
  659. firstName:firstName
  660. lastName:lastName
  661. phone:phone
  662. billingAddress:billingAddress
  663. shippingAddress:shippingAddress
  664. clientMetadataId:clientMetadataId
  665. payerId:payerId
  666. isDefault:isDefault
  667. creditFinancing:creditFinancing];
  668. return tokenizedPayPalAccount;
  669. }
  670. + (NSString *)intentTypeToString:(BTPayPalRequestIntent)intentType {
  671. NSString *result = nil;
  672. switch(intentType) {
  673. case BTPayPalRequestIntentAuthorize:
  674. result = @"authorize";
  675. break;
  676. case BTPayPalRequestIntentSale:
  677. result = @"sale";
  678. break;
  679. case BTPayPalRequestIntentOrder:
  680. result = @"order";
  681. break;
  682. default:
  683. result = @"authorize";
  684. break;
  685. }
  686. return result;
  687. }
  688. + (NSString *)landingPageTypeToString:(BTPayPalRequestLandingPageType)landingPageType {
  689. switch(landingPageType) {
  690. case BTPayPalRequestLandingPageTypeLogin:
  691. return @"login";
  692. case BTPayPalRequestLandingPageTypeBilling:
  693. return @"billing";
  694. default:
  695. return nil;
  696. }
  697. }
  698. #pragma mark - Delegate Informers
  699. - (void)informDelegateWillPerformAppSwitch {
  700. NSNotification *notification = [[NSNotification alloc] initWithName:BTAppSwitchWillSwitchNotification
  701. object:self
  702. userInfo:nil];
  703. [[NSNotificationCenter defaultCenter] postNotification:notification];
  704. if ([self.appSwitchDelegate respondsToSelector:@selector(appSwitcherWillPerformAppSwitch:)]) {
  705. [self.appSwitchDelegate appSwitcherWillPerformAppSwitch:self];
  706. }
  707. }
  708. - (void)informDelegateDidPerformAppSwitchToTarget:(PPOTRequestTarget)target {
  709. BTAppSwitchTarget appSwitchTarget;
  710. switch (target) {
  711. case PPOTRequestTargetBrowser:
  712. appSwitchTarget = BTAppSwitchTargetWebBrowser;
  713. break;
  714. case PPOTRequestTargetOnDeviceApplication:
  715. appSwitchTarget = BTAppSwitchTargetNativeApp;
  716. break;
  717. case PPOTRequestTargetNone:
  718. case PPOTRequestTargetUnknown:
  719. appSwitchTarget = BTAppSwitchTargetUnknown;
  720. // Should never happen
  721. break;
  722. }
  723. NSNotification *notification = [[NSNotification alloc] initWithName:BTAppSwitchDidSwitchNotification
  724. object:self
  725. userInfo:@{ BTAppSwitchNotificationTargetKey : @(appSwitchTarget) } ];
  726. [[NSNotificationCenter defaultCenter] postNotification:notification];
  727. if ([self.appSwitchDelegate respondsToSelector:@selector(appSwitcher:didPerformSwitchToTarget:)]) {
  728. [self.appSwitchDelegate appSwitcher:self didPerformSwitchToTarget:appSwitchTarget];
  729. }
  730. }
  731. - (void)informDelegateWillProcessAppSwitchReturn {
  732. NSNotification *notification = [[NSNotification alloc] initWithName:BTAppSwitchWillProcessPaymentInfoNotification
  733. object:self
  734. userInfo:nil];
  735. [[NSNotificationCenter defaultCenter] postNotification:notification];
  736. if ([self.appSwitchDelegate respondsToSelector:@selector(appSwitcherWillProcessPaymentInfo:)]) {
  737. [self.appSwitchDelegate appSwitcherWillProcessPaymentInfo:self];
  738. }
  739. }
  740. - (void)informDelegateAppContextWillSwitch {
  741. NSNotification *notification = [[NSNotification alloc] initWithName:BTAppContextWillSwitchNotification
  742. object:self
  743. userInfo:nil];
  744. [[NSNotificationCenter defaultCenter] postNotification:notification];
  745. if ([self.appSwitchDelegate respondsToSelector:@selector(appContextWillSwitch:)]) {
  746. [self.appSwitchDelegate appContextWillSwitch:self];
  747. }
  748. }
  749. - (void)informDelegateAppContextDidReturn {
  750. NSNotification *notification = [[NSNotification alloc] initWithName:BTAppContextDidReturnNotification
  751. object:self
  752. userInfo:nil];
  753. [[NSNotificationCenter defaultCenter] postNotification:notification];
  754. if ([self.appSwitchDelegate respondsToSelector:@selector(appContextDidReturn:)]) {
  755. [self.appSwitchDelegate appContextDidReturn:self];
  756. }
  757. }
  758. - (void)informDelegatePresentingViewControllerRequestPresent:(NSURL*)appSwitchURL {
  759. if (self.viewControllerPresentingDelegate != nil && [self.viewControllerPresentingDelegate respondsToSelector:@selector(paymentDriver:requestsPresentationOfViewController:)]) {
  760. if (@available(iOS 9.0, *)) {
  761. self.safariViewController = [[SFSafariViewController alloc] initWithURL:appSwitchURL];
  762. self.safariViewController.delegate = self;
  763. self.safariViewController.transitioningDelegate = self;
  764. [self.viewControllerPresentingDelegate paymentDriver:self requestsPresentationOfViewController:self.safariViewController];
  765. }
  766. } else {
  767. [[BTLogger sharedLogger] critical:@"Unable to display View Controller to continue PayPal flow. BTPayPalDriver needs a viewControllerPresentingDelegate<BTViewControllerPresentingDelegate> to be set."];
  768. }
  769. }
  770. - (void)informDelegatePresentingViewControllerNeedsDismissal {
  771. if (self.viewControllerPresentingDelegate != nil && [self.viewControllerPresentingDelegate respondsToSelector:@selector(paymentDriver:requestsDismissalOfViewController:)]) {
  772. if (@available(iOS 9.0, *)) {
  773. [self.viewControllerPresentingDelegate paymentDriver:self requestsDismissalOfViewController:self.safariViewController];
  774. self.safariViewController = nil;
  775. }
  776. } else {
  777. [[BTLogger sharedLogger] critical:@"Unable to dismiss View Controller to end PayPal flow. BTPayPalDriver needs a viewControllerPresentingDelegate<BTViewControllerPresentingDelegate> to be set."];
  778. }
  779. }
  780. #pragma mark - SFSafariViewControllerDelegate
  781. static NSString * const SFSafariViewControllerFinishedURL = @"sfsafariviewcontroller://finished";
  782. - (void)safariViewControllerDidFinish:(__unused SFSafariViewController *)controller API_AVAILABLE(ios(9.0)) {
  783. [self.class handleAppSwitchReturnURL:[NSURL URLWithString:SFSafariViewControllerFinishedURL]];
  784. }
  785. #pragma mark - Preflight check
  786. - (BOOL)verifyAppSwitchWithRemoteConfiguration:(BTJSON *)configuration error:(NSError * __autoreleasing *)error {
  787. if (![configuration[@"paypalEnabled"] isTrue]) {
  788. [self.apiClient sendAnalyticsEvent:@"ios.paypal-otc.preflight.disabled"];
  789. if (error != NULL) {
  790. *error = [NSError errorWithDomain:BTPayPalDriverErrorDomain
  791. code:BTPayPalDriverErrorTypeDisabled
  792. userInfo:@{ NSLocalizedDescriptionKey: @"PayPal is not enabled for this merchant",
  793. NSLocalizedRecoverySuggestionErrorKey: @"Enable PayPal for this merchant in the Braintree Control Panel" }];
  794. }
  795. return NO;
  796. }
  797. if (self.returnURLScheme == nil || [self.returnURLScheme isEqualToString:@""]) {
  798. NSString *recoverySuggestion = @"PayPal requires a return URL scheme to be configured via [BTAppSwitch setReturnURLScheme:]. This custom URL scheme must also be registered with your app.";
  799. [[BTLogger sharedLogger] critical:recoverySuggestion];
  800. [self.apiClient sendAnalyticsEvent:@"ios.paypal-otc.preflight.nil-return-url-scheme"];
  801. if (error != NULL) {
  802. *error = [NSError errorWithDomain:BTPayPalDriverErrorDomain
  803. code:BTPayPalDriverErrorTypeIntegrationReturnURLScheme
  804. userInfo:@{ NSLocalizedDescriptionKey: @"PayPal app switch is missing a returnURLScheme",
  805. NSLocalizedRecoverySuggestionErrorKey: recoverySuggestion }];
  806. }
  807. return NO;
  808. }
  809. if (![[self.class payPalClass] doesApplicationSupportOneTouchCallbackURLScheme:self.returnURLScheme]) {
  810. NSString *recoverySuggestion = [NSString stringWithFormat:@"PayPal requires [BTAppSwitch setReturnURLScheme:] to be configured to begin with your app's bundle ID (%@). Currently, it is set to (%@).", [NSBundle mainBundle].bundleIdentifier, self.returnURLScheme];
  811. [[BTLogger sharedLogger] critical:recoverySuggestion];
  812. [self.apiClient sendAnalyticsEvent:@"ios.paypal-otc.preflight.invalid-return-url-scheme"];
  813. if (error != NULL) {
  814. *error = [NSError errorWithDomain:BTPayPalDriverErrorDomain
  815. code:BTPayPalDriverErrorTypeIntegrationReturnURLScheme
  816. userInfo:@{NSLocalizedFailureReasonErrorKey: @"Application does not support One Touch callback URL scheme",
  817. NSLocalizedRecoverySuggestionErrorKey: recoverySuggestion }];
  818. }
  819. return NO;
  820. }
  821. return YES;
  822. }
  823. #pragma mark - Analytics Helpers
  824. + (NSString *)eventStringForPaymentType:(BTPayPalPaymentType)paymentType {
  825. switch (paymentType) {
  826. case BTPayPalPaymentTypeBillingAgreement:
  827. return @"paypal-ba";
  828. case BTPayPalPaymentTypeFuturePayments:
  829. return @"paypal-future-payments";
  830. case BTPayPalPaymentTypeCheckout:
  831. return @"paypal-single-payment";
  832. case BTPayPalPaymentTypeUnknown:
  833. return nil;
  834. }
  835. }
  836. + (NSString *)eventStringForRequestTarget:(PPOTRequestTarget)requestTarget {
  837. switch (requestTarget) {
  838. case PPOTRequestTargetNone:
  839. return @"none";
  840. case PPOTRequestTargetUnknown:
  841. return @"unknown";
  842. case PPOTRequestTargetOnDeviceApplication:
  843. return @"appswitch";
  844. case PPOTRequestTargetBrowser:
  845. return @"webswitch";
  846. }
  847. }
  848. - (void)sendAnalyticsEventForInitiatingOneTouchForPaymentType:(BTPayPalPaymentType)paymentType
  849. withSuccess:(BOOL)success
  850. target:(PPOTRequestTarget)target {
  851. if (paymentType == BTPayPalPaymentTypeUnknown) return;
  852. NSString *eventName = [NSString stringWithFormat:@"ios.%@.%@.initiate.%@", [self.class eventStringForPaymentType:paymentType], [self.class eventStringForRequestTarget:target], success ? @"started" : @"failed"];
  853. [self.apiClient sendAnalyticsEvent:eventName];
  854. if ((paymentType == BTPayPalPaymentTypeCheckout || paymentType == BTPayPalPaymentTypeBillingAgreement) && self.payPalRequest.offerCredit) {
  855. NSString *eventName = [NSString stringWithFormat:@"ios.%@.%@.credit.offered.%@", [self.class eventStringForPaymentType:paymentType], [self.class eventStringForRequestTarget:target], success ? @"started" : @"failed"];
  856. [self.apiClient sendAnalyticsEvent:eventName];
  857. }
  858. }
  859. - (void)sendAnalyticsEventForHandlingOneTouchResult:(PPOTResult *)result forPaymentType:(BTPayPalPaymentType)paymentType {
  860. if (paymentType == BTPayPalPaymentTypeUnknown) return;
  861. NSString *eventName = [NSString stringWithFormat:@"ios.%@.%@", [self.class eventStringForPaymentType:paymentType], [self.class eventStringForRequestTarget:result.target]];
  862. switch (result.type) {
  863. case PPOTResultTypeError:
  864. if (result.error.code == PPOTErrorCodePersistedDataFetchFailed) {
  865. return [self.apiClient sendAnalyticsEvent:[NSString stringWithFormat:@"%@.failed-keychain", eventName]];
  866. }
  867. return [self.apiClient sendAnalyticsEvent:[NSString stringWithFormat:@"%@.failed", eventName]];
  868. case PPOTResultTypeCancel:
  869. if (result.error) {
  870. return [self.apiClient sendAnalyticsEvent:[NSString stringWithFormat:@"%@.canceled-with-error", eventName]];
  871. } else {
  872. return [self.apiClient sendAnalyticsEvent:[NSString stringWithFormat:@"%@.canceled", eventName]];
  873. }
  874. case PPOTResultTypeSuccess:
  875. return [self.apiClient sendAnalyticsEvent:[NSString stringWithFormat:@"%@.succeeded", eventName]];
  876. }
  877. }
  878. - (void)sendAnalyticsEventIfCreditFinancingInNonce:(BTPayPalAccountNonce *)payPalAccountNonce forPaymentType:(BTPayPalPaymentType)paymentType {
  879. if ([payPalAccountNonce creditFinancing]) {
  880. NSString *eventName = [NSString stringWithFormat:@"ios.%@.credit.accepted", [self.class eventStringForPaymentType:paymentType]];
  881. [self.apiClient sendAnalyticsEvent:eventName];
  882. }
  883. }
  884. - (void)sendAnalyticsEventForTokenizationSuccessForPaymentType:(BTPayPalPaymentType)paymentType {
  885. if (paymentType == BTPayPalPaymentTypeUnknown) return;
  886. NSString *eventName = [NSString stringWithFormat:@"ios.%@.tokenize.succeeded", [self.class eventStringForPaymentType:paymentType]];
  887. [self.apiClient sendAnalyticsEvent:eventName];
  888. }
  889. - (void)sendAnalyticsEventForTokenizationFailureForPaymentType:(BTPayPalPaymentType)paymentType {
  890. if (paymentType == BTPayPalPaymentTypeUnknown) return;
  891. NSString *eventName = [NSString stringWithFormat:@"ios.%@.tokenize.failed", [self.class eventStringForPaymentType:paymentType]];
  892. [self.apiClient sendAnalyticsEvent:eventName];
  893. }
  894. #pragma mark - App Switch handling
  895. - (BOOL)isiOSAppAvailableForAppSwitch {
  896. return [[self.class payPalClass] isWalletAppInstalled];
  897. }
  898. + (BOOL)canHandleAppSwitchReturnURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication {
  899. return appSwitchReturnBlock != nil && [PPOTCore canParseURL:url sourceApplication:sourceApplication];
  900. }
  901. + (void)handleAppSwitchReturnURL:(NSURL *)url {
  902. if (appSwitchReturnBlock) {
  903. appSwitchReturnBlock(url);
  904. }
  905. }
  906. - (NSString *)returnURLScheme {
  907. if (!_returnURLScheme) {
  908. _returnURLScheme = [[BTAppSwitch sharedInstance] returnURLScheme];
  909. }
  910. return _returnURLScheme;
  911. }
  912. #pragma mark - BTPayPalApprovalHandler delegate methods
  913. - (void)onApprovalComplete:(NSURL *)url {
  914. [self.class handleAppSwitchReturnURL:url];
  915. }
  916. - (void)onApprovalCancel {
  917. [self.class handleAppSwitchReturnURL:[NSURL URLWithString:SFSafariViewControllerFinishedURL]];
  918. }
  919. #pragma mark - Internal
  920. - (NSURL *)decorateApprovalURL:(NSURL*)approvalURL forRequest:(BTPayPalRequest *)paypalRequest {
  921. if (approvalURL != nil && paypalRequest.userAction != BTPayPalRequestUserActionDefault) {
  922. NSURLComponents* approvalURLComponents = [[NSURLComponents alloc] initWithURL:approvalURL resolvingAgainstBaseURL:NO];
  923. if (approvalURLComponents != nil) {
  924. NSString *userActionValue = [BTPayPalDriver userActionTypeToString:paypalRequest.userAction];
  925. if ([userActionValue length] > 0) {
  926. NSString *query = [approvalURLComponents query];
  927. NSString *delimiter = [query length] == 0 ? @"" : @"&";
  928. query = [NSString stringWithFormat:@"%@%@useraction=%@", query, delimiter, userActionValue];
  929. approvalURLComponents.query = query;
  930. }
  931. return [approvalURLComponents URL];
  932. }
  933. }
  934. return approvalURL;
  935. }
  936. + (NSString *)userActionTypeToString:(BTPayPalRequestUserAction)userActionType {
  937. NSString *result = nil;
  938. switch(userActionType) {
  939. case BTPayPalRequestUserActionCommit:
  940. result = @"commit";
  941. break;
  942. default:
  943. result = @"";
  944. break;
  945. }
  946. return result;
  947. }
  948. - (BTPayPalRequestFactory *)requestFactory {
  949. if (!_requestFactory) {
  950. _requestFactory = [[BTPayPalRequestFactory alloc] init];
  951. }
  952. return _requestFactory;
  953. }
  954. static Class PayPalClass;
  955. + (void)setPayPalClass:(Class)payPalClass {
  956. if ([payPalClass isSubclassOfClass:[PPOTCore class]]) {
  957. PayPalClass = payPalClass;
  958. }
  959. }
  960. + (Class)payPalClass {
  961. return PayPalClass;
  962. }
  963. @end