// // PPOTConfiguration.m // PayPalOneTouch // // Copyright © 2015 PayPal, Inc. All rights reserved. // #import "PPOTConfiguration.h" #if __has_include("PayPalUtils.h") #import "PPOTJSONHelper.h" #import "PPOTMacros.h" #import "PPOTSimpleKeychain.h" #import "PPOTURLSession.h" #else #import #import #import #import #endif #if __has_include("BraintreeCore.h") #import "BTLogger_Internal.h" #else #import #endif #import #include "PPDefaultConfigurationJSON.h" // `PPDefaultConfigurationJSON.h` is generated by a build script from `otc-config.ios.json`; // it defines these two variables: // unsigned char configuration_otc_config_ios_json[]; // unsigned int configuration_otc_config_ios_json_len; // `configuration_otc_config_ios_json` holds the contents of the `otc-config.ios.json` file. #define PPEnvironmentProduction @"live" #define kConfigurationFileDownloadURL CARDIO_STR(@"https://www.paypalobjects.com/webstatic/otc/otc-config.ios.json") #define kConfigurationFileDownloadTimeout 60 #define kConfigurationFileDownloadRetryInterval (5 * 60) // 5 minutes #define kPPOTConfigurationFileMaximumAcceptableObsolescence (4 * 60 * 60) // 4 hours #define kPPOTConfigurationSupportedProtocolVersionsForWallet @[@1, @2, @3] #define kPPOTConfigurationSupportedProtocolVersionsForBrowser @[@0, @3] #define kPPOTConfigurationKeyOs CARDIO_STR(@"os") #define kPPOTConfigurationKeyFileTimestamp CARDIO_STR(@"file_timestamp") #define kPPOTConfigurationKeyTarget CARDIO_STR(@"target") #define kPPOTConfigurationKeyProtocolVersion CARDIO_STR(@"protocol") #define kPPOTConfigurationKeySupportedLocales CARDIO_STR(@"supported_locales") #define kPPOTConfigurationKeyScope CARDIO_STR(@"scope") #define kPPOTConfigurationKeyURLScheme CARDIO_STR(@"scheme") #define kPPOTConfigurationKeyApplications CARDIO_STR(@"applications") #define kPPOTConfigurationKeyEndpoints CARDIO_STR(@"endpoints") #define kPPOTConfigurationKeyURL CARDIO_STR(@"url") #define kPPOTConfigurationKeyCertificateSerialNumber CARDIO_STR(@"certificate_serial_number") #define kPPOTConfigurationKeyCertificate CARDIO_STR(@"certificate") #define kPPOTConfigurationKeyOAuthRecipes CARDIO_STR(@"oauth2_recipes_in_decreasing_priority_order") #define kPPOTConfigurationKeyCheckoutRecipes CARDIO_STR(@"checkout_recipes_in_decreasing_priority_order") #define kPPOTConfigurationKeyBillingAgreementRecipes CARDIO_STR(@"billing_agreement_recipes_in_decreasing_priority_order") #define kPPOTConfigurationValueWallet CARDIO_STR(@"wallet") #define kPPOTConfigurationValueBrowser CARDIO_STR(@"browser") #define kPPOTCoderKeyConfigurationRecipeTarget CARDIO_STR(@"target") #define kPPOTCoderKeyConfigurationRecipeProtocolVersion CARDIO_STR(@"protocol") #define kPPOTCoderKeyConfigurationRecipeSupportedLocales CARDIO_STR(@"supportedLocales") #define kPPOTCoderKeyConfigurationRecipeTargetAppURLScheme CARDIO_STR(@"targetAppURLScheme") #define kPPOTCoderKeyConfigurationRecipeTargetAppBundleIDs CARDIO_STR(@"targetAppBundleIDs") #define kPPOTCoderKeyConfigurationRecipeEndpoints CARDIO_STR(@"endpoints") #define kPPOTCoderKeyConfigurationRecipeScope CARDIO_STR(@"scope") #define kPPOTCoderKeyConfigurationRecipeURL CARDIO_STR(@"url") #define kPPOTCoderKeyConfigurationRecipeCertificateSerialNumber CARDIO_STR(@"certificate_serial_number") #define kPPOTCoderKeyConfigurationRecipeCertificate CARDIO_STR(@"certificate") #define kPPOTCoderKeyConfigurationDownloadTime CARDIO_STR(@"downloadTime") #define kPPOTCoderKeyConfigurationTimestamp CARDIO_STR(@"timestamp") #define kPPOTCoderKeyConfigurationOAuthRecipes CARDIO_STR(@"oAuthRecipes") #define kPPOTCoderKeyConfigurationCheckoutRecipes CARDIO_STR(@"checkoutRecipes") #define kPPOTCoderKeyConfigurationBillingAgreementRecipes CARDIO_STR(@"billingAgreementRecipes") #define kPPOTKeychainConfiguration CARDIO_STR(@"PayPal_OTC_Configuration") #define LOG_ERROR_AND_RETURN_NIL { PPSDKLog(@"Bad configuration: error %d", __LINE__); return nil; } #define STRING_FROM_DICTIONARY(STRING, DICTIONARY, KEY) \ NSString *STRING = [PPOTJSONHelper stringFromDictionary:DICTIONARY withKey:KEY]; \ if (!STRING) LOG_ERROR_AND_RETURN_NIL #define DICTIONARY_FROM_DICTIONARY(DICTIONARY1, DICTIONARY2, KEY, REQUIRED) \ NSDictionary *DICTIONARY1 = [PPOTJSONHelper dictionaryFromDictionary:DICTIONARY2 withKey:KEY]; \ if (REQUIRED && !DICTIONARY1) LOG_ERROR_AND_RETURN_NIL #define STRING_ARRAY_FROM_DICTIONARY(ARRAY, DICTIONARY, KEY, REQUIRED) \ NSArray *ARRAY = [PPOTJSONHelper stringArrayFromDictionary:DICTIONARY withKey:KEY]; \ if (REQUIRED && !ARRAY) LOG_ERROR_AND_RETURN_NIL #define DICTIONARY_ARRAY_FROM_DICTIONARY(ARRAY, DICTIONARY, KEY, REQUIRED) \ NSArray *ARRAY = [PPOTJSONHelper dictionaryArrayFromDictionary:DICTIONARY withKey:KEY]; \ if (REQUIRED && !ARRAY) LOG_ERROR_AND_RETURN_NIL #pragma mark - PPOTConfigurationRecipe @implementation PPOTConfigurationRecipe - (instancetype)initWithDictionary:(NSDictionary *)dictionary { STRING_FROM_DICTIONARY(targetString, dictionary, kPPOTConfigurationKeyTarget) STRING_FROM_DICTIONARY(protocolVersionString, dictionary, kPPOTConfigurationKeyProtocolVersion) NSNumber *protocolVersionNumber = [NSNumber numberWithInteger:[protocolVersionString integerValue]]; if ((self = [super init])) { if ([targetString isEqualToString:kPPOTConfigurationValueWallet]) { _target = PPOTRequestTargetOnDeviceApplication; if (![kPPOTConfigurationSupportedProtocolVersionsForWallet containsObject:protocolVersionNumber]) { LOG_ERROR_AND_RETURN_NIL } _protocolVersion = protocolVersionNumber; STRING_ARRAY_FROM_DICTIONARY(supportedLocalesArray, dictionary, kPPOTConfigurationKeySupportedLocales, NO) // protect against capitalization mistakes: NSMutableArray *uppercasedSupportedLocalesArray = [NSMutableArray arrayWithCapacity:[supportedLocalesArray count]]; for (NSString *locale in supportedLocalesArray) { [uppercasedSupportedLocalesArray addObject:[locale uppercaseString]]; } _supportedLocales = uppercasedSupportedLocalesArray; STRING_FROM_DICTIONARY(targetAppURLScheme, dictionary, kPPOTConfigurationKeyURLScheme) if ([targetAppURLScheme rangeOfString:@":"].location != NSNotFound || [targetAppURLScheme rangeOfString:@"/"].location != NSNotFound) { LOG_ERROR_AND_RETURN_NIL } _targetAppURLScheme = targetAppURLScheme; STRING_ARRAY_FROM_DICTIONARY(targetsArray, dictionary, kPPOTConfigurationKeyApplications, YES) _targetAppBundleIDs = targetsArray; } else if ([targetString isEqualToString:kPPOTConfigurationValueBrowser]) { _target = PPOTRequestTargetBrowser; if (![kPPOTConfigurationSupportedProtocolVersionsForBrowser containsObject:protocolVersionNumber]) { LOG_ERROR_AND_RETURN_NIL } _protocolVersion = protocolVersionNumber; } else { LOG_ERROR_AND_RETURN_NIL } } return self; } #pragma mark - NSCoding - (instancetype)initWithCoder:(NSCoder *)aDecoder { if ((self = [self init])) { _target = ((NSNumber *)[aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationRecipeTarget]).unsignedIntegerValue; _protocolVersion = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationRecipeProtocolVersion]; if (_target == PPOTRequestTargetOnDeviceApplication) { _targetAppURLScheme = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationRecipeTargetAppURLScheme]; _targetAppBundleIDs = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationRecipeTargetAppBundleIDs]; _supportedLocales = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationRecipeSupportedLocales]; } } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:@(self.target) forKey:kPPOTCoderKeyConfigurationRecipeTarget]; [aCoder encodeObject:self.protocolVersion forKey:kPPOTCoderKeyConfigurationRecipeProtocolVersion]; if (self.target == PPOTRequestTargetOnDeviceApplication) { [aCoder encodeObject:self.targetAppURLScheme forKey:kPPOTCoderKeyConfigurationRecipeTargetAppURLScheme]; [aCoder encodeObject:self.targetAppBundleIDs forKey:kPPOTCoderKeyConfigurationRecipeTargetAppBundleIDs]; [aCoder encodeObject:self.supportedLocales forKey:kPPOTCoderKeyConfigurationRecipeSupportedLocales]; } } @end #pragma mark - PPOTConfigurationRecipeEndpoint @implementation PPOTConfigurationRecipeEndpoint - (instancetype)initWithURL:(NSString *)url withCertificateSerialNumber:(NSString *)certificateSerialNumber withBase64EncodedCertificate:(NSString *)base64EncodedCertificate { if ((self = [super init])) { if (![url length] || ![certificateSerialNumber length] || ![base64EncodedCertificate length]) { LOG_ERROR_AND_RETURN_NIL } if (![url hasPrefix:@"https://"] && ![url hasPrefix:@"http://"]) { LOG_ERROR_AND_RETURN_NIL } _url = url; _certificateSerialNumber = certificateSerialNumber; _base64EncodedCertificate = base64EncodedCertificate; } return self; } #pragma mark - NSCoding - (instancetype)initWithCoder:(NSCoder *)aDecoder { if ((self = [super init])) { _url = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationRecipeURL]; _certificateSerialNumber = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationRecipeCertificateSerialNumber]; _base64EncodedCertificate = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationRecipeCertificate]; } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.url forKey:kPPOTCoderKeyConfigurationRecipeURL]; [aCoder encodeObject:self.certificateSerialNumber forKey:kPPOTCoderKeyConfigurationRecipeCertificateSerialNumber]; [aCoder encodeObject:self.base64EncodedCertificate forKey:kPPOTCoderKeyConfigurationRecipeCertificate]; } @end #pragma mark - PPOTConfigurationOAuthRecipe @implementation PPOTConfigurationOAuthRecipe - (instancetype)initWithDictionary:(NSDictionary *)dictionary { if ((self = [super initWithDictionary:dictionary])) { STRING_ARRAY_FROM_DICTIONARY(scopeStrings, dictionary, kPPOTConfigurationKeyScope, YES) _scope = [NSSet setWithArray:scopeStrings]; DICTIONARY_FROM_DICTIONARY(jsonEndpoints, dictionary, kPPOTConfigurationKeyEndpoints, self.target == PPOTRequestTargetBrowser && [self.protocolVersion isEqual:@(3)]) if (![jsonEndpoints count]) { _endpoints = nil; } else { NSMutableDictionary *endpoints = [NSMutableDictionary dictionaryWithCapacity:[jsonEndpoints count]]; for (NSString *environment in jsonEndpoints) { NSString *url = jsonEndpoints[environment][kPPOTConfigurationKeyURL]; NSString *certificateSerialNumber = jsonEndpoints[environment][kPPOTConfigurationKeyCertificateSerialNumber]; NSString *base64EncodedCertificate = jsonEndpoints[environment][kPPOTConfigurationKeyCertificate]; PPOTConfigurationRecipeEndpoint *endpoint = [[PPOTConfigurationRecipeEndpoint alloc] initWithURL:url withCertificateSerialNumber:certificateSerialNumber withBase64EncodedCertificate:base64EncodedCertificate]; if (!endpoint) { LOG_ERROR_AND_RETURN_NIL } endpoints[environment] = endpoint; } if (!endpoints[PPEnvironmentProduction]) { LOG_ERROR_AND_RETURN_NIL } _endpoints = endpoints; } } return self; } #pragma mark - NSCoding - (instancetype)initWithCoder:(NSCoder *)aDecoder { if ((self = [super initWithCoder:aDecoder])) { _scope = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationRecipeScope]; if (self.target == PPOTRequestTargetBrowser) { _endpoints = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationRecipeEndpoints]; } } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [super encodeWithCoder:aCoder]; [aCoder encodeObject:self.scope forKey:kPPOTCoderKeyConfigurationRecipeScope]; if (self.target == PPOTRequestTargetBrowser) { [aCoder encodeObject:self.endpoints forKey:kPPOTCoderKeyConfigurationRecipeEndpoints]; } } @end #pragma mark - PPOTConfigurationCheckoutRecipe @implementation PPOTConfigurationCheckoutRecipe - (instancetype)initWithDictionary:(NSDictionary *)dict { if ((self = [super initWithDictionary:dict])) { // no subclass-specific properties, so far } return self; } #pragma mark - NSCoding - (instancetype)initWithCoder:(NSCoder *)aDecoder { if ((self = [super initWithCoder:aDecoder])) { // no subclass-specific properties, so far } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [super encodeWithCoder:aCoder]; // no subclass-specific properties, so far } @end #pragma mark - PPOTConfigurationBillingAgreementRecipe @implementation PPOTConfigurationBillingAgreementRecipe - (instancetype)initWithDictionary:(NSDictionary *)dict { if ((self = [super initWithDictionary:dict])) { // no subclass-specific properties, so far } return self; } #pragma mark - NSCoding - (instancetype)initWithCoder:(NSCoder *)aDecoder { if ((self = [super initWithCoder:aDecoder])) { // no subclass-specific properties, so far } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [super encodeWithCoder:aCoder]; // no subclass-specific properties, so far } @end #pragma mark - PPOTConfiguration typedef void (^PPOTConfigurationFileDownloadCompletionBlock)(NSData *fileData); @interface PPOTConfiguration () @property (nonatomic, strong, readwrite) NSDate *downloadTime; @end @implementation PPOTConfiguration #pragma mark - debug-only stuff #ifdef DEBUG static BOOL alwaysUseHardcodedConfiguration = NO; + (void)useHardcodedConfiguration:(BOOL)useHardcodedConfiguration { alwaysUseHardcodedConfiguration = useHardcodedConfiguration; } #endif #pragma mark - public methods + (void)updateCacheAsNecessary { // If there is no persisted configuration, or if it's stale, // then download a fresh configuration file and persist it. static int nobodyIsWorkingOnThisAtTheMoment = 1; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if (OSAtomicCompareAndSwapInt(1, 0, &nobodyIsWorkingOnThisAtTheMoment)) { PPOTConfiguration *currentConfiguration = [PPOTConfiguration fetchPersistentConfiguration]; if (!currentConfiguration || fabs([currentConfiguration.downloadTime timeIntervalSinceNow]) > kPPOTConfigurationFileMaximumAcceptableObsolescence) { static NSDate *lastConfigurationFileDownloadAttemptTime = nil; if (!lastConfigurationFileDownloadAttemptTime || fabs([lastConfigurationFileDownloadAttemptTime timeIntervalSinceNow]) > kConfigurationFileDownloadRetryInterval) { lastConfigurationFileDownloadAttemptTime = [NSDate date]; NSURL *url = [NSURL URLWithString:kConfigurationFileDownloadURL]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; [request setHTTPMethod:@"GET"]; // TODO: Can simplify by not specifying timeout interval. This might be better anyway to not specify because of slow networks. PPOTURLSession *session = [PPOTURLSession sessionWithTimeoutIntervalForRequest:kConfigurationFileDownloadTimeout]; [session sendRequest:request completionBlock:^(NSData *data, __attribute__((unused)) NSHTTPURLResponse *response, __attribute__((unused)) NSError *error) { #ifdef DEBUG NSString *dataString = nil; if (data) { dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; } else { dataString = @""; } [[BTLogger sharedLogger] debug:@"Downloaded JSON config\n-> HTTP status: %ld\n-> file contents:\n%@\n", (long)response.statusCode, dataString]; #endif PPOTConfiguration *configuration = data ? [[PPOTConfiguration alloc] initWithJSON:data] : nil; if (configuration) { configuration.downloadTime = [NSDate date]; [PPOTConfiguration storePersistentConfiguration:configuration]; } [session finishTasksAndInvalidate]; }]; } } nobodyIsWorkingOnThisAtTheMoment = 1; } #pragma clang diagnostic pop } + (PPOTConfiguration *)getCurrentConfiguration { #ifdef DEBUG if (alwaysUseHardcodedConfiguration) { return [self defaultConfiguration]; } #endif PPOTConfiguration *currentConfiguration = [PPOTConfiguration fetchPersistentConfiguration]; if (!currentConfiguration) { currentConfiguration = [self defaultConfiguration]; } return currentConfiguration; } + (PPOTConfiguration *)configurationWithDictionary:(NSDictionary *)dictionary { return [[PPOTConfiguration alloc] initWithDictionary:dictionary]; } #pragma mark - private methods + (void)initialize { #ifdef DEBUG NSAssert([PPOTConfiguration defaultConfiguration] != nil, @"otc-config.ios.json is invalid"); #endif if (self == [PPOTConfiguration class]) { [self updateCacheAsNecessary]; } } + (PPOTConfiguration *)defaultConfiguration { NSData *defaultConfigurationJSON = [NSData dataWithBytes:configuration_otc_config_ios_json length:configuration_otc_config_ios_json_len]; #ifdef DEBUG NSString *str = [[NSString alloc] initWithData:defaultConfigurationJSON encoding:NSUTF8StringEncoding]; [[BTLogger sharedLogger] debug:@"Using default JSON config %@\n", str]; #endif PPOTConfiguration *defaultConfiguration = [[PPOTConfiguration alloc] initWithJSON:defaultConfigurationJSON]; return defaultConfiguration; } - (instancetype)initWithDictionary:(NSDictionary *)dictionary { STRING_FROM_DICTIONARY(os, dictionary, kPPOTConfigurationKeyOs) if (![os isEqualToString:@"iOS"]) { LOG_ERROR_AND_RETURN_NIL } STRING_FROM_DICTIONARY(fileTimestamp, dictionary, kPPOTConfigurationKeyFileTimestamp) // Currently we only support config file format 1.0. // If we ever need to update the file format, then the code here would presumably // first look for sub-dictionary "2.0" (or whatever) and then fallback to "1.0" as needed. DICTIONARY_FROM_DICTIONARY(subDictionary, dictionary, @"1.0", YES) DICTIONARY_ARRAY_FROM_DICTIONARY(prioritizedOAuthRecipesDictionaries, subDictionary, kPPOTConfigurationKeyOAuthRecipes, NO) DICTIONARY_ARRAY_FROM_DICTIONARY(prioritizedCheckoutRecipesDictionaries, subDictionary, kPPOTConfigurationKeyCheckoutRecipes, NO) DICTIONARY_ARRAY_FROM_DICTIONARY(prioritizedBillingAgreementRecipesDictionaries, subDictionary, kPPOTConfigurationKeyBillingAgreementRecipes, NO) if ((self = [super init])) { _downloadTime = [NSDate dateWithTimeIntervalSince1970:0]; // by default, mark file as obsolete _fileTimestamp = fileTimestamp; _prioritizedOAuthRecipes = [self prioritizedRecipesFromArray:prioritizedOAuthRecipesDictionaries withRecipeAdapter:^PPOTConfigurationRecipe *(NSDictionary *recipeDictionary) { return [[PPOTConfigurationOAuthRecipe alloc] initWithDictionary:recipeDictionary]; }]; _prioritizedCheckoutRecipes = [self prioritizedRecipesFromArray:prioritizedCheckoutRecipesDictionaries withRecipeAdapter:^PPOTConfigurationRecipe* (NSDictionary* recipeDictionary) { return [[PPOTConfigurationCheckoutRecipe alloc] initWithDictionary:recipeDictionary]; }]; _prioritizedBillingAgreementRecipes = [self prioritizedRecipesFromArray:prioritizedBillingAgreementRecipesDictionaries withRecipeAdapter:^PPOTConfigurationRecipe* (NSDictionary* recipeDictionary) { return [[PPOTConfigurationBillingAgreementRecipe alloc] initWithDictionary:recipeDictionary]; }]; if (!_prioritizedOAuthRecipes || !_prioritizedCheckoutRecipes || !_prioritizedBillingAgreementRecipes) { return nil; } } return self; } - (NSArray*)prioritizedRecipesFromArray:(NSArray*)recipes withRecipeAdapter:(PPOTConfigurationRecipe* (^)(NSDictionary*))recipeAdapter { NSMutableArray *prioritizedRecipes = [NSMutableArray arrayWithCapacity:[recipes count]]; for (NSDictionary *recipeDictionary in recipes) { PPOTConfigurationRecipe *recipe = recipeAdapter(recipeDictionary); if (recipe) { [prioritizedRecipes addObject:recipe]; } } return prioritizedRecipes; } - (instancetype)initWithJSON:(NSData *)jsonData { NSError *error = nil; id jsonObject = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; if (error || ![jsonObject isKindOfClass:[NSDictionary class]]) { LOG_ERROR_AND_RETURN_NIL } self = [self initWithDictionary:((NSDictionary *)jsonObject)]; return self; } #pragma mark - description - (NSString *)description { return [NSString stringWithFormat:@"PPOTConfiguration: %ld Authorization recipes, %ld Checkout recipes, %ld Billing Agreement recipes", (unsigned long)[self.prioritizedOAuthRecipes count], (unsigned long)[self.prioritizedCheckoutRecipes count], (unsigned long)[self.prioritizedBillingAgreementRecipes count]]; } #pragma mark - NSCoding - (instancetype)initWithCoder:(NSCoder *)aDecoder { if ((self = [self init])) { _downloadTime = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationDownloadTime]; _fileTimestamp = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationTimestamp]; _prioritizedOAuthRecipes = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationOAuthRecipes]; _prioritizedCheckoutRecipes = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationCheckoutRecipes]; _prioritizedBillingAgreementRecipes = [aDecoder decodeObjectForKey:kPPOTCoderKeyConfigurationBillingAgreementRecipes]; } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.downloadTime forKey:kPPOTCoderKeyConfigurationDownloadTime]; [aCoder encodeObject:self.fileTimestamp forKey:kPPOTCoderKeyConfigurationTimestamp]; [aCoder encodeObject:self.prioritizedOAuthRecipes forKey:kPPOTCoderKeyConfigurationOAuthRecipes]; [aCoder encodeObject:self.prioritizedCheckoutRecipes forKey:kPPOTCoderKeyConfigurationCheckoutRecipes]; [aCoder encodeObject:self.prioritizedBillingAgreementRecipes forKey:kPPOTCoderKeyConfigurationBillingAgreementRecipes]; } #pragma mark - keychain persistence + (PPOTConfiguration *)fetchPersistentConfiguration { return (PPOTConfiguration *) [PPOTSimpleKeychain unarchiveObjectWithDataForKey:kPPOTKeychainConfiguration]; } + (void)storePersistentConfiguration:(PPOTConfiguration *)configuration { NSData *data = [NSKeyedArchiver archivedDataWithRootObject:configuration]; [PPOTSimpleKeychain setData:data forKey:kPPOTKeychainConfiguration]; } @end @implementation PPConfiguration @end @implementation PPConfigurationCheckoutRecipe @end @implementation PPConfigurationBillingAgreementRecipe @end @implementation PPConfigurationOAuthRecipe @end @implementation PPConfigurationRecipeEndpoint @end