PPOTRequest.m 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. //
  2. // PPOTRequest.m
  3. // PayPalOneTouch
  4. //
  5. // Copyright © 2015 PayPal, Inc. All rights reserved.
  6. //
  7. #import "PPOTRequest_Internal.h"
  8. #import "PPOTAnalyticsDefines.h"
  9. #import "PPOTAppSwitchUtil.h"
  10. #import "PPOTConfiguration.h"
  11. #import "PPOTOAuth2SwitchRequest.h"
  12. #import "PPOTAnalyticsTracker.h"
  13. #import "PPOTPersistentRequestData.h"
  14. #import "PPOTError.h"
  15. #if __has_include("PayPalUtils.h")
  16. #import "PPOTDevice.h"
  17. #import "PPOTMacros.h"
  18. #else
  19. #import <PayPalUtils/PPOTDevice.h>
  20. #import <PayPalUtils/PPOTMacros.h>
  21. #endif
  22. #import <UIKit/UIKit.h>
  23. NSString *const PayPalEnvironmentProduction = PPRequestEnvironmentProduction;
  24. NSString *const PayPalEnvironmentSandbox = PPRequestEnvironmentSandbox;
  25. NSString *const PayPalEnvironmentMock = PPRequestEnvironmentNoNetwork;
  26. #define kPPOTAppSwitchSchemeToCheck CARDIO_STR(@"http")
  27. @implementation PPOTRequest
  28. #pragma mark - initialization
  29. + (void)initialize {
  30. if (self == [PPOTRequest class]) {
  31. [PPOTConfiguration updateCacheAsNecessary]; // called by all public methods
  32. }
  33. }
  34. - (instancetype)initWithClientID:(NSString *)clientID
  35. environment:(NSString *)environment
  36. callbackURLScheme:(NSString *)callbackURLScheme {
  37. if (!clientID.length) {
  38. PPSDKLog(@"clientID is required.");
  39. return nil;
  40. }
  41. if (!environment.length) {
  42. PPSDKLog(@"environment is required.");
  43. return nil;
  44. }
  45. if (![PPOTAppSwitchUtil isCallbackURLSchemeValid:callbackURLScheme]) {
  46. PPSDKLog(@"callbackURLScheme is not configured or nil.");
  47. return nil;
  48. }
  49. self = [super init];
  50. if (self) {
  51. _clientID = clientID;
  52. _environment = environment;
  53. _callbackURLScheme = callbackURLScheme;
  54. [PPOTConfiguration updateCacheAsNecessary]; // called by all public methods
  55. }
  56. return self;
  57. }
  58. #pragma mark - public methods
  59. - (void)getTargetApp:(PPOTRequestPreflightCompletionBlock)completionBlock {
  60. PPAssert(completionBlock, @"getTargetApp: completionBlock is required");
  61. [PPOTConfiguration updateCacheAsNecessary]; // called by all public methods
  62. [self determineConfigurationRecipe:^{
  63. NSString *analyticsPage = nil;
  64. switch (self.configurationRecipe.target) {
  65. case PPOTRequestTargetBrowser:
  66. analyticsPage = kAnalyticsAppSwitchPreflightBrowser;
  67. break;
  68. case PPOTRequestTargetOnDeviceApplication:
  69. analyticsPage = kAnalyticsAppSwitchPreflightWallet;
  70. break;
  71. case PPOTRequestTargetNone:
  72. default:
  73. analyticsPage = kAnalyticsAppSwitchPreflightNone;
  74. break;
  75. }
  76. NSString *protocol = [NSString stringWithFormat:@"v%ld", self.configurationRecipe.protocolVersion.longValue];
  77. analyticsPage = [analyticsPage stringByAppendingString:protocol];
  78. [[PPOTAnalyticsTracker sharedManager] trackPage:analyticsPage
  79. environment:self.environment
  80. clientID:self.clientID
  81. error:nil
  82. hermesToken:nil];
  83. completionBlock(self.configurationRecipe.target);
  84. }];
  85. }
  86. - (void)performWithAdapterBlock:(PPOTRequestAdapterBlock)adapterBlock {
  87. PPAssert(adapterBlock, @"performWithAdapterBlock: adapterBlock is required");
  88. [PPOTConfiguration updateCacheAsNecessary]; // called by all public methods
  89. [self determineConfigurationRecipe:^{
  90. BOOL success = YES;
  91. PPOTRequestTarget target = PPOTRequestTargetNone;
  92. NSError *error = nil;
  93. NSString *requestClientMetadataId = nil;
  94. NSURL *appSwitchURL = nil;
  95. if (self.configurationRecipe) {
  96. PPOTSwitchRequest *appSwitchRequest = [self getAppSwitchRequestForConfigurationRecipe:self.configurationRecipe];
  97. if (appSwitchRequest) {
  98. appSwitchURL = [appSwitchRequest encodedURL];
  99. requestClientMetadataId = appSwitchRequest.clientMetadataID;
  100. NSString *analyticsPage = nil;
  101. if ([[appSwitchURL.absoluteString lowercaseString] hasPrefix:kPPOTAppSwitchSchemeToCheck]) {
  102. target = PPOTRequestTargetBrowser;
  103. analyticsPage = kAnalyticsAppSwitchToBrowser;
  104. }
  105. else {
  106. target = PPOTRequestTargetOnDeviceApplication;
  107. analyticsPage = kAnalyticsAppSwitchToWallet;
  108. }
  109. NSString *protocol = [NSString stringWithFormat:@"v%ld", self.configurationRecipe.protocolVersion.longValue];
  110. analyticsPage = [analyticsPage stringByAppendingString:protocol];
  111. [PPOTPersistentRequestData storeWithConfigurationRecipe:self.configurationRecipe withRequest:appSwitchRequest];
  112. NSString *hermesToken = nil;
  113. if ([self respondsToSelector:@selector(approvalURL)]) {
  114. NSDictionary *queryDictionary = [PPOTAppSwitchUtil parseQueryString:[[self performSelector:@selector(approvalURL)] query]];
  115. hermesToken = queryDictionary[kPPOTAppSwitchHermesTokenKey];
  116. }
  117. [[PPOTAnalyticsTracker sharedManager] trackPage:analyticsPage
  118. environment:self.environment
  119. clientID:self.clientID
  120. error:error
  121. hermesToken:hermesToken];
  122. } else {
  123. success = NO;
  124. error = [PPOTError errorWithErrorCode:PPOTErrorCodeNoTargetAppFound];
  125. }
  126. } else {
  127. PPSDKLog(@"No appropriate configuration recipe found");
  128. success = NO;
  129. error = [PPOTError errorWithErrorCode:PPOTErrorCodeNoTargetAppFound];
  130. }
  131. adapterBlock(success, appSwitchURL, target, requestClientMetadataId, error);
  132. }];
  133. }
  134. #pragma mark - add subclass-specific info to appSwitchRequest
  135. - (PPOTSwitchRequest *)getAppSwitchRequestForConfigurationRecipe:(__attribute__((unused)) PPOTConfigurationRecipe *)configurationRecipe {
  136. PPAssert(NO, @"getAppSwitchRequestForConfigurationRecipe: subclass of PPOTRequest must override");
  137. return nil;
  138. }
  139. #pragma mark - configuration methods
  140. - (void)determineConfigurationRecipe:(void (^)(void))completionBlock {
  141. PPAssert(completionBlock, @"establishConfigurationRecipe: completionBlock is required");
  142. if (self.configurationRecipe) {
  143. completionBlock();
  144. return;
  145. }
  146. #ifdef DEBUG
  147. [PPOTConfiguration useHardcodedConfiguration:self.useHardcodedConfiguration];
  148. #endif
  149. [self getAppropriateConfigurationRecipe:^(PPOTConfigurationRecipe *configurationRecipe) {
  150. self.configurationRecipe = configurationRecipe;
  151. completionBlock();
  152. }];
  153. }
  154. - (void)getAppropriateConfigurationRecipe:(__attribute__((unused)) void (^)(PPOTConfigurationRecipe *configurationRecipe))completionBlock {
  155. PPAssert(NO, @"subclass must override");
  156. }
  157. - (BOOL)isConfigurationRecipeTargetSupported:(PPOTConfigurationRecipe *)configurationRecipe {
  158. // Confirm that the recipe's target is available (Browser is always installed; Wallet may or may not be installed),
  159. // and also that the recipe's target is not rejected by `self.forcedTarget`.
  160. switch (configurationRecipe.target) {
  161. case PPOTRequestTargetOnDeviceApplication: {
  162. return NO;
  163. }
  164. case PPOTRequestTargetBrowser: {
  165. if (self.forcedTarget.integerValue == PPOTRequestTargetOnDeviceApplication) {
  166. return NO;
  167. }
  168. return YES;
  169. }
  170. default: {
  171. return NO;
  172. }
  173. }
  174. }
  175. - (BOOL)isConfigurationRecipeLocaleSupported:(PPOTConfigurationRecipe *)configurationRecipe {
  176. if (![configurationRecipe.supportedLocales count]) {
  177. return YES;
  178. }
  179. return [configurationRecipe.supportedLocales containsObject:[[PPOTDevice complicatedDeviceLocale] uppercaseString]];
  180. }
  181. #pragma mark - utility method
  182. #define kPPOTAppSwitchHermesTokenKey @"token"
  183. #define kPPOTAppSwitchBillingAgreementTokenKey @"ba_token"
  184. + (NSString *)tokenFromApprovalURL:(NSURL *)approvalURL {
  185. NSDictionary *queryDictionary = [PPOTRequest parseQueryString:[approvalURL query]];
  186. return queryDictionary[kPPOTAppSwitchHermesTokenKey] ?: queryDictionary[kPPOTAppSwitchBillingAgreementTokenKey];
  187. }
  188. + (NSDictionary *)parseQueryString:(NSString *)query {
  189. NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:6];
  190. NSArray *pairs = [query componentsSeparatedByString:@"&"];
  191. for (NSString *pair in pairs) {
  192. NSArray *elements = [pair componentsSeparatedByString:@"="];
  193. if (elements.count > 1) {
  194. NSString *key = [[elements objectAtIndex:0] stringByRemovingPercentEncoding];
  195. NSString *val = [[elements objectAtIndex:1] stringByRemovingPercentEncoding];
  196. if (key.length && val.length) {
  197. dict[key] = val;
  198. }
  199. }
  200. }
  201. return dict;
  202. }
  203. @end