From fdf60444fe9e70fbb7b71377cf8d06c378b0b669 Mon Sep 17 00:00:00 2001 From: Akshet Pandey Date: Fri, 19 May 2017 00:49:49 -0700 Subject: [PATCH] Implement Firebase Remote Config for iOS --- ios/RNFirebase.xcodeproj/project.pbxproj | 6 + ios/RNFirebase/RNFirebase.m | 74 +---------- ios/RNFirebase/RNFirebaseRemoteConfig.h | 14 ++ ios/RNFirebase/RNFirebaseRemoteConfig.m | 161 +++++++++++++++++++++++ lib/firebase.js | 3 + lib/modules/remoteConfig/index.js | 137 +++++++++++++++++++ 6 files changed, 322 insertions(+), 73 deletions(-) create mode 100644 ios/RNFirebase/RNFirebaseRemoteConfig.h create mode 100644 ios/RNFirebase/RNFirebaseRemoteConfig.m create mode 100644 lib/modules/remoteConfig/index.js diff --git a/ios/RNFirebase.xcodeproj/project.pbxproj b/ios/RNFirebase.xcodeproj/project.pbxproj index a59022be..4832aeab 100644 --- a/ios/RNFirebase.xcodeproj/project.pbxproj +++ b/ios/RNFirebase.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ D96290851D6D28B80099A3EC /* RNFirebaseDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = D96290841D6D28B80099A3EC /* RNFirebaseDatabase.m */; }; D9D62E7C1D6D86FD003D826D /* RNFirebaseStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = D9D62E7B1D6D86FD003D826D /* RNFirebaseStorage.m */; }; D9D62E801D6D8717003D826D /* RNFirebaseAuth.m in Sources */ = {isa = PBXBuildFile; fileRef = D9D62E7F1D6D8717003D826D /* RNFirebaseAuth.m */; }; + EC841F001ECE79D6001AD3D9 /* RNFirebaseRemoteConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = EC841EFF1ECE79D6001AD3D9 /* RNFirebaseRemoteConfig.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -48,6 +49,8 @@ D9D62E7B1D6D86FD003D826D /* RNFirebaseStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNFirebaseStorage.m; path = RNFirebase/RNFirebaseStorage.m; sourceTree = ""; }; D9D62E7E1D6D8717003D826D /* RNFirebaseAuth.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNFirebaseAuth.h; path = RNFirebase/RNFirebaseAuth.h; sourceTree = ""; }; D9D62E7F1D6D8717003D826D /* RNFirebaseAuth.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNFirebaseAuth.m; path = RNFirebase/RNFirebaseAuth.m; sourceTree = ""; }; + EC841EFE1ECE79D6001AD3D9 /* RNFirebaseRemoteConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNFirebaseRemoteConfig.h; path = RNFirebase/RNFirebaseRemoteConfig.h; sourceTree = ""; }; + EC841EFF1ECE79D6001AD3D9 /* RNFirebaseRemoteConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNFirebaseRemoteConfig.m; path = RNFirebase/RNFirebaseRemoteConfig.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -82,6 +85,8 @@ D96290351D6D145F0099A3EC /* Modules */ = { isa = PBXGroup; children = ( + EC841EFE1ECE79D6001AD3D9 /* RNFirebaseRemoteConfig.h */, + EC841EFF1ECE79D6001AD3D9 /* RNFirebaseRemoteConfig.m */, D90882D41D89C18C00FB6742 /* RNFirebaseMessaging.h */, D90882D51D89C18C00FB6742 /* RNFirebaseMessaging.m */, D9D62E7E1D6D8717003D826D /* RNFirebaseAuth.h */, @@ -163,6 +168,7 @@ D962903F1D6D15B00099A3EC /* RNFirebaseErrors.m in Sources */, D950369E1D19C77400F7094D /* RNFirebase.m in Sources */, D90882D61D89C18C00FB6742 /* RNFirebaseMessaging.m in Sources */, + EC841F001ECE79D6001AD3D9 /* RNFirebaseRemoteConfig.m in Sources */, 29C199451EA7A851007B6BF8 /* RNFirebaseCrash.m in Sources */, D96290851D6D28B80099A3EC /* RNFirebaseDatabase.m in Sources */, ); diff --git a/ios/RNFirebase/RNFirebase.m b/ios/RNFirebase/RNFirebase.m index 57f65161..35849c4c 100644 --- a/ios/RNFirebase/RNFirebase.m +++ b/ios/RNFirebase/RNFirebase.m @@ -243,82 +243,10 @@ RCT_EXPORT_METHOD(configure:(RCTResponseSenderBlock)callback) callback:callback]; } -#pragma mark - Storage - +#pragma mark Storage #pragma mark RemoteConfig -// RCT_EXPORT_METHOD(setDefaultRemoteConfig:(NSDictionary *)props -// callback:(RCTResponseSenderBlock) callback) -// { -// if (!self.remoteConfigInstance) { -// // Create remote Config instance -// self.remoteConfigInstance = [FIRRemoteConfig remoteConfig]; -// } - -// [self.remoteConfigInstance setDefaults:props]; -// callback(@[[NSNull null], props]); -// } - -// RCT_EXPORT_METHOD(setDev:(RCTResponseSenderBlock) callback) -// { -// FIRRemoteConfigSettings *remoteConfigSettings = [[FIRRemoteConfigSettings alloc] initWithDeveloperModeEnabled:YES]; -// self.remoteConfigInstance.configSettings = remoteConfigSettings; -// callback(@[[NSNull null], @"ok"]); -// } - -// RCT_EXPORT_METHOD(configValueForKey:(NSString *)name -// callback:(RCTResponseSenderBlock) callback) -// { -// if (!self.remoteConfigInstance) { -// NSDictionary *err = @{ -// @"error": @"No configuration instance", -// @"msg": @"No configuration instance set. Please call setDefaultRemoteConfig before using this feature" -// }; -// callback(@[err]); -// } - - -// FIRRemoteConfigValue *value = [self.remoteConfigInstance configValueForKey:name]; -// NSString *valueStr = value.stringValue; - -// if (valueStr == nil) { -// valueStr = @""; -// } -// callback(@[[NSNull null], valueStr]); -// } - -// RCT_EXPORT_METHOD(fetchWithExpiration:(NSNumber*)expirationSeconds -// callback:(RCTResponseSenderBlock) callback) -// { -// if (!self.remoteConfigInstance) { -// NSDictionary *err = @{ -// @"error": @"No configuration instance", -// @"msg": @"No configuration instance set. Please call setDefaultRemoteConfig before using this feature" -// }; -// callback(@[err]); -// } - -// NSTimeInterval expirationDuration = [expirationSeconds doubleValue]; - -// [self.remoteConfigInstance fetchWithExpirationDuration:expirationDuration completionHandler:^(FIRRemoteConfigFetchStatus status, NSError *error) { -// if (status == FIRRemoteConfigFetchStatusSuccess) { -// NSLog(@"Config fetched!"); -// [self.remoteConfigInstance activateFetched]; -// callback(@[[NSNull null], @(YES)]); -// } else { -// NSLog(@"Error %@", error.localizedDescription); - -// NSDictionary *err = @{ -// @"error": @"No configuration instance", -// @"msg": [error localizedDescription] -// }; - -// callback(@[err]); -// } -// }]; -// } - #pragma mark Database #pragma mark Messaging diff --git a/ios/RNFirebase/RNFirebaseRemoteConfig.h b/ios/RNFirebase/RNFirebaseRemoteConfig.h new file mode 100644 index 00000000..82be031f --- /dev/null +++ b/ios/RNFirebase/RNFirebaseRemoteConfig.h @@ -0,0 +1,14 @@ +#ifndef RNFirebaseRemoteConfig_h +#define RNFirebaseRemoteConfig_h + +#if __has_include() +#import +#else // Compatibility for RN version < 0.40 +#import "RCTBridgeModule.h" +#endif + +@interface RNFirebaseRemoteConfig : NSObject + +@end + +#endif diff --git a/ios/RNFirebase/RNFirebaseRemoteConfig.m b/ios/RNFirebase/RNFirebaseRemoteConfig.m new file mode 100644 index 00000000..fe2a5638 --- /dev/null +++ b/ios/RNFirebase/RNFirebaseRemoteConfig.m @@ -0,0 +1,161 @@ +#import "RNFirebaseRemoteConfig.h" + +#if __has_include() +#import +#else // Compatibility for RN version < 0.40 +#import "RCTConvert.h" +#endif +#if __has_include() +#import +#else // Compatibility for RN version < 0.40 +#import "RCTUtils.h" +#endif + +#import "FirebaseRemoteConfig/FirebaseRemoteConfig.h" + +NSString *convertFIRRemoteConfigFetchStatusToNSString(FIRRemoteConfigFetchStatus value) +{ + switch(value){ + case FIRRemoteConfigFetchStatusNoFetchYet: + return @"remoteConfitFetchStatusNoFetchYet"; + case FIRRemoteConfigFetchStatusSuccess: + return @"remoteConfitFetchStatusSuccess"; + case FIRRemoteConfigFetchStatusFailure: + return @"remoteConfitFetchStatusFailure"; + case FIRRemoteConfigFetchStatusThrottled: + return @"remoteConfitFetchStatusThrottled"; + default: + return @"remoteConfitFetchStatusFailure"; + } +} + +NSString *convertFIRRemoteConfigSourceToNSString(FIRRemoteConfigSource value) +{ + switch(value) { + case FIRRemoteConfigSourceRemote: + return @"remoteConfigSourceRemote"; + case FIRRemoteConfigSourceDefault: + return @"remoteConfigSourceDefault"; + case FIRRemoteConfigSourceStatic: + return @"remoteConfigSourceStatic"; + default: + return @"remoteConfigSourceStatic"; + } +} + +NSDictionary *convertFIRRemoteConfigValueToNSDictionary(FIRRemoteConfigValue *value) +{ + return @{ + @"stringValue" : value.stringValue ?: [NSNull null], + @"numberValue" : value.numberValue ?: [NSNull null], + @"dataValue" : value.dataValue ? [value.dataValue base64EncodedStringWithOptions:0] : [NSNull null], + @"boolValue" : @(value.boolValue), + @"source" : convertFIRRemoteConfigSourceToNSString(value.source) + }; +} + +@interface RNFirebaseRemoteConfig () + +@property (nonatomic, readwrite, weak) FIRRemoteConfig *remoteConfig; + +@end + +@implementation RNFirebaseRemoteConfig + +RCT_EXPORT_MODULE(RNFirebaseRemoteConfig); + +- (id)init +{ + if (self = [super init]) { + _remoteConfig = [FIRRemoteConfig remoteConfig]; + } + return self; +} + +RCT_EXPORT_METHOD(enableDeveloperMode) +{ + FIRRemoteConfigSettings *remoteConfigSettings = [[FIRRemoteConfigSettings alloc] initWithDeveloperModeEnabled:YES]; + self.remoteConfig.configSettings = remoteConfigSettings; +} + +RCT_EXPORT_METHOD(fetch:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + [self.remoteConfig fetchWithCompletionHandler:^(FIRRemoteConfigFetchStatus status, NSError *__nullable error) { + if (error) { + RCTLogError(@"\nError: %@", RCTJSErrorFromNSError(error)); + reject(convertFIRRemoteConfigFetchStatusToNSString(status), error.localizedDescription, error); + } else { + resolve(convertFIRRemoteConfigFetchStatusToNSString(status)); + } + }]; +} + +RCT_EXPORT_METHOD(fetchWithExpirationDuration:(NSNumber *)expirationDuration + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + [self.remoteConfig fetchWithExpirationDuration:expirationDuration.doubleValue completionHandler:^(FIRRemoteConfigFetchStatus status, NSError *__nullable error) { + if (error) { + RCTLogError(@"\nError: %@", RCTJSErrorFromNSError(error)); + reject(convertFIRRemoteConfigFetchStatusToNSString(status), error.localizedDescription, error); + } else { + resolve(convertFIRRemoteConfigFetchStatusToNSString(status)); + } + }]; +} + +RCT_EXPORT_METHOD(activateFetched:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + BOOL status = [self.remoteConfig activateFetched]; + if (status) { + resolve(@(status)); + } else { + reject(@"activate_failed", @"Did not activate remote config", nil); + } +} + +RCT_EXPORT_METHOD(configValueForKey:(NSString *)key + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + FIRRemoteConfigValue *value = [self.remoteConfig configValueForKey:key]; + resolve(convertFIRRemoteConfigValueToNSDictionary(value)); +} + +RCT_EXPORT_METHOD(configValuesForKeys:(NSArray *)keys + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSMutableDictionary *res = [[NSMutableDictionary alloc] init]; + for (NSString *key in keys) { + FIRRemoteConfigValue *value = [self.remoteConfig configValueForKey:key]; + res[key] = convertFIRRemoteConfigValueToNSDictionary(value); + } + resolve(res); +} + +RCT_EXPORT_METHOD(keysWithPrefix:(NSString *)prefix + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSSet *keys = [self.remoteConfig keysWithPrefix:prefix]; + if (keys.count) { + resolve(keys); + } else { + reject(@"no_keys_matching_prefix", @"There are no keys that match that prefix", nil); + } +} + +RCT_EXPORT_METHOD(setDefaults:(NSDictionary *)defaults) +{ + [self.remoteConfig setDefaults:defaults]; +} + +RCT_EXPORT_METHOD(setDefaultsFromPlistFileName:(NSString *)fileName) +{ + [self.remoteConfig setDefaultsFromPlistFileName:fileName]; +} + +@end diff --git a/lib/firebase.js b/lib/firebase.js index 4abfc79d..6e99fb56 100644 --- a/lib/firebase.js +++ b/lib/firebase.js @@ -14,6 +14,7 @@ import Database, { statics as DatabaseStatics } from './modules/database'; import Messaging, { statics as MessagingStatics } from './modules/messaging'; import Analytics from './modules/analytics'; import Crash from './modules/crash'; +import RemoteConfig from './modules/remoteConfig'; const instances: Object = { default: null }; const FirebaseModule = NativeModules.RNFirebase; @@ -40,6 +41,7 @@ export default class Firebase { database: Function; analytics: Function; messaging: Function; + remoteConfig: Function; eventHandlers: Object; debug: boolean; @@ -83,6 +85,7 @@ export default class Firebase { this.messaging = this._staticsOrInstance('messaging', MessagingStatics, Messaging); this.analytics = this._staticsOrInstance('analytics', {}, Analytics); this.crash = this._staticsOrInstance('crash', {}, Crash); + this.remoteConfig = this._staticsOrInstance('remoteConfig', {}, RemoteConfig); // init auth to start listeners this.auth(); diff --git a/lib/modules/remoteConfig/index.js b/lib/modules/remoteConfig/index.js new file mode 100644 index 00000000..a5edc54a --- /dev/null +++ b/lib/modules/remoteConfig/index.js @@ -0,0 +1,137 @@ +/** + * @flow + */ +import { NativeModules } from 'react-native'; + +import { Base } from './../base'; + +const FirebaseRemoteConfig = NativeModules.RNFirebaseRemoteConfig; + +type RemoteConfigOptions = {} + +/** + * @class Config + */ +export default class RemoteConfig extends Base { + constructor(firebase: Object, options: RemoteConfigOptions = {}) { + super(firebase, options); + this.namespace = 'firebase:config'; + this.developerModeEnabled = false; + } + + /** + * Enable Remote Config developer mode to allow for frequent refreshes of the cache + */ + enableDeveloperMode() { + if (!this.developerModeEnabled) { + this.log.debug('Enabled developer mode'); + FirebaseRemoteConfig.enableDeveloperMode(); + this.developerModeEnabled = true + } + } + + /** + * Fetches Remote Config data + * Call activateFetched to make fetched data available in app + * @returns {*|Promise.}: + * One of + * - remoteConfitFetchStatusSuccess + * - remoteConfitFetchStatusFailure + * - remoteConfitFetchStatusThrottled + * rejects on remoteConfitFetchStatusFailure and remoteConfitFetchStatusThrottled + * resolves on remoteConfitFetchStatusSuccess + */ + fetch() { + this.log.debug('Fetching remote config data'); + return FirebaseRemoteConfig.fetch(); + } + + /** + * Fetches Remote Config data and sets a duration that specifies how long config data lasts. + * Call activateFetched to make fetched data available + * @param expiration: Duration that defines how long fetched config data is available, in + * seconds. When the config data expires, a new fetch is required. + * @returns {*|Promise.} + * One of + * - remoteConfitFetchStatusSuccess + * - remoteConfitFetchStatusFailure + * - remoteConfitFetchStatusThrottled + * rejects on remoteConfitFetchStatusFailure and remoteConfitFetchStatusThrottled + * resolves on remoteConfitFetchStatusSuccess + */ + fetchWithExpirationDuration(expiration: Number) { + this.log.debug(`Fetching remote config data with expiration ${expiration.toString()}`); + return FirebaseRemoteConfig.fetchWithExpirationDuration(expiration); + } + + /** + * Applies Fetched Config data to the Active Config + * @returns {*|Promise.} + * resolves if there was a Fetched Config, and it was activated, + * rejects if no Fetched Config was found, or the Fetched Config was already activated. + */ + activateFetched() { + this.log.debug('Activating remote config'); + return FirebaseRemoteConfig.activateFetched(); + } + + /** + * Gets the config value of the default namespace. + * @param key: Config key + * @returns {*|Promise.}, will always resolve + * Object looks like + * { + * "stringValue" : stringValue, + * "numberValue" : numberValue, + * "dataValue" : dataValue, + * "boolValue" : boolValue, + * "source" : OneOf(remoteConfigSourceRemote|remoteConfigSourceDefault|remoteConfigSourceStatic) + * } + */ + configValueForKey(key: String) { + return FirebaseRemoteConfig.configValueForKey(key); + } + + /** + * Gets the config value of the default namespace. + * @param key: Config key + * @returns {*|Promise.}, will always resolve. + * Result will be a dictionary of key and config objects + * Object looks like + * { + * "stringValue" : stringValue, + * "numberValue" : numberValue, + * "dataValue" : dataValue, + * "boolValue" : boolValue, + * "source" : OneOf(remoteConfigSourceRemote|remoteConfigSourceDefault|remoteConfigSourceStatic) + * } + */ + configValuesForKeys(keys: Array) { + return FirebaseRemoteConfig.configValuesForKeys(keys); + } + + /** + * Get the set of parameter keys that start with the given prefix, from the default namespace + * @param prefix: The key prefix to look for. If prefix is nil or empty, returns all the keys. + * @returns {*|Promise.>} + */ + keysWithPrefix(prefix: String) { + return FirebaseRemoteConfig.keysWithPrefix(prefix); + } + + /** + * Sets config defaults for parameter keys and values in the default namespace config. + * @param defaults: A dictionary mapping a String key to a Object values. + */ + setDefaults(defaults: Object) { + FirebaseRemoteConfig.setDefaults(defaults); + } + + /** + * Sets default configs from plist for default namespace; + * @param filename: The plist file name, with no file name extension + */ + setDefaultsFromPlistFileName(filename: String) { + FirebaseRemoteConfig.setDefaultsFromPlistFileName(filename); + } +}