PPOTAppSwitchUtil.m 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. //
  2. // PPOTAppSwitchUtil.m
  3. // PayPalOneTouch
  4. //
  5. // Copyright © 2014 PayPal, Inc. All rights reserved.
  6. //
  7. #import <UIKit/UIKit.h>
  8. #import "PPOTAppSwitchUtil.h"
  9. #import "PPOTConfiguration.h"
  10. #import "PPOTAnalyticsDefines.h"
  11. #import "PPOTAnalyticsTracker.h"
  12. #import "PPOTPersistentRequestData.h"
  13. #if __has_include("PayPalUtils.h")
  14. #import "PPOTMacros.h"
  15. #import "PPOTVersion.h"
  16. #import "PPOTString.h"
  17. #import "PPOTJSONHelper.h"
  18. #else
  19. #import <PayPalUtils/PPOTMacros.h>
  20. #import <PayPalUtils/PPOTVersion.h>
  21. #import <PayPalUtils/PPOTString.h>
  22. #import <PayPalUtils/PPOTJSONHelper.h>
  23. #endif
  24. #define STR_TO_URL_SCHEME(str) [NSURL URLWithString:[NSString stringWithFormat:@"%@://", str]]
  25. @implementation PPOTAppSwitchUtil
  26. + (NSString *)bundleId {
  27. return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"];
  28. }
  29. + (NSString *)bundleName {
  30. return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
  31. }
  32. + (BOOL)isCallbackURLSchemeValid:(NSString *)callbackURLScheme {
  33. NSString *bundleID = [[self bundleId] lowercaseString];
  34. // There are issues returning to the app if the return URL begins with a `-`
  35. // Allow callback URLs that remove the leading `-`
  36. // Ex: An app with Bundle ID `-com.example.myapp` can use the callback URL `com.example.myapp.payments`
  37. if (bundleID.length <= 1) {
  38. return NO;
  39. } else if ([[bundleID substringToIndex:1] isEqualToString:@"-"] && ![[callbackURLScheme lowercaseString] hasPrefix:bundleID]) {
  40. bundleID = [bundleID substringFromIndex:1];
  41. }
  42. if (bundleID && ![[callbackURLScheme lowercaseString] hasPrefix:bundleID]) {
  43. PPSDKLog(@"callback URL scheme must start with %@ ", bundleID);
  44. return NO;
  45. }
  46. // check the actual plist that the app is fully configured rather than just making canOpenURL call
  47. NSArray *urlTypes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleURLTypes"];
  48. for (NSDictionary *item in urlTypes) {
  49. NSArray *bundleURLSchemes = item[@"CFBundleURLSchemes"];
  50. if (NSNotFound != [bundleURLSchemes indexOfObject:callbackURLScheme]) {
  51. return YES;
  52. }
  53. }
  54. PPSDKLog(@"callback URL scheme %@ is not found in .plist", callbackURLScheme);
  55. return NO;
  56. }
  57. + (NSString *)protocolFromTargetAppURLScheme:(NSString *)targetAppURLScheme {
  58. NSArray *components = [targetAppURLScheme componentsSeparatedByString:@"."];
  59. return components[[components count] - 1];
  60. }
  61. + (NSDictionary *)parseQueryString:(NSString *)query {
  62. NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:6];
  63. NSArray *pairs = [query componentsSeparatedByString:@"&"];
  64. for (NSString *pair in pairs) {
  65. NSArray *elements = [pair componentsSeparatedByString:@"="];
  66. if (elements.count > 1) {
  67. NSString *key = [[elements objectAtIndex:0] stringByRemovingPercentEncoding];
  68. NSString *val = [[elements objectAtIndex:1] stringByRemovingPercentEncoding];
  69. if (key.length && val.length) {
  70. dict[key] = val;
  71. }
  72. }
  73. }
  74. return dict;
  75. }
  76. + (NSURL *)URLAction:(NSString *)action
  77. targetAppURLScheme:(NSString *)targetAppURLScheme
  78. callbackURLScheme:(NSString *)callbackURLScheme
  79. payload:(NSDictionary *)payload {
  80. NSString *successURL;
  81. NSString *cancelURL;
  82. [self redirectURLsForCallbackURLScheme:callbackURLScheme withReturnURL:&successURL withCancelURL:&cancelURL];
  83. NSString *encodedPayload = [PPOTString stringByURLEncodingAllCharactersInString:[PPOTJSONHelper base64EncodedJSONStringWithDictionary:payload]];
  84. NSString* urlString = [NSString stringWithFormat:@"%@://%@?%@=%@&%@=%@&%@=%@&%@=%@",
  85. targetAppURLScheme, action, kPPOTAppSwitchPayloadKey, encodedPayload,
  86. kPPOTAppSwitchXSourceKey, [self bundleId],
  87. kPPOTAppSwitchXSuccessKey, successURL,
  88. kPPOTAppSwitchXCancelKey, cancelURL];
  89. return [NSURL URLWithString:urlString];
  90. }
  91. + (NSURL *)URLAction:(NSString *)action
  92. callbackURLScheme:(NSString *)callbackURLScheme
  93. payload:(NSDictionary *)payload {
  94. NSString *successURL;
  95. NSString *cancelURL;
  96. [self redirectURLsForCallbackURLScheme:callbackURLScheme withReturnURL:&successURL withCancelURL:&cancelURL];
  97. NSString *encodedPayload = [PPOTString stringByURLEncodingAllCharactersInString:[PPOTJSONHelper base64EncodedJSONStringWithDictionary:payload]];
  98. NSString* urlString = [NSString stringWithFormat:@"%@?%@=%@&%@=%@&%@=%@&%@=%@",
  99. action, kPPOTAppSwitchPayloadKey, encodedPayload,
  100. kPPOTAppSwitchXSourceKey, [self bundleId],
  101. kPPOTAppSwitchXSuccessKey, successURL,
  102. kPPOTAppSwitchXCancelKey, cancelURL];
  103. return [NSURL URLWithString:urlString];
  104. }
  105. #pragma mark - build or parse our redirect urls
  106. + (void)redirectURLsForCallbackURLScheme:(NSString *)callbackURLScheme withReturnURL:(NSString * __autoreleasing *)returnURL withCancelURL:(NSString * __autoreleasing *)cancelURL {
  107. PPAssert(returnURL, @"redirectURLsForCallbackURLScheme: returnURL is required");
  108. PPAssert(cancelURL, @"redirectURLsForCallbackURLScheme: cancelURL is required");
  109. *returnURL = nil;
  110. *cancelURL = nil;
  111. if ([PPOTAppSwitchUtil isCallbackURLSchemeValid:callbackURLScheme]) {
  112. NSString *hostAndPath = [self redirectURLHostAndPath];
  113. *returnURL = [NSString stringWithFormat:@"%@://%@%@", callbackURLScheme, hostAndPath, kPPOTAppSwitchSuccessAction];
  114. *cancelURL = [NSString stringWithFormat:@"%@://%@%@", callbackURLScheme, hostAndPath, kPPOTAppSwitchCancelAction];
  115. }
  116. }
  117. + (BOOL)isValidURLAction:(NSURL *)urlAction {
  118. NSString *scheme = urlAction.scheme;
  119. if (!scheme.length) {
  120. return NO;
  121. }
  122. NSString *hostAndPath = [urlAction.host stringByAppendingString:urlAction.path];
  123. NSMutableArray *pathComponents = [[hostAndPath componentsSeparatedByString:@"/"] mutableCopy];
  124. [pathComponents removeLastObject]; // remove the action (`success`, `cancel`, etc)
  125. hostAndPath = [pathComponents componentsJoinedByString:@"/"];
  126. if ([hostAndPath length]) {
  127. hostAndPath = [hostAndPath stringByAppendingString:@"/"];
  128. }
  129. if (![hostAndPath isEqualToString:[self redirectURLHostAndPath]]) {
  130. return NO;
  131. }
  132. NSString *action = [self actionFromURLAction:urlAction];
  133. if (!action.length) {
  134. return NO;
  135. }
  136. NSArray *validActions = @[kPPOTAppSwitchSuccessAction, kPPOTAppSwitchCancelAction, kPPOTAppSwitchAuthenticateAction];
  137. if (![validActions containsObject:action]) {
  138. return NO;
  139. }
  140. NSString *query = [urlAction query];
  141. if (!query.length) {
  142. // should always have at least a payload or else a Hermes token
  143. return NO;
  144. }
  145. return YES;
  146. }
  147. + (NSString *)actionFromURLAction:(NSURL *)urlAction {
  148. NSString *action = [urlAction.lastPathComponent componentsSeparatedByString:@"?"][0];
  149. if (![action length]) {
  150. action = urlAction.host;
  151. }
  152. return action;
  153. }
  154. + (NSString *)redirectURLHostAndPath {
  155. // Return either an empty string;
  156. // or else a non-empty `host` or `host/path`, ending with `/`
  157. return @"onetouch/v1/";
  158. }
  159. @end