BTVenmoDriver.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. #import "BTVenmoDriver_Internal.h"
  2. #import "BTVenmoAccountNonce_Internal.h"
  3. #import "BTVenmoAppSwitchRequestURL.h"
  4. #import "BTVenmoAppSwitchReturnURL.h"
  5. #import "BTVenmoRequest_Internal.h"
  6. #if __has_include(<Braintree/BraintreeVenmo.h>) // CocoaPods
  7. #import <Braintree/BTConfiguration+Venmo.h>
  8. #import <Braintree/BraintreeCore.h>
  9. #import <Braintree/BTAPIClient_Internal.h>
  10. #import <Braintree/BTPaymentMethodNonceParser.h>
  11. #import <Braintree/BTLogger_Internal.h>
  12. #elif SWIFT_PACKAGE // SPM
  13. #import <BraintreeVenmo/BTConfiguration+Venmo.h>
  14. #import <BraintreeCore/BraintreeCore.h>
  15. #import "../BraintreeCore/BTAPIClient_Internal.h"
  16. #import "../BraintreeCore/BTPaymentMethodNonceParser.h"
  17. #import "../BraintreeCore/BTLogger_Internal.h"
  18. #else // Carthage
  19. #import <BraintreeVenmo/BTConfiguration+Venmo.h>
  20. #import <BraintreeCore/BraintreeCore.h>
  21. #import <BraintreeCore/BTAPIClient_Internal.h>
  22. #import <BraintreeCore/BTPaymentMethodNonceParser.h>
  23. #import <BraintreeCore/BTLogger_Internal.h>
  24. #endif
  25. @interface BTVenmoDriver ()
  26. @property (nonatomic, copy) void (^appSwitchCompletionBlock)(BTVenmoAccountNonce *, NSError *);
  27. @end
  28. NSString * const BTVenmoDriverErrorDomain = @"com.braintreepayments.BTVenmoDriverErrorDomain";
  29. NSString * const BTVenmoAppStoreUrl = @"https://itunes.apple.com/us/app/venmo-send-receive-money/id351727428";
  30. @implementation BTVenmoDriver
  31. static BTVenmoDriver *appSwitchedDriver;
  32. + (void)load {
  33. if (self == [BTVenmoDriver class]) {
  34. [[BTAppContextSwitcher sharedInstance] registerAppContextSwitchDriver:self];
  35. [[BTPaymentMethodNonceParser sharedParser] registerType:@"VenmoAccount" withParsingBlock:^BTPaymentMethodNonce * _Nullable(BTJSON * _Nonnull venmoJSON) {
  36. return [BTVenmoAccountNonce venmoAccountWithJSON:venmoJSON];
  37. }];
  38. }
  39. }
  40. - (instancetype)initWithAPIClient:(BTAPIClient *)apiClient {
  41. if (self = [super init]) {
  42. _apiClient = apiClient;
  43. }
  44. return self;
  45. }
  46. - (instancetype)init {
  47. return nil;
  48. }
  49. #pragma mark - Accessors
  50. - (id)application NS_EXTENSION_UNAVAILABLE("Uses APIs (i.e UIApplication.sharedApplication) not available for use in App Extensions.") {
  51. if (!_application) {
  52. _application = [UIApplication sharedApplication];
  53. }
  54. return _application;
  55. }
  56. - (NSBundle *)bundle {
  57. if (!_bundle) {
  58. _bundle = [NSBundle mainBundle];
  59. }
  60. return _bundle;
  61. }
  62. - (UIDevice *)device {
  63. if (!_device) {
  64. _device = [UIDevice currentDevice];
  65. }
  66. return _device;
  67. }
  68. - (NSString *)returnURLScheme {
  69. if (!_returnURLScheme) {
  70. _returnURLScheme = [BTAppContextSwitcher sharedInstance].returnURLScheme;
  71. }
  72. return _returnURLScheme;
  73. }
  74. #pragma mark - Tokenization
  75. - (void)tokenizeVenmoAccountWithVenmoRequest:(BTVenmoRequest *)venmoRequest completion:(void (^)(BTVenmoAccountNonce * _Nullable venmoAccount, NSError * _Nullable error))completionBlock {
  76. if (!venmoRequest) {
  77. NSError *error = [NSError errorWithDomain:BTVenmoDriverErrorDomain
  78. code:BTVenmoDriverErrorTypeIntegration
  79. userInfo:@{NSLocalizedDescriptionKey: @"BTVenmoDriver failed because BTVenmoRequest is nil."}];
  80. completionBlock(nil, error);
  81. return;
  82. }
  83. if (!self.apiClient) {
  84. NSError *error = [NSError errorWithDomain:BTVenmoDriverErrorDomain
  85. code:BTVenmoDriverErrorTypeIntegration
  86. userInfo:@{NSLocalizedDescriptionKey: @"BTVenmoDriver failed because BTAPIClient is nil."}];
  87. completionBlock(nil, error);
  88. return;
  89. }
  90. if (self.returnURLScheme == nil || [self.returnURLScheme isEqualToString:@""]) {
  91. [[BTLogger sharedLogger] critical:@"Venmo requires a return URL scheme to be configured via [BTAppContextSwitcher setReturnURLScheme:]"];
  92. NSError *error = [NSError errorWithDomain:BTVenmoDriverErrorDomain
  93. code:BTVenmoDriverErrorTypeAppNotAvailable
  94. userInfo:@{NSLocalizedDescriptionKey: @"UIApplication failed to perform app switch to Venmo."}];
  95. completionBlock(nil, error);
  96. return;
  97. } else if (!self.bundle.bundleIdentifier || ![self.returnURLScheme hasPrefix:self.bundle.bundleIdentifier]) {
  98. [[BTLogger sharedLogger] critical:@"Venmo requires [BTAppContextSwitcher setReturnURLScheme:] to be configured to begin with your app's bundle ID (%@). Currently, it is set to (%@) ", [NSBundle mainBundle].bundleIdentifier, self.returnURLScheme];
  99. }
  100. [self.apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *configurationError) {
  101. if (configurationError) {
  102. completionBlock(nil, configurationError);
  103. return;
  104. }
  105. NSError *error;
  106. if (![self verifyAppSwitchWithConfiguration:configuration error:&error]) {
  107. completionBlock(nil, error);
  108. return;
  109. }
  110. NSString *merchantProfileID = venmoRequest.profileID ?: configuration.venmoMerchantID;
  111. NSString *bundleDisplayName = [self.bundle objectForInfoDictionaryKey:@"CFBundleDisplayName"];
  112. BTMutableClientMetadata *metadata = [self.apiClient.metadata mutableCopy];
  113. metadata.source = BTClientMetadataSourceVenmoApp;
  114. if (venmoRequest.paymentMethodUsage != BTVenmoPaymentMethodUsageUnspecified) {
  115. NSMutableDictionary *inputParams = [@{
  116. @"paymentMethodUsage": venmoRequest.paymentMethodUsageAsString,
  117. @"merchantProfileId": merchantProfileID,
  118. @"customerClient": @"MOBILE_APP",
  119. @"intent": @"CONTINUE"
  120. } mutableCopy];
  121. if (venmoRequest.displayName) {
  122. inputParams[@"displayName"] = venmoRequest.displayName;
  123. }
  124. NSDictionary *params = @{
  125. @"query": @"mutation CreateVenmoPaymentContext($input: CreateVenmoPaymentContextInput!) { createVenmoPaymentContext(input: $input) { venmoPaymentContext { id } } }",
  126. @"variables": @{
  127. @"input": inputParams
  128. }
  129. };
  130. [self.apiClient POST:@"" parameters:params httpType:BTAPIClientHTTPTypeGraphQLAPI completion:^(BTJSON *body, __unused NSHTTPURLResponse *response, NSError *err) {
  131. if (err) {
  132. if (err.code == NETWORK_CONNECTION_LOST_CODE) {
  133. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.network-connection.failure"];
  134. }
  135. NSError *error = [NSError errorWithDomain:BTVenmoDriverErrorDomain
  136. code:BTVenmoDriverErrorTypeInvalidRequestURL
  137. userInfo:@{NSLocalizedDescriptionKey: @"Failed to fetch a Venmo paymentContextID while constructing the requestURL."}];
  138. completionBlock(nil, error);
  139. return;
  140. }
  141. NSString *paymentContextID = [body[@"data"][@"createVenmoPaymentContext"][@"venmoPaymentContext"][@"id"] asString];
  142. if (paymentContextID == nil) {
  143. NSError *error = [NSError errorWithDomain:BTVenmoDriverErrorDomain
  144. code:BTVenmoDriverErrorTypeInvalidRequestURL
  145. userInfo:@{NSLocalizedDescriptionKey: @"Failed to parse a Venmo paymentContextID while constructing the requestURL. Please contact support."}];
  146. completionBlock(nil, error);
  147. return;
  148. }
  149. NSURL *appSwitchURL = [BTVenmoAppSwitchRequestURL appSwitchURLForMerchantID:merchantProfileID
  150. accessToken:configuration.venmoAccessToken
  151. returnURLScheme:self.returnURLScheme
  152. bundleDisplayName:bundleDisplayName
  153. environment:configuration.venmoEnvironment
  154. paymentContextID:paymentContextID
  155. metadata:self.apiClient.metadata];
  156. [self performAppSwitch:appSwitchURL shouldVault:venmoRequest.vault completion:completionBlock];
  157. }];
  158. } else {
  159. NSURL *appSwitchURL = [BTVenmoAppSwitchRequestURL appSwitchURLForMerchantID:merchantProfileID
  160. accessToken:configuration.venmoAccessToken
  161. returnURLScheme:self.returnURLScheme
  162. bundleDisplayName:bundleDisplayName
  163. environment:configuration.venmoEnvironment
  164. paymentContextID:nil
  165. metadata:self.apiClient.metadata];
  166. [self performAppSwitch:appSwitchURL shouldVault:venmoRequest.vault completion:completionBlock];
  167. }
  168. }];
  169. }
  170. #pragma mark - Vaulting
  171. - (void)vaultVenmoAccountNonce:(NSString *)nonce {
  172. NSMutableDictionary *params = [NSMutableDictionary new];
  173. params[@"venmoAccount"] = @{
  174. @"nonce": nonce
  175. };
  176. [self.apiClient POST:@"v1/payment_methods/venmo_accounts"
  177. parameters:params
  178. completion:^(BTJSON *body, __unused NSHTTPURLResponse *response, NSError *error) {
  179. if (error) {
  180. if (error.code == NETWORK_CONNECTION_LOST_CODE) {
  181. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.network-connection.failure"];
  182. }
  183. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.vault.failure"];
  184. self.appSwitchCompletionBlock(nil, error);
  185. } else {
  186. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.vault.success"];
  187. BTJSON *venmoAccountJson = body[@"venmoAccounts"][0];
  188. self.appSwitchCompletionBlock([BTVenmoAccountNonce venmoAccountWithJSON:venmoAccountJson], venmoAccountJson.asError);
  189. }
  190. self.appSwitchCompletionBlock = nil;
  191. }];
  192. }
  193. #pragma mark - App switch
  194. - (void)performAppSwitch:(NSURL *)appSwitchURL shouldVault:(BOOL)vault completion:(void (^)(BTVenmoAccountNonce * _Nullable venmoAccount, NSError * _Nullable error))completionBlock {
  195. if (!appSwitchURL) {
  196. NSError *error = [NSError errorWithDomain:BTVenmoDriverErrorDomain
  197. code:BTVenmoDriverErrorTypeInvalidRequestURL
  198. userInfo:@{NSLocalizedDescriptionKey: @"Failed to create Venmo app switch request URL."}];
  199. completionBlock(nil, error);
  200. return;
  201. }
  202. [self.application openURL:appSwitchURL options:[NSDictionary dictionary] completionHandler:^(BOOL success) {
  203. [self invokedOpenURLSuccessfully:success shouldVault:vault completion:completionBlock];
  204. }];
  205. }
  206. - (void)invokedOpenURLSuccessfully:(BOOL)success shouldVault:(BOOL)vault completion:(void (^)(BTVenmoAccountNonce *venmoAccount, NSError *configurationError))completionBlock {
  207. self.shouldVault = success && vault;
  208. if (success) {
  209. self.appSwitchCompletionBlock = completionBlock;
  210. appSwitchedDriver = self;
  211. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.appswitch.initiate.success"];
  212. } else {
  213. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.appswitch.initiate.error.failure"];
  214. NSError *error = [NSError errorWithDomain:BTVenmoDriverErrorDomain
  215. code:BTVenmoDriverErrorTypeAppSwitchFailed
  216. userInfo:@{NSLocalizedDescriptionKey: @"UIApplication failed to perform app switch to Venmo."}];
  217. completionBlock(nil, error);
  218. }
  219. }
  220. - (BOOL)isiOSAppAvailableForAppSwitch {
  221. return [self.application canOpenURL:[BTVenmoAppSwitchRequestURL baseAppSwitchURL]];
  222. }
  223. #pragma mark - App switch return
  224. + (void)handleReturnURL:(NSURL *)url {
  225. [appSwitchedDriver handleOpenURL:url];
  226. appSwitchedDriver = nil;
  227. }
  228. + (BOOL)canHandleReturnURL:(NSURL *)url {
  229. return [BTVenmoAppSwitchReturnURL isValidURL:url];
  230. }
  231. - (void)handleOpenURL:(NSURL *)url {
  232. BTVenmoAppSwitchReturnURL *returnURL = [[BTVenmoAppSwitchReturnURL alloc] initWithURL:url];
  233. switch (returnURL.state) {
  234. case BTVenmoAppSwitchReturnURLStateSucceededWithPaymentContext: {
  235. NSDictionary *params = @{
  236. @"query": @"query PaymentContext($id: ID!) { node(id: $id) { ... on VenmoPaymentContext { paymentMethodId userName payerInfo { firstName lastName phoneNumber email externalId userName } } } }",
  237. @"variables": @{ @"id": returnURL.paymentContextID }
  238. };
  239. [self.apiClient POST:@"" parameters:params httpType:BTAPIClientHTTPTypeGraphQLAPI completion:^(BTJSON *body, __unused NSHTTPURLResponse *response, NSError *error) {
  240. if (error) {
  241. if (error.code == NETWORK_CONNECTION_LOST_CODE) {
  242. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.network-connection.failure"];
  243. }
  244. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.appswitch.handle.client-failure"];
  245. self.appSwitchCompletionBlock(nil, error);
  246. self.appSwitchCompletionBlock = nil;
  247. return;
  248. }
  249. BTVenmoAccountNonce *venmoAccountNonce = [[BTVenmoAccountNonce alloc] initWithPaymentContextJSON:body];
  250. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.appswitch.handle.success"];
  251. if (self.shouldVault && self.apiClient.clientToken != nil) {
  252. [self vaultVenmoAccountNonce:venmoAccountNonce.nonce];
  253. } else {
  254. self.appSwitchCompletionBlock(venmoAccountNonce, nil);
  255. self.appSwitchCompletionBlock = nil;
  256. }
  257. }];
  258. break;
  259. }
  260. case BTVenmoAppSwitchReturnURLStateSucceeded: {
  261. NSError *error = nil;
  262. if (!returnURL.nonce) {
  263. error = [NSError errorWithDomain:BTVenmoDriverErrorDomain code:BTVenmoDriverErrorTypeInvalidReturnURL userInfo:@{NSLocalizedDescriptionKey: @"Return URL is missing nonce"}];
  264. } else if (!returnURL.username) {
  265. error = [NSError errorWithDomain:BTVenmoDriverErrorDomain code:BTVenmoDriverErrorTypeInvalidReturnURL userInfo:@{NSLocalizedDescriptionKey: @"Return URL is missing username"}];
  266. }
  267. if (error) {
  268. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.appswitch.handle.client-failure"];
  269. self.appSwitchCompletionBlock(nil, error);
  270. self.appSwitchCompletionBlock = nil;
  271. return;
  272. }
  273. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.appswitch.handle.success"];
  274. if (self.shouldVault && self.apiClient.clientToken != nil) {
  275. [self vaultVenmoAccountNonce:returnURL.nonce];
  276. } else {
  277. BTJSON *json = [[BTJSON alloc] initWithValue:@{
  278. @"nonce": returnURL.nonce,
  279. @"details": @{@"username": returnURL.username},
  280. @"description": returnURL.username
  281. }];
  282. BTVenmoAccountNonce *card = [BTVenmoAccountNonce venmoAccountWithJSON:json];
  283. self.appSwitchCompletionBlock(card, nil);
  284. self.appSwitchCompletionBlock = nil;
  285. }
  286. break;
  287. }
  288. case BTVenmoAppSwitchReturnURLStateFailed: {
  289. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.appswitch.handle.failed"];
  290. self.appSwitchCompletionBlock(nil, returnURL.error);
  291. self.appSwitchCompletionBlock = nil;
  292. break;
  293. }
  294. case BTVenmoAppSwitchReturnURLStateCanceled: {
  295. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.appswitch.handle.cancel"];
  296. self.appSwitchCompletionBlock(nil, nil);
  297. self.appSwitchCompletionBlock = nil;
  298. break;
  299. }
  300. default:
  301. // should not happen
  302. break;
  303. }
  304. }
  305. #pragma mark - App Store switch
  306. - (void)openVenmoAppPageInAppStore {
  307. NSURL *venmoAppStoreUrl = [NSURL URLWithString:BTVenmoAppStoreUrl];
  308. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.app-store.invoked"];
  309. [self.application openURL:venmoAppStoreUrl
  310. options:[NSDictionary dictionary]
  311. completionHandler:nil];
  312. }
  313. #pragma mark - Helpers
  314. - (BOOL)verifyAppSwitchWithConfiguration:(BTConfiguration *)configuration error:(NSError * __autoreleasing *)error {
  315. if (!configuration.isVenmoEnabled) {
  316. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.appswitch.initiate.error.disabled"];
  317. if (error) {
  318. *error = [NSError errorWithDomain:BTVenmoDriverErrorDomain
  319. code:BTVenmoDriverErrorTypeDisabled
  320. userInfo:@{ NSLocalizedDescriptionKey:@"Venmo is not enabled for this merchant account." }];
  321. }
  322. return NO;
  323. }
  324. if (![self isiOSAppAvailableForAppSwitch]) {
  325. [self.apiClient sendAnalyticsEvent:@"ios.pay-with-venmo.appswitch.initiate.error.unavailable"];
  326. if (error) {
  327. *error = [NSError errorWithDomain:BTVenmoDriverErrorDomain
  328. code:BTVenmoDriverErrorTypeAppNotAvailable
  329. userInfo:@{ NSLocalizedDescriptionKey:@"The Venmo app is not installed on this device, or it is not configured or available for app switch." }];
  330. }
  331. return NO;
  332. }
  333. NSString *bundleDisplayName = [self.bundle objectForInfoDictionaryKey:@"CFBundleDisplayName"];
  334. if (!bundleDisplayName) {
  335. if (error) {
  336. *error = [NSError errorWithDomain:BTVenmoDriverErrorDomain
  337. code:BTVenmoDriverErrorTypeBundleDisplayNameMissing
  338. userInfo:@{NSLocalizedDescriptionKey: @"CFBundleDisplayName must be non-nil. Please set 'Bundle display name' in your Info.plist."}];
  339. }
  340. return NO;
  341. }
  342. return YES;
  343. }
  344. @end