This commit is contained in:
Yao
2024-12-20 17:49:45 +08:00
parent 86b0363ce1
commit 654d456c7d
7011 changed files with 1705926 additions and 7 deletions

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <FirebaseCore/FIRApp.h>
@class FIRComponentContainer;
@class FIRHeartbeatLogger;
@protocol FIRLibrary;
/**
* The internal interface to `FirebaseApp`. This is meant for first-party integrators, who need to
* receive `FirebaseApp` notifications, log info about the success or failure of their
* configuration, and access other internal functionality of `FirebaseApp`.
*/
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, FIRConfigType) {
FIRConfigTypeCore = 1,
FIRConfigTypeSDK = 2,
};
extern NSString *const kFIRDefaultAppName;
extern NSString *const kFIRAppReadyToConfigureSDKNotification;
extern NSString *const kFIRAppDeleteNotification;
extern NSString *const kFIRAppIsDefaultAppKey;
extern NSString *const kFIRAppNameKey;
extern NSString *const kFIRGoogleAppIDKey;
extern NSString *const kFirebaseCoreErrorDomain;
/** The `UserDefaults` suite name for `FirebaseCore`, for those storage locations that use it. */
extern NSString *const kFirebaseCoreDefaultsSuiteName;
/**
* The format string for the `UserDefaults` key used for storing the data collection enabled flag.
* This includes formatting to append the `FirebaseApp`'s name.
*/
extern NSString *const kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat;
/**
* The plist key used for storing the data collection enabled flag.
*/
extern NSString *const kFIRGlobalAppDataCollectionEnabledPlistKey;
/** @var FirebaseAuthStateDidChangeInternalNotification
@brief The name of the @c NotificationCenter notification which is posted when the auth state
changes (e.g. a new token has been produced, a user logs in or out). The object parameter of
the notification is a dictionary possibly containing the key:
@c FirebaseAuthStateDidChangeInternalNotificationTokenKey (the new access token.) If it does not
contain this key it indicates a sign-out event took place.
*/
extern NSString *const FIRAuthStateDidChangeInternalNotification;
/** @var FirebaseAuthStateDidChangeInternalNotificationTokenKey
@brief A key present in the dictionary object parameter of the
@c FirebaseAuthStateDidChangeInternalNotification notification. The value associated with this
key will contain the new access token.
*/
extern NSString *const FIRAuthStateDidChangeInternalNotificationTokenKey;
/** @var FirebaseAuthStateDidChangeInternalNotificationAppKey
@brief A key present in the dictionary object parameter of the
@c FirebaseAuthStateDidChangeInternalNotification notification. The value associated with this
key will contain the FirebaseApp associated with the auth instance.
*/
extern NSString *const FIRAuthStateDidChangeInternalNotificationAppKey;
/** @var FirebaseAuthStateDidChangeInternalNotificationUIDKey
@brief A key present in the dictionary object parameter of the
@c FirebaseAuthStateDidChangeInternalNotification notification. The value associated with this
key will contain the new user's UID (or nil if there is no longer a user signed in).
*/
extern NSString *const FIRAuthStateDidChangeInternalNotificationUIDKey;
@interface FIRApp ()
/**
* A flag indicating if this is the default app (has the default app name).
*/
@property(nonatomic, readonly) BOOL isDefaultApp;
/**
* The container of interop SDKs for this app.
*/
@property(nonatomic) FIRComponentContainer *container;
/**
* The heartbeat logger associated with this app.
*
* Firebase apps have a 1:1 relationship with heartbeat loggers.
*/
@property(readonly) FIRHeartbeatLogger *heartbeatLogger;
/**
* Checks if the default app is configured without trying to configure it.
*/
+ (BOOL)isDefaultAppConfigured;
/**
* Registers a given third-party library with the given version number to be reported for
* analytics.
*
* @param name Name of the library.
* @param version Version of the library.
*/
+ (void)registerLibrary:(nonnull NSString *)name withVersion:(nonnull NSString *)version;
/**
* Registers a given internal library to be reported for analytics.
*
* @param library Optional parameter for component registration.
* @param name Name of the library.
*/
+ (void)registerInternalLibrary:(nonnull Class<FIRLibrary>)library
withName:(nonnull NSString *)name;
/**
* Registers a given internal library with the given version number to be reported for
* analytics. This should only be used for non-Firebase libraries that have their own versioning
* scheme.
*
* @param library Optional parameter for component registration.
* @param name Name of the library.
* @param version Version of the library.
*/
+ (void)registerInternalLibrary:(nonnull Class<FIRLibrary>)library
withName:(nonnull NSString *)name
withVersion:(nonnull NSString *)version;
/**
* A concatenated string representing all the third-party libraries and version numbers.
*/
+ (NSString *)firebaseUserAgent;
/**
* Can be used by the unit tests in each SDK to reset `FirebaseApp`. This method is thread unsafe.
*/
+ (void)resetApps;
/**
* Can be used by the unit tests in each SDK to set customized options.
*/
- (instancetype)initInstanceWithName:(NSString *)name options:(FIROptions *)options;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2018 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class FIRApp;
@class FIRComponentContainer;
NS_ASSUME_NONNULL_BEGIN
/// Provides a system to clean up cached instances returned from the component system.
NS_SWIFT_NAME(ComponentLifecycleMaintainer)
@protocol FIRComponentLifecycleMaintainer
/// The associated app will be deleted, clean up any resources as they are about to be deallocated.
- (void)appWillBeDeleted:(FIRApp *)app;
@end
typedef _Nullable id (^FIRComponentCreationBlock)(FIRComponentContainer *container,
BOOL *isCacheable)
NS_SWIFT_NAME(ComponentCreationBlock);
@class FIRDependency;
/// Describes the timing of instantiation. Note: new components should default to lazy unless there
/// is a strong reason to be eager.
typedef NS_ENUM(NSInteger, FIRInstantiationTiming) {
FIRInstantiationTimingLazy,
FIRInstantiationTimingAlwaysEager,
FIRInstantiationTimingEagerInDefaultApp
} NS_SWIFT_NAME(InstantiationTiming);
/// A component that can be used from other Firebase SDKs.
NS_SWIFT_NAME(Component)
@interface FIRComponent : NSObject
/// The protocol describing functionality provided from the `Component`.
@property(nonatomic, strong, readonly) Protocol *protocol;
/// The timing of instantiation.
@property(nonatomic, readonly) FIRInstantiationTiming instantiationTiming;
/// An array of dependencies for the component.
@property(nonatomic, copy, readonly) NSArray<FIRDependency *> *dependencies;
/// A block to instantiate an instance of the component with the appropriate dependencies.
@property(nonatomic, copy, readonly) FIRComponentCreationBlock creationBlock;
// There's an issue with long NS_SWIFT_NAMES that causes compilation to fail, disable clang-format
// for the next two methods.
// clang-format off
/// Creates a component with no dependencies that will be lazily initialized.
+ (instancetype)componentWithProtocol:(Protocol *)protocol
creationBlock:(FIRComponentCreationBlock)creationBlock
NS_SWIFT_NAME(init(_:creationBlock:));
/// Creates a component to be registered with the component container.
///
/// @param protocol - The protocol describing functionality provided by the component.
/// @param instantiationTiming - When the component should be initialized. Use .lazy unless there's
/// a good reason to be instantiated earlier.
/// @param dependencies - Any dependencies the `implementingClass` has, optional or required.
/// @param creationBlock - A block to instantiate the component with a container, and if
/// @return A component that can be registered with the component container.
+ (instancetype)componentWithProtocol:(Protocol *)protocol
instantiationTiming:(FIRInstantiationTiming)instantiationTiming
dependencies:(NSArray<FIRDependency *> *)dependencies
creationBlock:(FIRComponentCreationBlock)creationBlock
NS_SWIFT_NAME(init(_:instantiationTiming:dependencies:creationBlock:));
// clang-format on
/// Unavailable.
- (instancetype)init NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2018 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// A type-safe macro to retrieve a component from a container. This should be used to retrieve
/// components instead of using the container directly.
#define FIR_COMPONENT(type, container) \
[FIRComponentType<id<type>> instanceForProtocol:@protocol(type) inContainer:container]
@class FIRApp;
/// A container that holds different components that are registered via the
/// `registerAsComponentRegistrant` call. These classes should conform to `ComponentRegistrant`
/// in order to properly register components for Core.
NS_SWIFT_NAME(FirebaseComponentContainer)
@interface FIRComponentContainer : NSObject
/// A weak reference to the app that an instance of the container belongs to.
@property(nonatomic, weak, readonly) FIRApp *app;
// TODO: See if we can get improved type safety here.
/// A Swift only API for fetching an instance since the top macro isn't available.
- (nullable id)__instanceForProtocol:(Protocol *)protocol NS_SWIFT_NAME(instance(for:));
/// Unavailable. Use the `container` property on `FirebaseApp`.
- (instancetype)init NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2018 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class FIRComponentContainer;
NS_ASSUME_NONNULL_BEGIN
/// Do not use directly. A placeholder type in order to provide a macro that will warn users of
/// mis-matched protocols.
NS_SWIFT_NAME(ComponentType)
@interface FIRComponentType<__covariant T> : NSObject
/// Do not use directly. A factory method to retrieve an instance that provides a specific
/// functionality.
+ (nullable T)instanceForProtocol:(Protocol *)protocol
inContainer:(FIRComponentContainer *)container;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2018 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// A dependency on a specific protocol's functionality.
NS_SWIFT_NAME(Dependency)
@interface FIRDependency : NSObject
/// The protocol describing functionality being depended on.
@property(nonatomic, strong, readonly) Protocol *protocol;
/// A flag to specify if the dependency is required or not.
@property(nonatomic, readonly) BOOL isRequired;
/// Initializes a dependency that is required. Calls `init(protocol:isRequired:)` with true for
/// the required parameter.
/// Creates a required dependency on the specified protocol's functionality.
+ (instancetype)dependencyWithProtocol:(Protocol *)protocol;
/// Creates a dependency on the specified protocol's functionality and specify if it's required for
/// the class's functionality.
+ (instancetype)dependencyWithProtocol:(Protocol *)protocol isRequired:(BOOL)required;
/// Use `init(withProtocol:isRequired:)` instead.
- (instancetype)init NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,90 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#ifndef FIREBASE_BUILD_CMAKE
@class FIRHeartbeatsPayload;
#endif // FIREBASE_BUILD_CMAKE
/// Enum representing different daily heartbeat codes.
/// This enum is only used by clients using platform logging V1. This is because
/// the V1 payload only supports a single daily heartbeat.
typedef NS_ENUM(NSInteger, FIRDailyHeartbeatCode) {
/// Represents the absence of a daily heartbeat.
FIRDailyHeartbeatCodeNone = 0,
/// Represents the presence of a daily heartbeat.
FIRDailyHeartbeatCodeSome = 2,
};
@protocol FIRHeartbeatLoggerProtocol <NSObject>
/// Asynchronously logs a heartbeat.
- (void)log;
#ifndef FIREBASE_BUILD_CMAKE
/// Flushes heartbeats from storage into a structured payload of heartbeats.
- (FIRHeartbeatsPayload *)flushHeartbeatsIntoPayload;
#endif // FIREBASE_BUILD_CMAKE
/// Gets the heartbeat code for today.
- (FIRDailyHeartbeatCode)heartbeatCodeForToday;
@end
#ifndef FIREBASE_BUILD_CMAKE
/// Returns a nullable string header value from a given heartbeats payload.
///
/// This API returns `nil` when the given heartbeats payload is considered empty.
///
/// @param heartbeatsPayload The heartbeats payload.
NSString *_Nullable FIRHeaderValueFromHeartbeatsPayload(FIRHeartbeatsPayload *heartbeatsPayload);
#endif // FIREBASE_BUILD_CMAKE
/// A thread safe, synchronized object that logs and flushes platform logging info.
@interface FIRHeartbeatLogger : NSObject <FIRHeartbeatLoggerProtocol>
/// Designated initializer.
///
/// @param appID The app ID that this heartbeat logger corresponds to.
- (instancetype)initWithAppID:(NSString *)appID;
/// Asynchronously logs a new heartbeat corresponding to the Firebase User Agent, if needed.
///
/// @note This API is thread-safe.
- (void)log;
#ifndef FIREBASE_BUILD_CMAKE
/// Flushes heartbeats from storage into a structured payload of heartbeats.
///
/// This API is for clients using platform logging V2.
///
/// @note This API is thread-safe.
/// @return A payload of heartbeats.
- (FIRHeartbeatsPayload *)flushHeartbeatsIntoPayload;
#endif // FIREBASE_BUILD_CMAKE
/// Gets today's corresponding heartbeat code.
///
/// This API is for clients using platform logging V1.
///
/// @note This API is thread-safe.
/// @return Heartbeat code indicating whether or not there is an unsent global heartbeat.
- (FIRDailyHeartbeatCode)heartbeatCodeForToday;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2018 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef FIRLibrary_h
#define FIRLibrary_h
#import <Foundation/Foundation.h>
@class FIRApp;
@class FIRComponent;
NS_ASSUME_NONNULL_BEGIN
/// Provide an interface to register a library for userAgent logging and availability to others.
NS_SWIFT_NAME(Library)
@protocol FIRLibrary
/// Returns one or more Components that will be registered in
/// FirebaseApp and participate in dependency resolution and injection.
+ (NSArray<FIRComponent *> *)componentsToRegister;
@optional
/// Implement this method if the library needs notifications for lifecycle events. This method is
/// called when the developer calls `FirebaseApp.configure()`.
+ (void)configureWithApp:(FIRApp *)app;
@end
NS_ASSUME_NONNULL_END
#endif /* FIRLibrary_h */

View File

@@ -0,0 +1,193 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <FirebaseCore/FIRLoggerLevel.h>
NS_ASSUME_NONNULL_BEGIN
/**
* The Firebase services used in Firebase logger.
*/
typedef NSString *const FIRLoggerService;
extern FIRLoggerService kFIRLoggerAnalytics;
extern FIRLoggerService kFIRLoggerCrash;
extern FIRLoggerService kFIRLoggerCore;
extern FIRLoggerService kFIRLoggerRemoteConfig;
/**
* The key used to store the logger's error count.
*/
extern NSString *const kFIRLoggerErrorCountKey;
/**
* The key used to store the logger's warning count.
*/
extern NSString *const kFIRLoggerWarningCountKey;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
/**
* Enables or disables Analytics debug mode.
* If set to true, the logging level for Analytics will be set to FirebaseLoggerLevelDebug.
* Enabling the debug mode has no effect if the app is running from App Store.
* (required) analytics debug mode flag.
*/
void FIRSetAnalyticsDebugMode(BOOL analyticsDebugMode);
/**
* Gets the current FIRLoggerLevel.
*/
FIRLoggerLevel FIRGetLoggerLevel(void);
/**
* Changes the default logging level of FirebaseLoggerLevelNotice to a user-specified level.
* The default level cannot be set above FirebaseLoggerLevelNotice if the app is running from App
* Store. (required) log level (one of the FirebaseLoggerLevel enum values).
*/
void FIRSetLoggerLevel(FIRLoggerLevel loggerLevel);
/**
* Checks if the specified logger level is loggable given the current settings.
* (required) log level (one of the FirebaseLoggerLevel enum values).
* (required) whether or not this function is called from the Analytics component.
*/
BOOL FIRIsLoggableLevel(FIRLoggerLevel loggerLevel, BOOL analyticsComponent);
/**
* Logs a message to the Xcode console and the device log. If running from AppStore, will
* not log any messages with a level higher than FirebaseLoggerLevelNotice to avoid log spamming.
* (required) log level (one of the FirebaseLoggerLevel enum values).
* (required) service name of type FirebaseLoggerService.
* (required) message code starting with "I-" which means iOS, followed by a capitalized
* three-character service identifier and a six digit integer message ID that is unique
* within the service.
* An example of the message code is @"I-COR000001".
* (required) message string which can be a format string.
* (optional) variable arguments list obtained from calling va_start, used when message is a format
* string.
*/
extern void FIRLogBasic(FIRLoggerLevel level,
FIRLoggerService service,
NSString *messageCode,
NSString *message,
// On 64-bit simulators, va_list is not a pointer, so cannot be marked nullable
// See: http://stackoverflow.com/q/29095469
#if __LP64__ && TARGET_OS_SIMULATOR || TARGET_OS_OSX
va_list args_ptr
#else
va_list _Nullable args_ptr
#endif
);
/**
* The following functions accept the following parameters in order:
* (required) service name of type FirebaseLoggerService.
* (required) message code starting from "I-" which means iOS, followed by a capitalized
* three-character service identifier and a six digit integer message ID that is unique
* within the service.
* An example of the message code is @"I-COR000001".
* See go/firebase-log-proposal for details.
* (required) message string which can be a format string.
* (optional) the list of arguments to substitute into the format string.
* Example usage:
* FirebaseLogError(kFirebaseLoggerCore, @"I-COR000001", @"Configuration of %@ failed.", app.name);
*/
extern void FIRLogError(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
NS_FORMAT_FUNCTION(3, 4);
extern void FIRLogWarning(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
NS_FORMAT_FUNCTION(3, 4);
extern void FIRLogNotice(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
NS_FORMAT_FUNCTION(3, 4);
extern void FIRLogInfo(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
NS_FORMAT_FUNCTION(3, 4);
extern void FIRLogDebug(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
NS_FORMAT_FUNCTION(3, 4);
// TODO: Come up with a better logging scheme for Swift.
/**
* Logs a debug message to the Xcode console and the device log. If running from AppStore, will
* not log any messages with a level higher than FirebaseLoggerLevelNotice to avoid log spamming.
* This function is intended to be used by Swift clients that do not support variadic parameters.
*
* @param service The service name of type `FirebaseLoggerService`.
* @param messageCode The mesage code. starting with "I-" which means iOS, followed by a capitalized
* three-character service identifier and a six digit integer message ID that is unique within the
* service. An example of the message code is @"I-COR000001".
* @param message The message string.
*/
extern void FIRLogDebugSwift(FIRLoggerService service, NSString *messageCode, NSString *message);
/**
* Logs a warning message to the Xcode console and the device log. If running from AppStore, will
* not log any messages with a level higher than FirebaseLoggerLevelNotice to avoid log spamming.
* This function is intended to be used by Swift clients that do not support variadic parameters.
*
* @param service The service name of type `FirebaseLoggerService`.
* @param messageCode The mesage code. starting with "I-" which means iOS, followed by a capitalized
* three-character service identifier and a six digit integer message ID that is unique within the
* service. An example of the message code is @"I-COR000001".
* @param message The message string.
*/
extern void FIRLogWarningSwift(FIRLoggerService service, NSString *messageCode, NSString *message);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
NS_SWIFT_NAME(FirebaseLogger)
@interface FIRLoggerWrapper : NSObject
/// Logs a given message at a given log level. This API is effectively a wrapper for the
/// `FIRLogBasic` C API.
///
/// - Parameters:
/// - level: The log level to use (defined by `FirebaseLoggerLevel` enum values).
/// - service: The service name of type `FirebaseLoggerService`.
/// - code: The mesage code. Starting with "I-" which means iOS, followed by a capitalized
/// three-character service identifier and a six digit integer message ID that is unique within
/// the service. An example of the message code is @"I-COR000001".
/// - message: Formatted string to be used as the log's message.
/// - args: Arguments list obtained from calling `va_start`, used when message is a format string.
+ (void)logWithLevel:(FIRLoggerLevel)level
withService:(FIRLoggerService)service
withCode:(NSString *)messageCode
withMessage:(NSString *)message
withArgs:(va_list)args;
/// Logs a given message at a given log level.
///
/// - Parameters:
/// - level: The log level to use (defined by `FirebaseLoggerLevel` enum values).
/// - service: The service name of type `FirebaseLoggerService`.
/// - code: The mesage code. Starting with "I-" which means iOS, followed by a capitalized
/// three-character service identifier and a six digit integer message ID that is unique within
/// the service. An example of the message code is @"I-COR000001".
/// - message: Formatted string to be used as the log's message.
/// - args: Arguments list obtained from calling `va_start`, used when message is a format string.
+ (void)logWithLevel:(FIRLoggerLevel)level
service:(FIRLoggerService)service
code:(NSString *)code
message:(NSString *)message
__attribute__((__swift_name__("log(level:service:code:message:)")));
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <FirebaseCore/FIROptions.h>
/**
* Keys for the strings in the plist file.
*/
extern NSString *const kFIRAPIKey;
extern NSString *const kFIRTrackingID;
extern NSString *const kFIRGoogleAppID;
extern NSString *const kFIRClientID;
extern NSString *const kFIRGCMSenderID;
extern NSString *const kFIRAndroidClientID;
extern NSString *const kFIRDatabaseURL;
extern NSString *const kFIRStorageBucket;
extern NSString *const kFIRBundleID;
extern NSString *const kFIRProjectID;
/**
* Keys for the plist file name
*/
extern NSString *const kServiceInfoFileName;
extern NSString *const kServiceInfoFileType;
/**
* This header file exposes the initialization of FirebaseOptions to internal use.
*/
@interface FIROptions ()
/**
* `resetDefaultOptions` and `initInternalWithOptionsDictionary` are exposed only for unit tests.
*/
+ (void)resetDefaultOptions;
/**
* Initializes the options with dictionary. The above strings are the keys of the dictionary.
* This is the designated initializer.
*/
- (instancetype)initInternalWithOptionsDictionary:(NSDictionary *)serviceInfoDictionary
NS_DESIGNATED_INITIALIZER;
/**
* `defaultOptions` and `defaultOptionsDictionary` are exposed in order to be used in FirebaseApp
* and other first party services.
*/
+ (FIROptions *)defaultOptions;
+ (NSDictionary *)defaultOptionsDictionary;
/**
* Indicates whether or not Analytics collection was explicitly enabled via a plist flag or at
* runtime.
*/
@property(nonatomic, readonly) BOOL isAnalyticsCollectionExplicitlySet;
/**
* Whether or not Analytics Collection was enabled. Analytics Collection is enabled unless
* explicitly disabled in GoogleService-Info.plist.
*/
@property(nonatomic, readonly) BOOL isAnalyticsCollectionEnabled;
/**
* Whether or not Analytics Collection was completely disabled. If true, then
* isAnalyticsCollectionEnabled will be false.
*/
@property(nonatomic, readonly) BOOL isAnalyticsCollectionDeactivated;
/**
* The version ID of the client library, e.g. @"1100000".
*/
@property(nonatomic, readonly, copy) NSString *libraryVersionID;
/**
* The flag indicating whether this object was constructed with the values in the default plist
* file.
*/
@property(nonatomic) BOOL usingOptionsFromDefaultPlist;
/**
* Whether or not Measurement was enabled. Measurement is enabled unless explicitly disabled in
* GoogleService-Info.plist.
*/
@property(nonatomic, readonly) BOOL isMeasurementEnabled;
/**
* Whether or not editing is locked. This should occur after `FirebaseOptions` has been set on a
* `FirebaseApp`.
*/
@property(nonatomic, getter=isEditingLocked) BOOL editingLocked;
@end

View File

@@ -0,0 +1,25 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
@import FirebaseCore;
#import "FIRAppInternal.h"
#import "FIRComponent.h"
#import "FIRComponentContainer.h"
#import "FIRComponentType.h"
#import "FIRDependency.h"
#import "FIRHeartbeatLogger.h"
#import "FIRLibrary.h"
#import "FIRLogger.h"
#import "FIROptionsInternal.h"

View File

@@ -0,0 +1,19 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// An umbrella header, for any other libraries in this repo to access Firebase
// Installations Public headers. Any package manager complexity should be
// handled here.
#import <FirebaseInstallations/FirebaseInstallations.h>

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/** Connector for bridging communication between Firebase SDKs and FIRMessaging API. */
NS_SWIFT_NAME(MessagingInterop) @protocol FIRMessagingInterop
/**
* The FCM registration token is used to identify this device so that FCM can send notifications to
* it. It is associated with your APNs token when the APNs token is supplied, so messages sent to
* the FCM token will be delivered over APNs.
*
* The FCM registration token is sometimes refreshed automatically. In your FIRMessaging delegate,
* the delegate method `messaging:didReceiveRegistrationToken:` will be called once a token is
* available, or has been refreshed. Typically it should be called once per app start, but
* may be called more often if the token is invalidated or updated.
*
* Once you have an FCM registration token, you should send it to your application server, so it can
* use the FCM token to send notifications to your device.
*/
@property(nonatomic, readonly, nullable) NSString *FCMToken NS_SWIFT_NAME(fcmToken);
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,1076 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if !__has_feature(objc_arc)
#error FIRMessagingLib should be compiled with ARC.
#endif
#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h"
#import <GoogleUtilities/GULAppDelegateSwizzler.h>
#import <GoogleUtilities/GULAppEnvironmentUtil.h>
#import <GoogleUtilities/GULReachabilityChecker.h>
#import <GoogleUtilities/GULUserDefaults.h>
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
#import "FirebaseMessaging/Interop/FIRMessagingInterop.h"
#import "FirebaseMessaging/Sources/FIRMessagingAnalytics.h"
#import "FirebaseMessaging/Sources/FIRMessagingCode.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingContextManagerService.h"
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingPubSub.h"
#import "FirebaseMessaging/Sources/FIRMessagingRemoteNotificationsProxy.h"
#import "FirebaseMessaging/Sources/FIRMessagingRmqManager.h"
#import "FirebaseMessaging/Sources/FIRMessagingSyncMessageManager.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import "FirebaseMessaging/Sources/FIRMessaging_Private.h"
#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessagingExtensionHelper.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingAuthService.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenInfo.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenManager.h"
#import "Interop/Analytics/Public/FIRAnalyticsInterop.h"
static NSString *const kFIRMessagingMessageViaAPNSRootKey = @"aps";
static NSString *const kFIRMessagingReachabilityHostname = @"www.google.com";
#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
const NSNotificationName FIRMessagingRegistrationTokenRefreshedNotification =
@"com.firebase.messaging.notif.fcm-token-refreshed";
#else
NSString *const FIRMessagingRegistrationTokenRefreshedNotification =
@"com.firebase.messaging.notif.fcm-token-refreshed";
#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
NSString *const kFIRMessagingUserDefaultsKeyAutoInitEnabled =
@"com.firebase.messaging.auto-init.enabled"; // Auto Init Enabled key stored in NSUserDefaults
NSString *const kFIRMessagingPlistAutoInitEnabled =
@"FirebaseMessagingAutoInitEnabled"; // Auto Init Enabled key stored in Info.plist
NSString *const FIRMessagingErrorDomain = @"com.google.fcm";
const BOOL FIRMessagingIsAPNSSyncMessage(NSDictionary *message) {
if ([message[kFIRMessagingMessageViaAPNSRootKey] isKindOfClass:[NSDictionary class]]) {
NSDictionary *aps = message[kFIRMessagingMessageViaAPNSRootKey];
if (aps && [aps isKindOfClass:[NSDictionary class]]) {
return [aps[kFIRMessagingMessageAPNSContentAvailableKey] boolValue];
}
}
return NO;
}
BOOL FIRMessagingIsContextManagerMessage(NSDictionary *message) {
return [FIRMessagingContextManagerService isContextManagerMessage:message];
}
@interface FIRMessagingMessageInfo ()
@property(nonatomic, readwrite, assign) FIRMessagingMessageStatus status;
@end
@implementation FIRMessagingMessageInfo
- (instancetype)init {
FIRMessagingInvalidateInitializer();
}
- (instancetype)initWithStatus:(FIRMessagingMessageStatus)status {
self = [super init];
if (self) {
_status = status;
}
return self;
}
@end
@interface FIRMessaging () <GULReachabilityDelegate>
// FIRApp properties
@property(nonatomic, readwrite, strong) NSData *apnsTokenData;
@property(nonatomic, readwrite, strong) FIRMessagingClient *client;
@property(nonatomic, readwrite, strong) GULReachabilityChecker *reachability;
@property(nonatomic, readwrite, strong) FIRMessagingPubSub *pubsub;
@property(nonatomic, readwrite, strong) FIRMessagingRmqManager *rmq2Manager;
@property(nonatomic, readwrite, strong) FIRMessagingSyncMessageManager *syncMessageManager;
@property(nonatomic, readwrite, strong) GULUserDefaults *messagingUserDefaults;
@property(nonatomic, readwrite, strong) FIRInstallations *installations;
@property(nonatomic, readwrite, strong) FIRMessagingTokenManager *tokenManager;
@property(nonatomic, readwrite, strong) FIRHeartbeatLogger *heartbeatLogger;
/// Message ID's logged for analytics. This prevents us from logging the same message twice
/// which can happen if the user inadvertently calls `appDidReceiveMessage` along with us
/// calling it implicitly during swizzling.
@property(nonatomic, readwrite, strong) NSMutableSet *loggedMessageIDs;
@property(nonatomic, readwrite, strong) id<FIRAnalyticsInterop> _Nullable analytics;
@end
@interface FIRMessaging () <FIRMessagingInterop, FIRLibrary>
@end
@implementation FIRMessaging
+ (FIRMessaging *)messaging {
FIRApp *defaultApp = [FIRApp defaultApp]; // Missing configure will be logged here.
id<FIRMessagingInterop> instance = FIR_COMPONENT(FIRMessagingInterop, defaultApp.container);
// We know the instance coming from the container is a FIRMessaging instance, cast it and move on.
return (FIRMessaging *)instance;
}
+ (FIRMessagingExtensionHelper *)extensionHelper {
static dispatch_once_t once;
static FIRMessagingExtensionHelper *extensionHelper;
dispatch_once(&once, ^{
extensionHelper = [[FIRMessagingExtensionHelper alloc] init];
});
return extensionHelper;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
- (instancetype)initWithAnalytics:(nullable id<FIRAnalyticsInterop>)analytics
userDefaults:(GULUserDefaults *)defaults
heartbeatLogger:(FIRHeartbeatLogger *)heartbeatLogger {
#pragma clang diagnostic pop
self = [super init];
if (self != nil) {
_loggedMessageIDs = [NSMutableSet set];
_messagingUserDefaults = defaults;
_analytics = analytics;
_heartbeatLogger = heartbeatLogger;
}
return self;
}
- (void)dealloc {
[self.reachability stop];
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self teardown];
}
#pragma mark - Config
+ (void)load {
[FIRApp registerInternalLibrary:(Class<FIRLibrary>)self withName:@"fire-fcm"];
}
+ (nonnull NSArray<FIRComponent *> *)componentsToRegister {
FIRDependency *analyticsDep = [FIRDependency dependencyWithProtocol:@protocol(FIRAnalyticsInterop)
isRequired:NO];
FIRComponentCreationBlock creationBlock =
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
if (!container.app.isDefaultApp) {
// Only start for the default FIRApp.
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeFIRApp001,
@"Firebase Messaging only works with the default app.");
return nil;
}
// Ensure it's cached so it returns the same instance every time messaging is called.
*isCacheable = YES;
id<FIRAnalyticsInterop> analytics = FIR_COMPONENT(FIRAnalyticsInterop, container);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
FIRMessaging *messaging =
[[FIRMessaging alloc] initWithAnalytics:analytics
userDefaults:[GULUserDefaults standardUserDefaults]
heartbeatLogger:container.app.heartbeatLogger];
#pragma clang diagnostic pop
[messaging start];
[messaging configureMessagingWithOptions:container.app.options];
[messaging configureNotificationSwizzlingIfEnabled];
return messaging;
};
FIRComponent *messagingProvider =
[FIRComponent componentWithProtocol:@protocol(FIRMessagingInterop)
instantiationTiming:FIRInstantiationTimingEagerInDefaultApp
dependencies:@[ analyticsDep ]
creationBlock:creationBlock];
return @[ messagingProvider ];
}
- (void)configureMessagingWithOptions:(FIROptions *)options {
NSString *GCMSenderID = options.GCMSenderID;
if (!GCMSenderID.length) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeFIRApp000,
@"Firebase not set up correctly, nil or empty senderID.");
[NSException raise:FIRMessagingErrorDomain
format:@"Could not configure Firebase Messaging. GCMSenderID must not be nil or "
@"empty."];
}
self.tokenManager.fcmSenderID = GCMSenderID;
self.tokenManager.firebaseAppID = options.googleAppID;
// FCM generates a FCM token during app start for sending push notification to device.
// This is not needed for app extension except for watch.
#if TARGET_OS_WATCH
[self didCompleteConfigure];
#else
if (![GULAppEnvironmentUtil isAppExtension]) {
[self didCompleteConfigure];
}
#endif
}
- (void)didCompleteConfigure {
NSString *cachedToken =
[self.tokenManager cachedTokenInfoWithAuthorizedEntity:self.tokenManager.fcmSenderID
scope:kFIRMessagingDefaultTokenScope]
.token;
// When there is a cached token, do the token refresh.
// Before fetching FCM token, confirm there is an APNS token to avoid validation error
// This error is innocuous since we refetch after APNS token is set but it can seem alarming
if (cachedToken) {
// Clean up expired tokens by checking the token refresh policy.
[self.installations installationIDWithCompletion:^(NSString *_Nullable identifier,
NSError *_Nullable error) {
if ([self.tokenManager checkTokenRefreshPolicyWithIID:identifier] && self.APNSToken) {
// Default token is expired, fetch default token from server.
[self retrieveFCMTokenForSenderID:self.tokenManager.fcmSenderID
completion:^(NSString *_Nullable FCMToken, NSError *_Nullable error){
}];
}
// Set the default FCM token, there's an issue that FIRApp configure
// happens before developers able to set the delegate
// Hence first token set must be happen here after listener is set
// TODO(chliangGoogle) Need to investigate better solution.
[self updateDefaultFCMToken:self.FCMToken];
}];
} else if (self.isAutoInitEnabled && self.APNSToken) {
// When there is no cached token, must check auto init is enabled.
// If it's disabled, don't initiate token generation/refresh.
// If no cache token and auto init is enabled, fetch a token from server.
[self retrieveFCMTokenForSenderID:self.tokenManager.fcmSenderID
completion:^(NSString *_Nullable FCMToken, NSError *_Nullable error){
}];
}
}
- (void)configureNotificationSwizzlingIfEnabled {
// Swizzle remote-notification-related methods (app delegate and UNUserNotificationCenter)
if ([FIRMessagingRemoteNotificationsProxy canSwizzleMethods]) {
NSString *docsURLString = @"https://firebase.google.com/docs/cloud-messaging/ios/client"
@"#method_swizzling_in_firebase_messaging";
FIRMessagingLoggerNotice(kFIRMessagingMessageCodeFIRApp000,
@"FIRMessaging Remote Notifications proxy enabled, will swizzle "
@"remote notification receiver handlers. If you'd prefer to manually "
@"integrate Firebase Messaging, add \"%@\" to your Info.plist, "
@"and set it to NO. Follow the instructions at:\n%@\nto ensure "
@"proper integration.",
kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey,
docsURLString);
[[FIRMessagingRemoteNotificationsProxy sharedProxy] swizzleMethodsIfPossible];
}
}
- (void)start {
[self setupFileManagerSubDirectory];
[self setupNotificationListeners];
self.tokenManager =
[[FIRMessagingTokenManager alloc] initWithHeartbeatLogger:self.heartbeatLogger];
self.installations = [FIRInstallations installations];
[self setupTopics];
// Print the library version for logging.
NSString *currentLibraryVersion = FIRFirebaseVersion();
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeMessagingPrintLibraryVersion,
@"FIRMessaging library version %@", currentLibraryVersion);
NSString *hostname = kFIRMessagingReachabilityHostname;
self.reachability = [[GULReachabilityChecker alloc] initWithReachabilityDelegate:self
withHost:hostname];
[self.reachability start];
// setup FIRMessaging objects
[self setupRmqManager];
[self setupSyncMessageManager];
}
- (void)setupFileManagerSubDirectory {
if (![[self class] hasSubDirectory:kFIRMessagingSubDirectoryName]) {
[[self class] createSubDirectory:kFIRMessagingSubDirectoryName];
}
if (![[self class] hasSubDirectory:kFIRMessagingInstanceIDSubDirectoryName]) {
[[self class] createSubDirectory:kFIRMessagingInstanceIDSubDirectoryName];
}
}
- (void)setupNotificationListeners {
// To prevent multiple notifications remove self as observer for all events.
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center removeObserver:self];
[center addObserver:self
selector:@selector(defaultFCMTokenWasRefreshed:)
name:kFIRMessagingRegistrationTokenRefreshNotification
object:nil];
}
- (void)setupRmqManager {
self.rmq2Manager = [[FIRMessagingRmqManager alloc] initWithDatabaseName:@"rmq2"];
[self.rmq2Manager loadRmqId];
}
- (void)setupTopics {
self.pubsub = [[FIRMessagingPubSub alloc] initWithTokenManager:self.tokenManager];
}
- (void)setupSyncMessageManager {
self.syncMessageManager =
[[FIRMessagingSyncMessageManager alloc] initWithRmqManager:self.rmq2Manager];
[self.syncMessageManager removeExpiredSyncMessages];
}
- (void)teardown {
self.pubsub = nil;
self.syncMessageManager = nil;
self.rmq2Manager = nil;
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeMessaging001, @"Did successfully teardown");
}
#pragma mark - Messages
- (FIRMessagingMessageInfo *)appDidReceiveMessage:(NSDictionary *)message {
if (!message.count) {
return [[FIRMessagingMessageInfo alloc] initWithStatus:FIRMessagingMessageStatusUnknown];
}
// For downstream messages that go via MCS we should strip out this key before sending
// the message to the device.
BOOL isOldMessage = NO;
NSString *messageID = message[kFIRMessagingMessageIDKey];
if (messageID.length) {
[self.rmq2Manager saveS2dMessageWithRmqId:messageID];
BOOL isSyncMessage = FIRMessagingIsAPNSSyncMessage(message);
if (isSyncMessage) {
isOldMessage = [self.syncMessageManager didReceiveAPNSSyncMessage:message];
}
// Prevent duplicates by keeping a cache of all the logged messages during each session.
// The duplicates only happen when the 3P app calls `appDidReceiveMessage:` along with
// us swizzling their implementation to call the same method implicitly.
// We need to rule out the contextual message because it shares the same message ID
// as the local notification it will schedule. And because it is also a APNSSync message
// its duplication is already checked previously.
if (!isOldMessage && !FIRMessagingIsContextManagerMessage(message)) {
isOldMessage = [self.loggedMessageIDs containsObject:messageID];
if (!isOldMessage) {
[self.loggedMessageIDs addObject:messageID];
}
}
}
if (!isOldMessage) {
[FIRMessagingAnalytics logMessage:message toAnalytics:_analytics];
[self handleContextManagerMessage:message];
[self handleIncomingLinkIfNeededFromMessage:message];
}
return [[FIRMessagingMessageInfo alloc] initWithStatus:FIRMessagingMessageStatusNew];
}
- (BOOL)handleContextManagerMessage:(NSDictionary *)message {
if (FIRMessagingIsContextManagerMessage(message)) {
return [FIRMessagingContextManagerService handleContextManagerMessage:message];
}
return NO;
}
- (void)handleIncomingLinkIfNeededFromMessage:(NSDictionary *)message {
#if TARGET_OS_IOS || TARGET_OS_TV
NSURL *url = [self linkURLFromMessage:message];
if (url == nil) {
return;
}
if (![NSThread isMainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self handleIncomingLinkIfNeededFromMessage:message];
});
return;
}
UIApplication *application = [GULAppDelegateSwizzler sharedApplication];
if (!application) {
return;
}
id<UIApplicationDelegate> appDelegate = application.delegate;
SEL continueUserActivitySelector = @selector(application:
continueUserActivity:restorationHandler:);
SEL openURLWithOptionsSelector = @selector(application:openURL:options:);
SEL openURLWithSourceApplicationSelector = @selector(application:
openURL:sourceApplication:annotation:);
// TODO(Xcode 15): When Xcode 15 is the minimum supported Xcode version, it will be unnecessary to
// check if `TARGET_OS_VISION` is defined.
#if TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION)
SEL handleOpenURLSelector = @selector(application:handleOpenURL:);
#endif // TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION)
// Due to FIRAAppDelegateProxy swizzling, this selector will most likely get chosen, whether or
// not the actual application has implemented
// |application:continueUserActivity:restorationHandler:|. A warning will be displayed to the user
// if they haven't implemented it.
if ([NSUserActivity class] != nil &&
[appDelegate respondsToSelector:continueUserActivitySelector]) {
NSUserActivity *userActivity =
[[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb];
userActivity.webpageURL = url;
[appDelegate application:application
continueUserActivity:userActivity
restorationHandler:^(NSArray *_Nullable restorableObjects){
// Do nothing, as we don't support the app calling this block
}];
} else if ([appDelegate respondsToSelector:openURLWithOptionsSelector]) {
[appDelegate application:application openURL:url options:@{}];
// Similarly, |application:openURL:sourceApplication:annotation:| will also always be called,
// due to the default swizzling done by FIRAAppDelegateProxy in Firebase Analytics
} else if ([appDelegate respondsToSelector:openURLWithSourceApplicationSelector]) {
// TODO(Xcode 15): When Xcode 15 is the minimum supported Xcode version, it will be unnecessary to
// check if `TARGET_OS_VISION` is defined.
#if TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[appDelegate application:application
openURL:url
sourceApplication:FIRMessagingAppIdentifier()
annotation:@{}];
#pragma clang diagnostic pop
} else if ([appDelegate respondsToSelector:handleOpenURLSelector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[appDelegate application:application handleOpenURL:url];
#pragma clang diagnostic pop
#endif // TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION)
}
#endif // TARGET_OS_IOS || TARGET_OS_TV
}
- (NSURL *)linkURLFromMessage:(NSDictionary *)message {
NSString *urlString = message[kFIRMessagingMessageLinkKey];
if (urlString == nil || ![urlString isKindOfClass:[NSString class]] || urlString.length == 0) {
return nil;
}
NSURL *url = [NSURL URLWithString:urlString];
return url;
}
#pragma mark - APNS
- (NSData *)APNSToken {
return self.apnsTokenData;
}
- (void)setAPNSToken:(NSData *)APNSToken {
[self setAPNSToken:APNSToken type:FIRMessagingAPNSTokenTypeUnknown];
}
- (void)setAPNSToken:(NSData *)apnsToken type:(FIRMessagingAPNSTokenType)type {
if ([apnsToken isEqual:self.apnsTokenData]) {
return;
}
self.apnsTokenData = apnsToken;
// Notify InstanceID that APNS Token has been set.
NSDictionary *userInfo = @{kFIRMessagingAPNSTokenType : @(type)};
// TODO(chliang) This is sent to InstanceID in case users are still using the deprecated SDK.
// Should be safe to remove once InstanceID is removed.
NSNotification *notification =
[NSNotification notificationWithName:kFIRMessagingAPNSTokenNotification
object:[apnsToken copy]
userInfo:userInfo];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
[self.tokenManager setAPNSToken:[apnsToken copy] withUserInfo:userInfo];
}
#pragma mark - FCM Token
- (BOOL)isAutoInitEnabled {
// Defer to the class method since we're just reading from regular userDefaults and we need to
// read this from IID without instantiating the Messaging singleton.
return [[self class] isAutoInitEnabledWithUserDefaults:_messagingUserDefaults];
}
/// Checks if Messaging auto-init is enabled in the user defaults instance passed in. This is
/// exposed as a class property for IID to fetch the property without instantiating an instance of
/// Messaging. Since Messaging can only be used with the default FIRApp, we can have one point of
/// entry without context of which FIRApp instance is being used.
/// ** THIS METHOD IS DEPENDED ON INTERNALLY BY IID USING REFLECTION. PLEASE DO NOT CHANGE THE
/// SIGNATURE, AS IT WOULD BREAK AUTOINIT FUNCTIONALITY WITHIN IID. **
+ (BOOL)isAutoInitEnabledWithUserDefaults:(GULUserDefaults *)userDefaults {
// Check storage
id isAutoInitEnabledObject =
[userDefaults objectForKey:kFIRMessagingUserDefaultsKeyAutoInitEnabled];
if (isAutoInitEnabledObject) {
return [isAutoInitEnabledObject boolValue];
}
// Check Info.plist
isAutoInitEnabledObject =
[[NSBundle mainBundle] objectForInfoDictionaryKey:kFIRMessagingPlistAutoInitEnabled];
if (isAutoInitEnabledObject) {
return [isAutoInitEnabledObject boolValue];
}
// If none of above exists, we default to the global switch that comes from FIRApp.
return [[FIRApp defaultApp] isDataCollectionDefaultEnabled];
}
- (void)setAutoInitEnabled:(BOOL)autoInitEnabled {
BOOL isFCMAutoInitEnabled = [self isAutoInitEnabled];
[_messagingUserDefaults setBool:autoInitEnabled
forKey:kFIRMessagingUserDefaultsKeyAutoInitEnabled];
[_messagingUserDefaults synchronize];
if (!isFCMAutoInitEnabled && autoInitEnabled) {
[self.tokenManager tokenAndRequestIfNotExist];
}
}
- (NSString *)FCMToken {
// Gets the current default token, and requets a new one if it doesn't exist.
NSString *token = [self.tokenManager tokenAndRequestIfNotExist];
return token;
}
- (void)tokenWithCompletion:(FIRMessagingFCMTokenFetchCompletion)completion {
FIROptions *options = FIRApp.defaultApp.options;
[self retrieveFCMTokenForSenderID:options.GCMSenderID completion:completion];
}
- (void)deleteTokenWithCompletion:(FIRMessagingDeleteFCMTokenCompletion)completion {
FIROptions *options = FIRApp.defaultApp.options;
[self deleteFCMTokenForSenderID:options.GCMSenderID completion:completion];
}
- (void)retrieveFCMTokenForSenderID:(nonnull NSString *)senderID
completion:(nonnull FIRMessagingFCMTokenFetchCompletion)completion {
if (!senderID.length) {
NSString *description = @"Couldn't fetch token because a Sender ID was not supplied. A valid "
@"Sender ID is required to fetch an FCM token";
FIRMessagingLoggerError(kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenFetch, @"%@",
description);
if (completion) {
NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeMissingAuthorizedEntity
failureReason:description];
completion(nil, error);
}
return;
}
NSDictionary *options = nil;
if (self.APNSToken) {
options = @{kFIRMessagingTokenOptionsAPNSKey : self.APNSToken};
} else {
FIRMessagingLoggerWarn(kFIRMessagingMessageCodeAPNSTokenNotAvailableDuringTokenFetch,
@"APNS device token not set before retrieving FCM Token for Sender ID "
@"'%@'."
@"Be sure to re-retrieve the FCM token once the APNS device token is "
@"set.",
senderID);
}
[self.tokenManager
tokenWithAuthorizedEntity:senderID
scope:kFIRMessagingDefaultTokenScope
options:options
handler:^(NSString *_Nullable FCMToken, NSError *_Nullable error) {
if (completion) {
completion(FCMToken, error);
}
}];
}
- (void)deleteFCMTokenForSenderID:(nonnull NSString *)senderID
completion:(nonnull FIRMessagingDeleteFCMTokenCompletion)completion {
if (!senderID.length) {
NSString *description = @"Couldn't delete token because a Sender ID was not supplied. A "
@"valid Sender ID is required to delete an FCM token";
FIRMessagingLoggerError(kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenDelete, @"%@",
description);
if (completion) {
NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidRequest
failureReason:description];
completion(error);
}
return;
}
FIRMessaging_WEAKIFY(self);
[self.installations
installationIDWithCompletion:^(NSString *_Nullable identifier, NSError *_Nullable error) {
FIRMessaging_STRONGIFY(self);
if (error) {
NSError *newError = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidIdentity
failureReason:@"Failed to get installation ID."];
completion(newError);
} else {
[self.tokenManager deleteTokenWithAuthorizedEntity:senderID
scope:kFIRMessagingDefaultTokenScope
instanceID:identifier
handler:^(NSError *_Nullable error) {
if (completion) {
completion(error);
}
}];
}
}];
}
- (void)deleteDataWithCompletion:(void (^)(NSError *_Nullable))completion {
FIRMessaging_WEAKIFY(self);
[self.tokenManager deleteWithHandler:^(NSError *error) {
FIRMessaging_STRONGIFY(self);
if (error) {
if (completion) {
completion(error);
}
return;
}
// Only request new token if FCM auto initialization is
// enabled.
if ([self isAutoInitEnabled]) {
// Deletion succeeds! Requesting new checkin, IID and token.
[self tokenWithCompletion:^(NSString *_Nullable token, NSError *_Nullable error) {
if (completion) {
completion(error);
}
}];
return;
}
if (completion) {
completion(nil);
}
}];
}
#pragma mark - FIRMessagingDelegate helper methods
- (void)setDelegate:(id<FIRMessagingDelegate>)delegate {
_delegate = delegate;
[self validateDelegateConformsToTokenAvailabilityMethods];
}
// Check if the delegate conforms to |didReceiveRegistrationToken:|
// and display a warning to the developer if not.
// NOTE: Once |didReceiveRegistrationToken:| can be made a required method, this
// check can be removed.
- (void)validateDelegateConformsToTokenAvailabilityMethods {
if (self.delegate && ![self.delegate respondsToSelector:@selector(messaging:
didReceiveRegistrationToken:)]) {
FIRMessagingLoggerWarn(kFIRMessagingMessageCodeTokenDelegateMethodsNotImplemented,
@"The object %@ does not respond to "
@"-messaging:didReceiveRegistrationToken:. Please implement "
@"-messaging:didReceiveRegistrationToken: to be provided with an FCM "
@"token.",
self.delegate.description);
}
}
- (void)notifyRefreshedFCMToken {
__weak FIRMessaging *weakSelf = self;
if (![NSThread isMainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf notifyRefreshedFCMToken];
});
return;
}
if ([self.delegate respondsToSelector:@selector(messaging:didReceiveRegistrationToken:)]) {
[self.delegate messaging:self didReceiveRegistrationToken:self.tokenManager.defaultFCMToken];
}
// Should always trigger the token refresh notification when the delegate method is called
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center postNotificationName:FIRMessagingRegistrationTokenRefreshedNotification
object:self.tokenManager.defaultFCMToken];
}
#pragma mark - Topics
+ (NSString *)normalizeTopic:(NSString *)topic {
if (!topic.length) {
return nil;
}
if (![FIRMessagingPubSub hasTopicsPrefix:topic]) {
topic = [FIRMessagingPubSub addPrefixToTopic:topic];
}
if ([FIRMessagingPubSub isValidTopicWithPrefix:topic]) {
return [topic copy];
}
return nil;
}
- (void)subscribeToTopic:(NSString *)topic {
[self subscribeToTopic:topic completion:nil];
}
- (void)subscribeToTopic:(NSString *)topic
completion:(nullable FIRMessagingTopicOperationCompletion)completion {
if ([FIRMessagingPubSub hasTopicsPrefix:topic]) {
FIRMessagingLoggerWarn(kFIRMessagingMessageCodeTopicFormatIsDeprecated,
@"Format '%@' is deprecated. Only '%@' should be used in "
@"subscribeToTopic.",
topic, [FIRMessagingPubSub removePrefixFromTopic:topic]);
}
__weak FIRMessaging *weakSelf = self;
[self
retrieveFCMTokenForSenderID:self.tokenManager.fcmSenderID
completion:^(NSString *_Nullable FCMToken, NSError *_Nullable error) {
if (error) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging010,
@"The subscription operation failed due to an "
@"error getting the FCM token: %@.",
error);
if (completion) {
completion(error);
}
return;
}
FIRMessaging *strongSelf = weakSelf;
NSString *normalizeTopic = [[strongSelf class] normalizeTopic:topic];
if (normalizeTopic.length) {
[strongSelf.pubsub subscribeToTopic:normalizeTopic handler:completion];
return;
}
NSString *failureReason = [NSString
stringWithFormat:@"Cannot parse topic name: '%@'. Will not subscribe.",
topic];
FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging009, @"%@",
failureReason);
if (completion) {
completion([NSError
messagingErrorWithCode:kFIRMessagingErrorCodeInvalidTopicName
failureReason:failureReason]);
}
}];
}
- (void)unsubscribeFromTopic:(NSString *)topic {
[self unsubscribeFromTopic:topic completion:nil];
}
- (void)unsubscribeFromTopic:(NSString *)topic
completion:(nullable FIRMessagingTopicOperationCompletion)completion {
if ([FIRMessagingPubSub hasTopicsPrefix:topic]) {
FIRMessagingLoggerWarn(kFIRMessagingMessageCodeTopicFormatIsDeprecated,
@"Format '%@' is deprecated. Only '%@' should be used in "
@"unsubscribeFromTopic.",
topic, [FIRMessagingPubSub removePrefixFromTopic:topic]);
}
__weak FIRMessaging *weakSelf = self;
[self retrieveFCMTokenForSenderID:self.tokenManager.fcmSenderID
completion:^(NSString *_Nullable FCMToken, NSError *_Nullable error) {
if (error) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging012,
@"The unsubscription operation failed due to "
@"an error getting the FCM token: %@.",
error);
if (completion) {
completion(error);
}
return;
}
FIRMessaging *strongSelf = weakSelf;
NSString *normalizeTopic = [[strongSelf class] normalizeTopic:topic];
if (normalizeTopic.length) {
[strongSelf.pubsub unsubscribeFromTopic:normalizeTopic
handler:completion];
return;
}
NSString *failureReason = [NSString
stringWithFormat:
@"Cannot parse topic name: '%@'. Will not unsubscribe.", topic];
FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging011, @"%@",
failureReason);
if (completion) {
completion([NSError
messagingErrorWithCode:kFIRMessagingErrorCodeInvalidTopicName
failureReason:failureReason]);
}
}];
}
#pragma mark - GULReachabilityDelegate
- (void)reachability:(GULReachabilityChecker *)reachability
statusChanged:(GULReachabilityStatus)status {
[self onNetworkStatusChanged];
}
#pragma mark - Network
- (void)onNetworkStatusChanged {
if ([self isNetworkAvailable]) {
[self.pubsub scheduleSync:YES];
}
}
- (BOOL)isNetworkAvailable {
GULReachabilityStatus status = self.reachability.reachabilityStatus;
return (status == kGULReachabilityViaCellular || status == kGULReachabilityViaWifi);
}
- (FIRMessagingNetworkStatus)networkType {
GULReachabilityStatus status = self.reachability.reachabilityStatus;
if (![self isNetworkAvailable]) {
return kFIRMessagingReachabilityNotReachable;
} else if (status == kGULReachabilityViaCellular) {
return kFIRMessagingReachabilityReachableViaWWAN;
} else {
return kFIRMessagingReachabilityReachableViaWiFi;
}
}
#pragma mark - Notifications
- (void)defaultFCMTokenWasRefreshed:(NSNotification *)notification {
if (notification.object && ![notification.object isKindOfClass:[NSString class]]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeMessaging015,
@"Invalid default FCM token type %@",
NSStringFromClass([notification.object class]));
return;
}
NSString *newToken = [(NSString *)notification.object copy];
[self updateDefaultFCMToken:newToken];
}
- (void)updateDefaultFCMToken:(NSString *)defaultFCMToken {
NSString *oldToken = self.tokenManager.defaultFCMToken;
NSString *newToken = defaultFCMToken;
if ([self.tokenManager hasTokenChangedFromOldToken:oldToken toNewToken:newToken]) {
// Make sure to set default token first before notifying others.
[self.tokenManager saveDefaultTokenInfoInKeychain:newToken];
[self notifyDelegateOfFCMTokenAvailability];
[self.pubsub scheduleSync:YES];
}
}
- (void)notifyDelegateOfFCMTokenAvailability {
__weak FIRMessaging *weakSelf = self;
if (![NSThread isMainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf notifyDelegateOfFCMTokenAvailability];
});
return;
}
if ([self.delegate respondsToSelector:@selector(messaging:didReceiveRegistrationToken:)]) {
[self.delegate messaging:self didReceiveRegistrationToken:self.tokenManager.defaultFCMToken];
}
// Should always trigger the token refresh notification when the delegate method is called
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center postNotificationName:FIRMessagingRegistrationTokenRefreshedNotification
object:self.tokenManager.defaultFCMToken];
}
#pragma mark - Application Support Directory
+ (BOOL)hasSubDirectory:(NSString *)subDirectoryName {
NSString *subDirectoryPath = [self pathForSubDirectory:subDirectoryName];
BOOL isDirectory;
if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
isDirectory:&isDirectory]) {
return NO;
} else if (!isDirectory) {
return NO;
}
return YES;
}
+ (NSString *)pathForSubDirectory:(NSString *)subDirectoryName {
NSArray *directoryPaths =
NSSearchPathForDirectoriesInDomains(FIRMessagingSupportedDirectory(), NSUserDomainMask, YES);
NSString *dirPath = directoryPaths.lastObject;
NSArray *components = @[ dirPath, subDirectoryName ];
return [NSString pathWithComponents:components];
}
+ (BOOL)createSubDirectory:(NSString *)subDirectoryName {
NSString *subDirectoryPath = [self pathForSubDirectory:subDirectoryName];
BOOL hasSubDirectory;
if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
isDirectory:&hasSubDirectory]) {
NSError *error;
[[NSFileManager defaultManager] createDirectoryAtPath:subDirectoryPath
withIntermediateDirectories:YES
attributes:nil
error:&error];
if (error) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging017,
@"Cannot create directory %@, error: %@", subDirectoryPath, error);
return NO;
}
} else {
if (!hasSubDirectory) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging018,
@"Found file instead of directory at %@", subDirectoryPath);
return NO;
}
}
return YES;
}
#pragma mark - Locales
+ (NSString *)currentLocale {
NSArray *locales = [self firebaseLocales];
NSArray *preferredLocalizations =
[NSBundle preferredLocalizationsFromArray:locales
forPreferences:[NSLocale preferredLanguages]];
NSString *legalDocsLanguage = [preferredLocalizations firstObject];
// Use en as the default language
return legalDocsLanguage ? legalDocsLanguage : @"en";
}
+ (NSArray *)firebaseLocales {
NSMutableArray *locales = [NSMutableArray array];
NSDictionary *localesMap = [self firebaselocalesMap];
for (NSString *key in localesMap) {
[locales addObjectsFromArray:localesMap[key]];
}
return locales;
}
+ (NSDictionary *)firebaselocalesMap {
return @{
// Albanian
@"sq" : @[ @"sq_AL" ],
// Belarusian
@"be" : @[ @"be_BY" ],
// Bulgarian
@"bg" : @[ @"bg_BG" ],
// Catalan
@"ca" : @[ @"ca", @"ca_ES" ],
// Croatian
@"hr" : @[ @"hr", @"hr_HR" ],
// Czech
@"cs" : @[ @"cs", @"cs_CZ" ],
// Danish
@"da" : @[ @"da", @"da_DK" ],
// Estonian
@"et" : @[ @"et_EE" ],
// Finnish
@"fi" : @[ @"fi", @"fi_FI" ],
// Hebrew
@"he" : @[ @"he", @"iw_IL" ],
// Hindi
@"hi" : @[ @"hi_IN" ],
// Hungarian
@"hu" : @[ @"hu", @"hu_HU" ],
// Icelandic
@"is" : @[ @"is_IS" ],
// Indonesian
@"id" : @[ @"id", @"in_ID", @"id_ID" ],
// Irish
@"ga" : @[ @"ga_IE" ],
// Korean
@"ko" : @[ @"ko", @"ko_KR", @"ko-KR" ],
// Latvian
@"lv" : @[ @"lv_LV" ],
// Lithuanian
@"lt" : @[ @"lt_LT" ],
// Macedonian
@"mk" : @[ @"mk_MK" ],
// Malay
@"ms" : @[ @"ms_MY" ],
// Maltese
@"mt" : @[ @"mt_MT" ],
// Polish
@"pl" : @[ @"pl", @"pl_PL", @"pl-PL" ],
// Romanian
@"ro" : @[ @"ro", @"ro_RO" ],
// Russian
@"ru" : @[ @"ru_RU", @"ru", @"ru_BY", @"ru_KZ", @"ru-RU" ],
// Slovak
@"sk" : @[ @"sk", @"sk_SK" ],
// Slovenian
@"sl" : @[ @"sl_SI" ],
// Swedish
@"sv" : @[ @"sv", @"sv_SE", @"sv-SE" ],
// Turkish
@"tr" : @[ @"tr", @"tr-TR", @"tr_TR" ],
// Ukrainian
@"uk" : @[ @"uk", @"uk_UA" ],
// Vietnamese
@"vi" : @[ @"vi", @"vi_VN" ],
// The following are groups of locales or locales that sub-divide a
// language).
// Arabic
@"ar" : @[
@"ar", @"ar_DZ", @"ar_BH", @"ar_EG", @"ar_IQ", @"ar_JO", @"ar_KW",
@"ar_LB", @"ar_LY", @"ar_MA", @"ar_OM", @"ar_QA", @"ar_SA", @"ar_SD",
@"ar_SY", @"ar_TN", @"ar_AE", @"ar_YE", @"ar_GB", @"ar-IQ", @"ar_US"
],
// Simplified Chinese
@"zh_Hans" : @[ @"zh_CN", @"zh_SG", @"zh-Hans" ],
// Traditional Chinese
@"zh_Hant" : @[ @"zh_HK", @"zh_TW", @"zh-Hant", @"zh-HK", @"zh-TW" ],
// Dutch
@"nl" : @[ @"nl", @"nl_BE", @"nl_NL", @"nl-NL" ],
// English
@"en" : @[
@"en", @"en_AU", @"en_CA", @"en_IN", @"en_IE", @"en_MT", @"en_NZ", @"en_PH",
@"en_SG", @"en_ZA", @"en_GB", @"en_US", @"en_AE", @"en-AE", @"en_AS", @"en-AU",
@"en_BD", @"en-CA", @"en_EG", @"en_ES", @"en_GB", @"en-GB", @"en_HK", @"en_ID",
@"en-IN", @"en_NG", @"en-PH", @"en_PK", @"en-SG", @"en-US"
],
// French
@"fr" :
@[ @"fr", @"fr_BE", @"fr_CA", @"fr_FR", @"fr_LU", @"fr_CH", @"fr-CA", @"fr-FR", @"fr_MA" ],
// German
@"de" : @[ @"de", @"de_AT", @"de_DE", @"de_LU", @"de_CH", @"de-DE" ],
// Greek
@"el" : @[ @"el", @"el_CY", @"el_GR" ],
// Italian
@"it" : @[ @"it", @"it_IT", @"it_CH", @"it-IT" ],
// Japanese
@"ja" : @[ @"ja", @"ja_JP", @"ja_JP_JP", @"ja-JP" ],
// Norwegian
@"no" : @[ @"nb", @"no_NO", @"no_NO_NY", @"nb_NO" ],
// Brazilian Portuguese
@"pt_BR" : @[ @"pt_BR", @"pt-BR" ],
// European Portuguese
@"pt_PT" : @[ @"pt", @"pt_PT", @"pt-PT" ],
// Serbian
@"sr" : @[ @"sr_BA", @"sr_ME", @"sr_RS", @"sr_Latn_BA", @"sr_Latn_ME", @"sr_Latn_RS" ],
// European Spanish
@"es_ES" : @[ @"es", @"es_ES", @"es-ES" ],
// Mexican Spanish
@"es_MX" : @[ @"es-MX", @"es_MX", @"es_US", @"es-US" ],
// Latin American Spanish
@"es_419" : @[
@"es_AR", @"es_BO", @"es_CL", @"es_CO", @"es_CR", @"es_DO", @"es_EC",
@"es_SV", @"es_GT", @"es_HN", @"es_NI", @"es_PA", @"es_PY", @"es_PE",
@"es_PR", @"es_UY", @"es_VE", @"es-AR", @"es-CL", @"es-CO"
],
// Thai
@"th" : @[ @"th", @"th_TH", @"th_TH_TH" ],
};
}
#pragma mark - Utilities used by InstanceID
+ (NSString *)FIRMessagingSDKVersion {
return FIRFirebaseVersion();
}
+ (NSString *)FIRMessagingSDKCurrentLocale {
return [self currentLocale];
}
@end

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2018 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "Interop/Analytics/Public/FIRAnalyticsInterop.h"
NS_ASSUME_NONNULL_BEGIN
/**
* Provides integration between FIRMessaging and Analytics.
*
* All Analytics dependencies should be kept in this class, and missing dependencies should be
* handled gracefully.
*
*/
@interface FIRMessagingAnalytics : NSObject
/**
* Determine whether a notification has the properties to be loggable to Analytics.
* If so, send the notification.
* @param notification The notification payload from APNs
* @param analytics The class to be used as the receiver of the logging method
*/
+ (void)logMessage:(NSDictionary *)notification
toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,237 @@
/*
* Copyright 2018 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/FIRMessagingAnalytics.h"
#import <GoogleUtilities/GULAppDelegateSwizzler.h>
#import <GoogleUtilities/GULAppEnvironmentUtil.h>
#import "Interop/Analytics/Public/FIRInteropEventNames.h"
#import "Interop/Analytics/Public/FIRInteropParameterNames.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
static NSString *const kLogTag = @"FIRMessagingAnalytics";
// aps Key
static NSString *const kApsKey = @"aps";
static NSString *const kApsAlertKey = @"alert";
static NSString *const kApsSoundKey = @"sound";
static NSString *const kApsBadgeKey = @"badge";
static NSString *const kApsContentAvailableKey = @"badge";
// Data Key
static NSString *const kDataKey = @"data";
static NSString *const kFIRParameterLabel = @"label";
static NSString *const kReengagementSource = @"Firebase";
static NSString *const kReengagementMedium = @"notification";
// Analytics
static NSString *const kAnalyticsEnabled = @"google.c.a.e";
static NSString *const kAnalyticsMessageTimestamp = @"google.c.a.ts";
static NSString *const kAnalyticsMessageUseDeviceTime = @"google.c.a.udt";
static NSString *const kAnalyticsTrackConversions = @"google.c.a.tc";
@implementation FIRMessagingAnalytics
+ (BOOL)canLogNotification:(NSDictionary *)notification {
if (!notification.count) {
// Payload is empty
return NO;
}
NSString *isAnalyticsLoggingEnabled = notification[kAnalyticsEnabled];
if (![isAnalyticsLoggingEnabled isKindOfClass:[NSString class]] ||
![isAnalyticsLoggingEnabled isEqualToString:@"1"]) {
// Analytics logging is not enabled
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAnalytics001,
@"Analytics logging is disabled. Do not log event.");
return NO;
}
return YES;
}
+ (void)logOpenNotification:(NSDictionary *)notification
toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
[self logUserPropertyForConversionTracking:notification toAnalytics:analytics];
[self logEvent:kFIRIEventNotificationOpen withNotification:notification toAnalytics:analytics];
}
+ (void)logForegroundNotification:(NSDictionary *)notification
toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
[self logEvent:kFIRIEventNotificationForeground
withNotification:notification
toAnalytics:analytics];
}
+ (void)logEvent:(NSString *)event
withNotification:(NSDictionary *)notification
toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
if (!event.length) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAnalyticsInvalidEvent,
@"Can't log analytics with empty event.");
return;
}
NSMutableDictionary *params = [self paramsForEvent:event withNotification:notification];
[analytics logEventWithOrigin:@"fcm" name:event parameters:params];
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAnalytics005, @"%@: Sending event: %@ params: %@",
kLogTag, event, params);
}
+ (NSMutableDictionary *)paramsForEvent:(NSString *)event
withNotification:(NSDictionary *)notification {
NSDictionary *analyticsDataMap = notification;
if (!analyticsDataMap.count) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAnalytics000,
@"No data found in notification. Will not log any analytics events.");
return nil;
}
if (![self canLogNotification:analyticsDataMap]) {
return nil;
}
NSMutableDictionary *params = [NSMutableDictionary dictionary];
NSString *composerIdentifier = analyticsDataMap[kFIRMessagingAnalyticsComposerIdentifier];
if ([composerIdentifier isKindOfClass:[NSString class]] && composerIdentifier.length) {
params[kFIRIParameterMessageIdentifier] = [composerIdentifier copy];
}
NSString *composerLabel = analyticsDataMap[kFIRMessagingAnalyticsComposerLabel];
if ([composerLabel isKindOfClass:[NSString class]] && composerLabel.length) {
params[kFIRIParameterMessageName] = [composerLabel copy];
}
NSString *messageLabel = analyticsDataMap[kFIRMessagingAnalyticsMessageLabel];
if ([messageLabel isKindOfClass:[NSString class]] && messageLabel.length) {
params[kFIRParameterLabel] = [messageLabel copy];
}
NSString *from = analyticsDataMap[kFIRMessagingFromKey];
if ([from isKindOfClass:[NSString class]] && [from containsString:@"/topics/"]) {
params[kFIRIParameterTopic] = [from copy];
}
id timestamp = analyticsDataMap[kAnalyticsMessageTimestamp];
if ([timestamp respondsToSelector:@selector(longLongValue)]) {
int64_t timestampValue = [timestamp longLongValue];
if (timestampValue != 0) {
params[kFIRIParameterMessageTime] = @(timestampValue);
}
}
if (analyticsDataMap[kAnalyticsMessageUseDeviceTime]) {
params[kFIRIParameterMessageDeviceTime] = analyticsDataMap[kAnalyticsMessageUseDeviceTime];
}
return params;
}
+ (void)logUserPropertyForConversionTracking:(NSDictionary *)notification
toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
NSInteger shouldTrackConversions = [notification[kAnalyticsTrackConversions] integerValue];
if (shouldTrackConversions != 1) {
return;
}
NSString *composerIdentifier = notification[kFIRMessagingAnalyticsComposerIdentifier];
if ([composerIdentifier isKindOfClass:[NSString class]] && composerIdentifier.length) {
// Set user property for event.
[analytics setUserPropertyWithOrigin:@"fcm"
name:kFIRIUserPropertyLastNotification
value:composerIdentifier];
// Set the re-engagement attribution properties.
NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:3];
params[kFIRIParameterSource] = kReengagementSource;
params[kFIRIParameterMedium] = kReengagementMedium;
params[kFIRIParameterCampaign] = composerIdentifier;
[analytics logEventWithOrigin:@"fcm" name:kFIRIEventFirebaseCampaign parameters:params];
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAnalytics003,
@"%@: Sending event: %@ params: %@", kLogTag,
kFIRIEventFirebaseCampaign, params);
} else {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAnalytics004,
@"%@: Failed to set user property: %@ value: %@", kLogTag,
kFIRIUserPropertyLastNotification, composerIdentifier);
}
}
+ (void)logMessage:(NSDictionary *)notification
toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
// iOS only because Analytics doesn't support other platforms.
#if TARGET_OS_IOS
if (![self canLogNotification:notification]) {
return;
}
UIApplication *application = [GULAppDelegateSwizzler sharedApplication];
if (!application) {
return;
}
UIApplicationState applicationState = application.applicationState;
switch (applicationState) {
case UIApplicationStateInactive:
// App was in background and in transition to open when user tapped
// on a display notification.
// Needs to check notification is displayed.
if ([[self class] isDisplayNotification:notification]) {
[self logOpenNotification:notification toAnalytics:analytics];
}
break;
case UIApplicationStateActive:
// App was in foreground when it received the notification.
[self logForegroundNotification:notification toAnalytics:analytics];
break;
default:
// App was either in background state or in transition from closed
// to open.
// Needs to check notification is displayed.
if ([[self class] isDisplayNotification:notification]) {
[self logOpenNotification:notification toAnalytics:analytics];
}
break;
}
#endif
}
+ (BOOL)isDisplayNotification:(NSDictionary *)notification {
NSDictionary *aps = notification[kApsKey];
if (!aps || ![aps isKindOfClass:[NSDictionary class]]) {
return NO;
}
NSDictionary *alert = aps[kApsAlertKey];
if (!alert) {
return NO;
}
if ([alert isKindOfClass:[NSDictionary class]]) {
return alert.allKeys.count > 0;
}
// alert can be string sometimes (if only body is specified)
if ([alert isKindOfClass:[NSString class]]) {
return YES;
}
return NO;
}
@end

View File

@@ -0,0 +1,261 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSInteger, FIRMessagingMessageCode) {
// FIRMessaging+FIRApp.m
kFIRMessagingMessageCodeFIRApp000 = 1000, // I-FCM001000
kFIRMessagingMessageCodeFIRApp001 = 1001, // I-FCM001001
// FIRMessaging.m
kFIRMessagingMessageCodeMessagingPrintLibraryVersion = 2000, // I-FCM002000
kFIRMessagingMessageCodeMessaging001 = 2001, // I-FCM002001
kFIRMessagingMessageCodeMessaging002 = 2002, // I-FCM002002 - no longer used
kFIRMessagingMessageCodeMessaging003 = 2003, // I-FCM002003
kFIRMessagingMessageCodeMessaging004 = 2004, // I-FCM002004
kFIRMessagingMessageCodeMessaging005 = 2005, // I-FCM002005
kFIRMessagingMessageCodeMessaging006 = 2006, // I-FCM002006 - no longer used
kFIRMessagingMessageCodeMessaging007 = 2007, // I-FCM002007 - no longer used
kFIRMessagingMessageCodeMessaging008 = 2008, // I-FCM002008 - no longer used
kFIRMessagingMessageCodeMessaging009 = 2009, // I-FCM002009
kFIRMessagingMessageCodeMessaging010 = 2010, // I-FCM002010
kFIRMessagingMessageCodeMessaging011 = 2011, // I-FCM002011
kFIRMessagingMessageCodeMessaging012 = 2012, // I-FCM002012
kFIRMessagingMessageCodeMessaging013 = 2013, // I-FCM002013
kFIRMessagingMessageCodeMessaging014 = 2014, // I-FCM002014
kFIRMessagingMessageCodeMessaging015 = 2015,
kFIRMessagingMessageCodeMessaging016 = 2016, // I-FCM002016 - no longer used
kFIRMessagingMessageCodeMessaging017 = 2017, // I-FCM002017
kFIRMessagingMessageCodeMessaging018 = 2018, // I-FCM002018
kFIRMessagingMessageCodeRemoteMessageDelegateMethodNotImplemented = 2019, // I-FCM002019
kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenFetch = 2020, // I-FCM002020
kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenDelete = 2021, // I-FCM002021
kFIRMessagingMessageCodeAPNSTokenNotAvailableDuringTokenFetch = 2022, // I-FCM002022
kFIRMessagingMessageCodeTokenDelegateMethodsNotImplemented = 2023, // I-FCM002023
kFIRMessagingMessageCodeTopicFormatIsDeprecated = 2024,
kFIRMessagingMessageCodeDirectChannelConnectionFailed = 2025,
kFIRMessagingMessageCodeInvalidClient = 2026, // no longer used
// DO NOT USE 4000, 4004 - 4013
kFIRMessagingMessageCodeClient001 = 4001, // I-FCM004000
kFIRMessagingMessageCodeClient002 = 4002, // I-FCM004001
kFIRMessagingMessageCodeClient003 = 4003, // I-FCM004002
// DO NOT USE 5000 - 5023
// FIRMessagingContextManagerService.m
kFIRMessagingMessageCodeContextManagerService000 = 6000, // I-FCM006000
kFIRMessagingMessageCodeContextManagerService001 = 6001, // I-FCM006001
kFIRMessagingMessageCodeContextManagerService002 = 6002, // I-FCM006002
kFIRMessagingMessageCodeContextManagerService003 = 6003, // I-FCM006003
kFIRMessagingMessageCodeContextManagerService004 = 6004, // I-FCM006004
kFIRMessagingMessageCodeContextManagerService005 = 6005, // I-FCM006005
kFIRMessagingMessageCodeContextManagerServiceFailedLocalSchedule = 6006, // I-FCM006006
// DO NOT USE 7000 - 7013
// FIRMessagingPendingTopicsList.m
kFIRMessagingMessageCodePendingTopicsList000 = 8000, // I-FCM008000
// FIRMessagingPubSub.m
kFIRMessagingMessageCodePubSub000 = 9000, // I-FCM009000
kFIRMessagingMessageCodePubSub001 = 9001, // I-FCM009001
kFIRMessagingMessageCodePubSub002 = 9002, // I-FCM009002
kFIRMessagingMessageCodePubSub003 = 9003, // I-FCM009003
kFIRMessagingMessageCodePubSubArchiveError = 9004,
kFIRMessagingMessageCodePubSubUnarchiveError = 9005,
// FIRMessagingReceiver.m
kFIRMessagingMessageCodeReceiver000 = 10000, // I-FCM010000
kFIRMessagingMessageCodeReceiver001 = 10001, // I-FCM010001
kFIRMessagingMessageCodeReceiver002 = 10002, // I-FCM010002
kFIRMessagingMessageCodeReceiver003 = 10003, // I-FCM010003
kFIRMessagingMessageCodeReceiver004 = 10004, // I-FCM010004 - no longer used
kFIRMessagingMessageCodeReceiver005 = 10005, // I-FCM010005
// FIRMessagingRegistrar.m
kFIRMessagingMessageCodeRegistrar000 = 11000, // I-FCM011000
// FIRMessagingRemoteNotificationsProxy.m
kFIRMessagingMessageCodeRemoteNotificationsProxy000 = 12000, // I-FCM012000
kFIRMessagingMessageCodeRemoteNotificationsProxy001 = 12001, // I-FCM012001
kFIRMessagingMessageCodeRemoteNotificationsProxyAPNSFailed = 12002, // I-FCM012002
kFIRMessagingMessageCodeRemoteNotificationsProxyMethodNotAdded = 12003, // I-FCM012003
// FIRMessagingRmq2PersistentStore.m
// DO NOT USE 13000, 13001, 13009
kFIRMessagingMessageCodeRmq2PersistentStore002 = 13002, // I-FCM013002
kFIRMessagingMessageCodeRmq2PersistentStore003 = 13003, // I-FCM013003
kFIRMessagingMessageCodeRmq2PersistentStore004 = 13004, // I-FCM013004
kFIRMessagingMessageCodeRmq2PersistentStore005 = 13005, // I-FCM013005
kFIRMessagingMessageCodeRmq2PersistentStore006 = 13006, // I-FCM013006
kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingDatabase = 13007, // I-FCM013007
kFIRMessagingMessageCodeRmq2PersistentStoreErrorOpeningDatabase = 13008, // I-FCM013008
kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingTable = 13010, // I-FCM013010
// FIRMessagingRmqManager.m
kFIRMessagingMessageCodeRmqManager000 = 14000, // I-FCM014000
// FIRMessagingSyncMessageManager.m
// DO NOT USE 16000, 16003
kFIRMessagingMessageCodeSyncMessageManager001 = 16001, // I-FCM016001
kFIRMessagingMessageCodeSyncMessageManager002 = 16002, // I-FCM016002
kFIRMessagingMessageCodeSyncMessageManager004 = 16004, // I-FCM016004
kFIRMessagingMessageCodeSyncMessageManager005 = 16005, // I-FCM016005
kFIRMessagingMessageCodeSyncMessageManager006 = 16006, // I-FCM016006
kFIRMessagingMessageCodeSyncMessageManager007 = 16007, // I-FCM016007
kFIRMessagingMessageCodeSyncMessageManager008 = 16008, // I-FCM016008
// FIRMessagingTopicOperation.m
kFIRMessagingMessageCodeTopicOption000 = 17000, // I-FCM017000
kFIRMessagingMessageCodeTopicOption001 = 17001, // I-FCM017001
kFIRMessagingMessageCodeTopicOption002 = 17002, // I-FCM017002
kFIRMessagingMessageCodeTopicOptionTopicEncodingFailed = 17003, // I-FCM017003
kFIRMessagingMessageCodeTopicOperationEmptyResponse = 17004, // I-FCM017004
// FIRMessagingUtilities.m
kFIRMessagingMessageCodeUtilities000 = 18000, // I-FCM018000
kFIRMessagingMessageCodeUtilities001 = 18001, // I-FCM018001
kFIRMessagingMessageCodeUtilities002 = 18002, // I-FCM018002
// FIRMessagingAnalytics.m
kFIRMessagingMessageCodeAnalytics000 = 19000, // I-FCM019000
kFIRMessagingMessageCodeAnalytics001 = 19001, // I-FCM019001
kFIRMessagingMessageCodeAnalytics002 = 19002, // I-FCM019002
kFIRMessagingMessageCodeAnalytics003 = 19003, // I-FCM019003
kFIRMessagingMessageCodeAnalytics004 = 19004, // I-FCM019004
kFIRMessagingMessageCodeAnalytics005 = 19005, // I-FCM019005
kFIRMessagingMessageCodeAnalyticsInvalidEvent = 19006, // I-FCM019006
kFIRMessagingMessageCodeAnalytics007 = 19007, // I-FCM019007
kFIRMessagingMessageCodeAnalyticsCouldNotInvokeAnalyticsLog = 19008, // I-FCM019008
// FIRMessagingExtensionHelper.m
kFIRMessagingServiceExtensionImageInvalidURL = 20000,
kFIRMessagingServiceExtensionImageNotDownloaded = 20001,
kFIRMessagingServiceExtensionLocalFileNotCreated = 20002,
kFIRMessagingServiceExtensionImageNotAttached = 20003,
kFIRMessagingServiceExtensionTransportBytesError = 20004,
kFIRMessagingServiceExtensionInvalidProjectID = 2005,
kFIRMessagingServiceExtensionInvalidMessageID = 2006,
kFIRMessagingServiceExtensionInvalidInstanceID = 2007,
kFIRMessagingMessageCodeFIRApp002 = 22002,
kFIRMessagingMessageCodeInternal001 = 22001,
kFIRMessagingMessageCodeInternal002 = 22002,
// FIRMessaging.m
// DO NOT USE 4000.
kFIRMessagingMessageCodeInstanceID000 = 23000,
kFIRMessagingMessageCodeInstanceID001 = 23001,
kFIRMessagingMessageCodeInstanceID002 = 23002,
kFIRMessagingMessageCodeInstanceID003 = 23003,
kFIRMessagingMessageCodeInstanceID004 = 23004,
kFIRMessagingMessageCodeInstanceID005 = 23005,
kFIRMessagingMessageCodeInstanceID006 = 23006,
kFIRMessagingMessageCodeInstanceID007 = 23007,
kFIRMessagingMessageCodeInstanceID008 = 23008,
kFIRMessagingMessageCodeInstanceID009 = 23009,
kFIRMessagingMessageCodeInstanceID010 = 23010,
kFIRMessagingMessageCodeInstanceID011 = 23011,
kFIRMessagingMessageCodeInstanceID012 = 23012,
kFIRMessagingMessageCodeInstanceID013 = 23013,
kFIRMessagingMessageCodeInstanceID014 = 23014,
kFIRMessagingMessageCodeInstanceID015 = 23015,
kFIRMessagingMessageCodeRefetchingTokenForAPNS = 23016,
kFIRMessagingMessageCodeInstanceID017 = 23017,
kFIRMessagingMessageCodeInstanceID018 = 23018,
// FIRMessagingAuthService.m
kFIRMessagingMessageCodeAuthService000 = 25000,
kFIRMessagingMessageCodeAuthService001 = 25001,
kFIRMessagingMessageCodeAuthService002 = 25002,
kFIRMessagingMessageCodeAuthService003 = 25003,
kFIRMessagingMessageCodeAuthService004 = 25004,
kFIRMessagingMessageCodeAuthServiceCheckinInProgress = 25004,
// FIRMessagingBackupExcludedPlist.m
// Do NOT USE 6003
kFIRMessagingMessageCodeBackupExcludedPlist000 = 26000,
kFIRMessagingMessageCodeBackupExcludedPlist001 = 26001,
kFIRMessagingMessageCodeBackupExcludedPlist002 = 26002,
// FIRMessagingCheckinService.m
kFIRMessagingMessageCodeService000 = 27000,
kFIRMessagingMessageCodeService001 = 27001,
kFIRMessagingMessageCodeService002 = 27002,
kFIRMessagingMessageCodeService003 = 27003,
kFIRMessagingMessageCodeService004 = 27004,
kFIRMessagingMessageCodeService005 = 27005,
kFIRMessagingMessageCodeService006 = 27006,
kFIRMessagingInvalidSettingResponse = 27008,
// FIRMessagingCheckinStore.m
// DO NOT USE 8002, 8004 - 8008
kFIRMessagingMessageCodeCheckinStore000 = 28000,
kFIRMessagingMessageCodeCheckinStore001 = 28001,
kFIRMessagingMessageCodeCheckinStore003 = 28003,
kFIRMessagingMessageCodeCheckinStoreCheckinPlistDeleted = 28009,
kFIRMessagingMessageCodeCheckinStoreCheckinPlistSaved = 28010,
// DO NOT USE 9000 - 9006
// DO NOT USE 10000 - 10009
// DO NOT USE 11000 - 11002
// DO NOT USE 12000 - 12014
// DO NOT USE 13004, 13005, 13007, 13008, 13010, 13011, 13013, 13014
kFIRMessagingMessageCodeStore000 = 33000,
kFIRMessagingMessageCodeStore002 = 33002,
kFIRMessagingMessageCodeStore003 = 33003,
kFIRMessagingMessageCodeStore006 = 33006,
kFIRMessagingMessageCodeStore009 = 33009,
kFIRMessagingMessageCodeStore012 = 33012,
// FIRMessagingTokenManager.m
// DO NOT USE 14002, 14005
kFIRMessagingMessageCodeTokenManager000 = 34000,
kFIRMessagingMessageCodeTokenManager001 = 34001,
kFIRMessagingMessageCodeTokenManager003 = 34003,
kFIRMessagingMessageCodeTokenManager004 = 34004,
kFIRMessagingMessageCodeTokenManagerErrorDeletingFCMTokensOnAppReset = 34006,
kFIRMessagingMessageCodeTokenManagerDeletedFCMTokensOnAppReset = 34007,
kFIRMessagingMessageCodeTokenManagerSavedAppVersion = 34008,
kFIRMessagingMessageCodeTokenManagerErrorInvalidatingAllTokens = 34009,
kFIRMessagingMessageCodeTokenManagerAPNSChanged = 34010,
kFIRMessagingMessageCodeTokenManagerAPNSChangedTokenInvalidated = 34011,
kFIRMessagingMessageCodeTokenManagerInvalidateStaleToken = 34012,
// FIRMessagingTokenStore.m
// DO NOT USE 15002 - 15013
kFIRMessagingMessageCodeTokenStore000 = 35000,
kFIRMessagingMessageCodeTokenStore001 = 35001,
kFIRMessagingMessageCodeTokenStoreExceptionUnarchivingTokenInfo = 35015,
// DO NOT USE 16000, 18004
// FIRMessagingUtilities.m
kFIRMessagingMessageCodeUtilitiesMissingBundleIdentifier = 38000,
kFIRMessagingMessageCodeUtilitiesAppEnvironmentUtilNotAvailable = 38001,
kFIRMessagingMessageCodeUtilitiesCannotGetHardwareModel = 38002,
kFIRMessagingMessageCodeUtilitiesCannotGetSystemVersion = 38003,
// FIRMessagingTokenOperation.m
kFIRMessagingMessageCodeTokenOperationFailedToSignParams = 39000,
// FIRMessagingTokenFetchOperation.m
// DO NOT USE 40004, 40005
kFIRMessagingMessageCodeTokenFetchOperationFetchRequest = 40000,
kFIRMessagingMessageCodeTokenFetchOperationRequestError = 40001,
kFIRMessagingMessageCodeTokenFetchOperationBadResponse = 40002,
kFIRMessagingMessageCodeTokenFetchOperationBadTokenStructure = 40003,
// FIRMessagingTokenDeleteOperation.m
kFIRMessagingMessageCodeTokenDeleteOperationFetchRequest = 41000,
kFIRMessagingMessageCodeTokenDeleteOperationRequestError = 41001,
kFIRMessagingMessageCodeTokenDeleteOperationBadResponse = 41002,
// FIRMessagingTokenInfo.m
kFIRMessagingMessageCodeTokenInfoBadAPNSInfo = 42000,
kFIRMessagingMessageCodeTokenInfoFirebaseAppIDChanged = 42001,
kFIRMessagingMessageCodeTokenInfoLocaleChanged = 42002,
// FIRMessagingKeychain.m
kFIRMessagingKeychainReadItemError = 43000,
kFIRMessagingKeychainAddItemError = 43001,
kFIRMessagingKeychainDeleteItemError = 43002,
kFIRMessagingKeychainCreateKeyPairError = 43003,
kFIRMessagingKeychainUpdateItemError = 43004,
};

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Global constants to be put here.
*
*/
#import <Foundation/Foundation.h>
#ifndef _FIRMessaging_CONSTANTS_H
#define _FIRMessaging_CONSTANTS_H
FOUNDATION_EXPORT NSString *const kFIRMessagingFromKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingMessageIDKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingMessageAPNSContentAvailableKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingMessageSyncMessageTTLKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingMessageLinkKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingSenderID;
FOUNDATION_EXPORT NSString *const kFIRMessagingFID;
FOUNDATION_EXPORT NSString *const kFIRMessagingAnalyticsComposerIdentifier;
FOUNDATION_EXPORT NSString *const kFIRMessagingAnalyticsMessageLabel;
FOUNDATION_EXPORT NSString *const kFIRMessagingAnalyticsComposerLabel;
FOUNDATION_EXPORT NSString *const kFIRMessagingProductID;
FOUNDATION_EXPORT NSString *const kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingSubDirectoryName;
#pragma mark - Notifications
FOUNDATION_EXPORT NSString *const kFIRMessagingCheckinFetchedNotification;
FOUNDATION_EXPORT NSString *const kFIRMessagingAPNSTokenNotification;
FOUNDATION_EXPORT NSString *const kFIRMessagingDefaultGCMTokenFailNotification;
FOUNDATION_EXPORT NSString *const kFIRMessagingRegistrationTokenRefreshNotification;
FOUNDATION_EXPORT const int kFIRMessagingSendTtlDefault; // 24 hours
/**
* Value included in a structured response indicating an identity reset.
*/
FOUNDATION_EXPORT NSString *const kFIRMessaging_CMD_RST;
#pragma mark - Miscellaneous
/// The scope used to save the IID "*" scope token. This is used for saving the
/// IID auth token that we receive from the server. This feature was never
/// implemented on the server side.
FOUNDATION_EXPORT NSString *const kFIRMessagingAllScopeIdentifier;
/// The scope used to save the IID "*" scope token.
FOUNDATION_EXPORT NSString *const kFIRMessagingDefaultTokenScope;
/// Denylisted "fiam" token scope.
FOUNDATION_EXPORT NSString *const kFIRMessagingFIAMTokenScope;
/// Subdirectory in search path directory to store InstanceID preferences.
FOUNDATION_EXPORT NSString *const kFIRMessagingInstanceIDSubDirectoryName;
/// The key for APNS token in options dictionary.
FOUNDATION_EXPORT NSString *const kFIRMessagingTokenOptionsAPNSKey;
/// The key for APNS token environment type in options dictionary.
FOUNDATION_EXPORT NSString *const kFIRMessagingTokenOptionsAPNSIsSandboxKey;
/// The key for GMP AppID sent in registration requests.
FOUNDATION_EXPORT NSString *const kFIRMessagingTokenOptionsFirebaseAppIDKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingAPNSTokenType;
/// The key to enable auto-register by swizzling AppDelegate's methods.
FOUNDATION_EXPORT NSString *const kFIRMessagingAppDelegateProxyEnabledInfoPlistKey;
/// Error code for missing entitlements in Keychain. iOS Keychain error
/// https://forums.developer.apple.com/thread/4743
FOUNDATION_EXPORT const int kFIRMessagingSecMissingEntitlementErrorCode;
/// The key for InstallationID or InstanceID in token request.
FOUNDATION_EXPORT NSString *const kFIRMessagingParamInstanceID;
#endif

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
NSString *const kFIRMessagingFromKey = @"from";
NSString *const kFIRMessagingSendTo = @"google."
@"to";
NSString *const kFIRMessagingSendTTL = @"google."
@"ttl";
NSString *const kFIRMessagingSendDelay = @"google."
@"delay";
NSString *const kFIRMessagingSendMessageID = @"google."
@"msg_id";
NSString *const KFIRMessagingSendMessageAppData = @"google."
@"data";
NSString *const kFIRMessagingMessageInternalReservedKeyword = @"gcm.";
NSString *const kFIRMessagingMessagePersistentIDKey = @"persistent_id";
NSString *const kFIRMessagingMessageIDKey = @"gcm.message_id";
NSString *const kFIRMessagingMessageAPNSContentAvailableKey = @"content-available";
NSString *const kFIRMessagingMessageSyncMessageTTLKey = @"gcm."
@"ttl";
NSString *const kFIRMessagingMessageLinkKey = @"gcm."
@"app_link";
NSString *const kFIRMessagingSenderID = @"google.c.sender.id";
NSString *const kFIRMessagingFID = @"google.c.fid";
NSString *const kFIRMessagingAnalyticsComposerIdentifier = @"google.c.a.c_id";
NSString *const kFIRMessagingAnalyticsMessageLabel = @"google.c.a.m_l";
NSString *const kFIRMessagingAnalyticsComposerLabel = @"google.c.a.c_l";
NSString *const kFIRMessagingProductID = @"google.product_id";
NSString *const kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey =
@"FirebaseAppDelegateProxyEnabled";
NSString *const kFIRMessagingSubDirectoryName = @"Google/FirebaseMessaging";
// Notifications
NSString *const kFIRMessagingCheckinFetchedNotification = @"com.google.gcm.notif-checkin-fetched";
NSString *const kFIRMessagingAPNSTokenNotification = @"com.firebase.iid.notif.apns-token";
NSString *const kFIRMessagingRegistrationTokenRefreshNotification =
@"com.firebase.iid.notif.refresh-token";
const int kFIRMessagingSendTtlDefault = 24 * 60 * 60; // 24 hours
// Commands
NSString *const kFIRMessaging_CMD_RST = @"RST";
// NOTIFICATIONS
NSString *const kFIRMessagingDefaultGCMTokenFailNotification =
@"com.firebase.iid.notif.fcm-token-fail";
// Miscellaneous
NSString *const kFIRMessagingAllScopeIdentifier = @"iid-all";
NSString *const kFIRMessagingDefaultTokenScope = @"*";
NSString *const kFIRMessagingFIAMTokenScope = @"fiam";
NSString *const kFIRMessagingInstanceIDSubDirectoryName = @"Google/FirebaseInstanceID";
// Registration Options
NSString *const kFIRMessagingTokenOptionsAPNSKey = @"apns_token";
NSString *const kFIRMessagingTokenOptionsAPNSIsSandboxKey = @"apns_sandbox";
NSString *const kFIRMessagingTokenOptionsFirebaseAppIDKey = @"gmp_app_id";
NSString *const kFIRMessagingParamInstanceID = @"appid";
NSString *const kFIRMessagingAPNSTokenType =
@"APNSTokenType"; // APNS Token type key stored in user info.
NSString *const kFIRMessagingAppDelegateProxyEnabledInfoPlistKey =
@"FirebaseAppDelegateProxyEnabled";
// iOS Keychain error https://forums.developer.apple.com/thread/4743
// An undocumented error code hence need to be redeclared.
const int kFIRMessagingSecMissingEntitlementErrorCode = -34018;

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerCategory;
FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerLocalTimeStart;
FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerLocalTimeEnd;
FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerBodyKey;
@interface FIRMessagingContextManagerService : NSObject
/**
* Check if the message is a context manager message or not.
*
* @param message The message to verify.
*
* @return YES if the message is a context manager message else NO.
*/
+ (BOOL)isContextManagerMessage:(NSDictionary *)message;
/**
* Handle context manager message.
*
* @param message The message to handle.
*
* @return YES if the message was handled successfully else NO.
*/
+ (BOOL)handleContextManagerMessage:(NSDictionary *)message;
@end

View File

@@ -0,0 +1,239 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 || \
__MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_14 || __TV_OS_VERSION_MAX_ALLOWED >= __TV_10_0 || \
__WATCH_OS_VERSION_MAX_ALLOWED >= __WATCHOS_3_0 || TARGET_OS_MACCATALYST
#import <UserNotifications/UserNotifications.h>
#endif
#import "FirebaseMessaging/Sources/FIRMessagingContextManagerService.h"
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import <GoogleUtilities/GULAppDelegateSwizzler.h>
#define kFIRMessagingContextManagerPrefix @"gcm."
#define kFIRMessagingContextManagerPrefixKey @"google.c.cm."
#define kFIRMessagingContextManagerNotificationKeyPrefix @"gcm.notification."
static NSString *const kLogTag = @"FIRMessagingAnalytics";
static NSString *const kLocalTimeFormatString = @"yyyy-MM-dd HH:mm:ss";
static NSString *const kContextManagerPrefixKey = kFIRMessagingContextManagerPrefixKey;
// Local timed messages (format yyyy-mm-dd HH:mm:ss)
NSString *const kFIRMessagingContextManagerLocalTimeStart =
kFIRMessagingContextManagerPrefixKey @"lt_start";
NSString *const kFIRMessagingContextManagerLocalTimeEnd =
kFIRMessagingContextManagerPrefixKey @"lt_end";
// Local Notification Params
NSString *const kFIRMessagingContextManagerBodyKey =
kFIRMessagingContextManagerNotificationKeyPrefix @"body";
NSString *const kFIRMessagingContextManagerTitleKey =
kFIRMessagingContextManagerNotificationKeyPrefix @"title";
NSString *const kFIRMessagingContextManagerBadgeKey =
kFIRMessagingContextManagerNotificationKeyPrefix @"badge";
NSString *const kFIRMessagingContextManagerCategoryKey =
kFIRMessagingContextManagerNotificationKeyPrefix @"click_action";
NSString *const kFIRMessagingContextManagerSoundKey =
kFIRMessagingContextManagerNotificationKeyPrefix @"sound";
NSString *const kFIRMessagingContextManagerContentAvailableKey =
kFIRMessagingContextManagerNotificationKeyPrefix @"content-available";
static NSString *const kFIRMessagingID = kFIRMessagingContextManagerPrefix @"message_id";
static NSString *const kFIRMessagingAPNSPayloadKey = @"aps";
typedef NS_ENUM(NSUInteger, FIRMessagingContextManagerMessageType) {
FIRMessagingContextManagerMessageTypeNone,
FIRMessagingContextManagerMessageTypeLocalTime,
};
@implementation FIRMessagingContextManagerService
+ (BOOL)isContextManagerMessage:(NSDictionary *)message {
// For now we only support local time in ContextManager.
if (![message[kFIRMessagingContextManagerLocalTimeStart] length]) {
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeContextManagerService000,
@"Received message missing local start time, not a contextual message.");
return NO;
}
return YES;
}
+ (BOOL)handleContextManagerMessage:(NSDictionary *)message {
NSString *startTimeString = message[kFIRMessagingContextManagerLocalTimeStart];
if (startTimeString.length) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeContextManagerService001,
@"%@ Received context manager message with local time %@", kLogTag,
startTimeString);
return [self handleContextManagerLocalTimeMessage:message];
}
return NO;
}
+ (BOOL)handleContextManagerLocalTimeMessage:(NSDictionary *)message {
NSString *startTimeString = message[kFIRMessagingContextManagerLocalTimeStart];
if (!startTimeString) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeContextManagerService002,
@"Invalid local start date format %@. Message dropped",
startTimeString);
return NO;
}
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
[dateFormatter setDateFormat:kLocalTimeFormatString];
NSDate *startDate = [dateFormatter dateFromString:startTimeString];
NSDate *currentDate = [NSDate date];
if ([currentDate compare:startDate] == NSOrderedAscending) {
[self scheduleLocalNotificationForMessage:message atDate:startDate];
} else {
// check end time has not passed
NSString *endTimeString = message[kFIRMessagingContextManagerLocalTimeEnd];
if (!endTimeString) {
FIRMessagingLoggerInfo(
kFIRMessagingMessageCodeContextManagerService003,
@"No end date specified for message, start date elapsed. Message dropped.");
return YES;
}
NSDate *endDate = [dateFormatter dateFromString:endTimeString];
if (!endTimeString) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeContextManagerService004,
@"Invalid local end date format %@. Message dropped", endTimeString);
return NO;
}
if ([endDate compare:currentDate] == NSOrderedAscending) {
// end date has already passed drop the message
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeContextManagerService005,
@"End date %@ has already passed. Message dropped.", endTimeString);
return YES;
}
// schedule message right now (buffer 10s)
[self scheduleLocalNotificationForMessage:message
atDate:[currentDate dateByAddingTimeInterval:10]];
}
return YES;
}
+ (void)scheduleiOS10LocalNotificationForMessage:(NSDictionary *)message
atDate:(NSDate *)date
API_AVAILABLE(macosx(10.14), ios(10.0), watchos(3.0), tvos(10.0)) {
NSCalendar *calendar = [NSCalendar currentCalendar];
NSCalendarUnit unit = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay |
NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
NSDateComponents *dateComponents = [calendar components:(NSCalendarUnit)unit fromDate:date];
UNCalendarNotificationTrigger *trigger =
[UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents repeats:NO];
UNMutableNotificationContent *content = [self contentFromContextualMessage:message];
NSString *identifier = message[kFIRMessagingID];
if (!identifier) {
identifier = [NSUUID UUID].UUIDString;
}
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier
content:content
trigger:trigger];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center
addNotificationRequest:request
withCompletionHandler:^(NSError *_Nullable error) {
if (error) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeContextManagerServiceFailedLocalSchedule,
@"Failed scheduling local timezone notification: %@.", error);
}
}];
}
+ (UNMutableNotificationContent *)contentFromContextualMessage:(NSDictionary *)message
API_AVAILABLE(macosx(10.14), ios(10.0), watchos(3.0), tvos(10.0)) {
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
NSDictionary *apsDictionary = message;
// Badge is universal
if (apsDictionary[kFIRMessagingContextManagerBadgeKey]) {
content.badge = apsDictionary[kFIRMessagingContextManagerBadgeKey];
}
#if TARGET_OS_IOS || TARGET_OS_OSX || TARGET_OS_WATCH
// The following fields are not available on tvOS
if ([apsDictionary[kFIRMessagingContextManagerBodyKey] length]) {
content.body = apsDictionary[kFIRMessagingContextManagerBodyKey];
}
if ([apsDictionary[kFIRMessagingContextManagerTitleKey] length]) {
content.title = apsDictionary[kFIRMessagingContextManagerTitleKey];
}
if (apsDictionary[kFIRMessagingContextManagerSoundKey]) {
#if !TARGET_OS_WATCH
// UNNotificationSound soundNamded: is not available in watchOS
content.sound =
[UNNotificationSound soundNamed:apsDictionary[kFIRMessagingContextManagerSoundKey]];
#else // !TARGET_OS_WATCH
content.sound = [UNNotificationSound defaultSound];
#endif // !TARGET_OS_WATCH
}
if (apsDictionary[kFIRMessagingContextManagerCategoryKey]) {
content.categoryIdentifier = apsDictionary[kFIRMessagingContextManagerCategoryKey];
}
NSDictionary *userInfo = [self parseDataFromMessage:message];
if (userInfo.count) {
content.userInfo = userInfo;
}
#endif // TARGET_OS_IOS || TARGET_OS_OSX || TARGET_OS_WATCH
return content;
}
+ (void)scheduleLocalNotificationForMessage:(NSDictionary *)message atDate:(NSDate *)date {
if (@available(macOS 10.14, *)) {
[self scheduleiOS10LocalNotificationForMessage:message atDate:date];
return;
}
}
+ (NSDictionary *)parseDataFromMessage:(NSDictionary *)message {
NSMutableDictionary *data = [NSMutableDictionary dictionary];
for (NSObject<NSCopying> *key in message) {
if ([key isKindOfClass:[NSString class]]) {
NSString *keyString = (NSString *)key;
if ([keyString isEqualToString:kFIRMessagingContextManagerContentAvailableKey]) {
continue;
} else if ([keyString hasPrefix:kContextManagerPrefixKey]) {
continue;
} else if ([keyString isEqualToString:kFIRMessagingAPNSPayloadKey]) {
// Local timezone message is scheduled with FCM payload. APNS payload with
// content_available should be ignored and not passed to the scheduled
// messages.
continue;
}
}
data[[key copy]] = message[key];
}
return [data copy];
}
@end

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef FIRMessaging_xcodeproj_FIRMessagingDefines_h
#define FIRMessaging_xcodeproj_FIRMessagingDefines_h
// WEAKIFY & STRONGIFY
// Helper macro.
#define _FIRMessaging_WEAKNAME(VAR) VAR##_weak_
#define FIRMessaging_WEAKIFY(VAR) __weak __typeof__(VAR) _FIRMessaging_WEAKNAME(VAR) = (VAR);
#define FIRMessaging_STRONGIFY(VAR) \
_Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wshadow\"") \
__strong __typeof__(VAR) VAR = _FIRMessaging_WEAKNAME(VAR); \
_Pragma("clang diagnostic pop")
#ifndef _FIRMessaging_UL
#define _FIRMessaging_UL(v) (unsigned long)(v)
#endif
#endif
// Invalidates the initializer from which it's called.
#ifndef FIRMessagingInvalidateInitializer
#define FIRMessagingInvalidateInitializer() \
do { \
[self class]; /* Avoid warning of dead store to |self|. */ \
NSAssert(NO, @"Invalid initializer."); \
return nil; \
} while (0)
#endif

View File

@@ -0,0 +1,294 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <nanopb/pb.h>
#import <nanopb/pb_decode.h>
#import <nanopb/pb_encode.h>
#import <GoogleDataTransport/GoogleDataTransport.h>
#import <GoogleUtilities/GULAppEnvironmentUtil.h>
#import "FirebaseMessaging/Sources/FIRMessagingCode.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/Protogen/nanopb/me.nanopb.h"
#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessagingExtensionHelper.h"
static NSString *const kPayloadOptionsName = @"fcm_options";
static NSString *const kPayloadOptionsImageURLName = @"image";
static NSString *const kNoExtension = @"";
static NSString *const kImagePathPrefix = @"image/";
#pragma mark - nanopb helper functions
/** Callocs a pb_bytes_array and copies the given NSData bytes into the bytes array.
*
* @note Memory needs to be free manually, through pb_free or pb_release.
* @param data The data to copy into the new bytes array.
*/
pb_bytes_array_t *FIRMessagingEncodeData(NSData *data) {
pb_bytes_array_t *pbBytesArray = calloc(1, PB_BYTES_ARRAY_T_ALLOCSIZE(data.length));
if (pbBytesArray != NULL) {
[data getBytes:pbBytesArray->bytes length:data.length];
pbBytesArray->size = (pb_size_t)data.length;
}
return pbBytesArray;
}
/** Callocs a pb_bytes_array and copies the given NSString's bytes into the bytes array.
*
* @note Memory needs to be free manually, through pb_free or pb_release.
* @param string The string to encode as pb_bytes.
*/
pb_bytes_array_t *FIRMessagingEncodeString(NSString *string) {
NSData *stringBytes = [string dataUsingEncoding:NSUTF8StringEncoding];
return FIRMessagingEncodeData(stringBytes);
}
@interface FIRMessagingMetricsLog : NSObject <GDTCOREventDataObject>
@property(nonatomic) fm_MessagingClientEventExtension eventExtension;
@end
@implementation FIRMessagingMetricsLog
- (instancetype)initWithEventExtension:(fm_MessagingClientEventExtension)eventExtension {
self = [super init];
if (self) {
_eventExtension = eventExtension;
}
return self;
}
- (NSData *)transportBytes {
pb_ostream_t sizestream = PB_OSTREAM_SIZING;
// Encode 1 time to determine the size.
if (!pb_encode(&sizestream, fm_MessagingClientEventExtension_fields, &_eventExtension)) {
FIRMessagingLoggerError(kFIRMessagingServiceExtensionTransportBytesError,
@"Error in nanopb encoding for size: %s", PB_GET_ERROR(&sizestream));
}
// Encode a 2nd time to actually get the bytes from it.
size_t bufferSize = sizestream.bytes_written;
CFMutableDataRef dataRef = CFDataCreateMutable(CFAllocatorGetDefault(), bufferSize);
CFDataSetLength(dataRef, bufferSize);
pb_ostream_t ostream = pb_ostream_from_buffer((void *)CFDataGetBytePtr(dataRef), bufferSize);
if (!pb_encode(&ostream, fm_MessagingClientEventExtension_fields, &_eventExtension)) {
FIRMessagingLoggerError(kFIRMessagingServiceExtensionTransportBytesError,
@"Error in nanopb encoding for bytes: %s", PB_GET_ERROR(&ostream));
}
CFDataSetLength(dataRef, ostream.bytes_written);
return CFBridgingRelease(dataRef);
}
@end
@interface FIRMessagingExtensionHelper ()
@property(nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property(nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@end
@implementation FIRMessagingExtensionHelper
- (void)populateNotificationContent:(UNMutableNotificationContent *)content
withContentHandler:(void (^)(UNNotificationContent *_Nonnull))contentHandler {
self.contentHandler = [contentHandler copy];
self.bestAttemptContent = content;
// The `userInfo` property isn't available on newer versions of tvOS.
#if TARGET_OS_IOS || TARGET_OS_OSX || TARGET_OS_WATCH
NSObject *currentImageURL = content.userInfo[kPayloadOptionsName][kPayloadOptionsImageURLName];
if (!currentImageURL || currentImageURL == [NSNull null]) {
[self deliverNotification];
return;
}
NSURL *attachmentURL = [NSURL URLWithString:(NSString *)currentImageURL];
if (attachmentURL) {
[self loadAttachmentForURL:attachmentURL
completionHandler:^(UNNotificationAttachment *attachment) {
if (attachment != nil) {
self.bestAttemptContent.attachments = @[ attachment ];
}
[self deliverNotification];
}];
} else {
FIRMessagingLoggerError(kFIRMessagingServiceExtensionImageInvalidURL,
@"The Image URL provided is invalid %@.", currentImageURL);
[self deliverNotification];
}
#else
[self deliverNotification];
#endif
}
#if TARGET_OS_IOS || TARGET_OS_OSX || TARGET_OS_WATCH
- (NSString *)fileExtensionForResponse:(NSURLResponse *)response {
NSString *suggestedPathExtension = [response.suggestedFilename pathExtension];
if (suggestedPathExtension.length > 0) {
return [NSString stringWithFormat:@".%@", suggestedPathExtension];
}
if ([response.MIMEType containsString:kImagePathPrefix]) {
return [response.MIMEType stringByReplacingOccurrencesOfString:kImagePathPrefix
withString:@"."];
}
return kNoExtension;
}
- (void)loadAttachmentForURL:(NSURL *)attachmentURL
completionHandler:(void (^)(UNNotificationAttachment *))completionHandler {
__block UNNotificationAttachment *attachment = nil;
NSURLSession *session = [NSURLSession
sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
[[session
downloadTaskWithURL:attachmentURL
completionHandler:^(NSURL *temporaryFileLocation, NSURLResponse *response, NSError *error) {
if (error != nil) {
FIRMessagingLoggerError(kFIRMessagingServiceExtensionImageNotDownloaded,
@"Failed to download image given URL %@, error: %@\n",
attachmentURL, error);
completionHandler(attachment);
return;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *fileExtension = [self fileExtensionForResponse:response];
NSURL *localURL = [NSURL
fileURLWithPath:[temporaryFileLocation.path stringByAppendingString:fileExtension]];
[fileManager moveItemAtURL:temporaryFileLocation toURL:localURL error:&error];
if (error) {
FIRMessagingLoggerError(
kFIRMessagingServiceExtensionLocalFileNotCreated,
@"Failed to move the image file to local location: %@, error: %@\n", localURL,
error);
completionHandler(attachment);
return;
}
attachment = [UNNotificationAttachment attachmentWithIdentifier:@""
URL:localURL
options:nil
error:&error];
if (error) {
FIRMessagingLoggerError(kFIRMessagingServiceExtensionImageNotAttached,
@"Failed to create attachment with URL %@, error: %@\n",
localURL, error);
completionHandler(attachment);
return;
}
completionHandler(attachment);
}] resume];
}
#endif
- (void)deliverNotification {
if (self.contentHandler) {
self.contentHandler(self.bestAttemptContent);
}
}
- (void)exportDeliveryMetricsToBigQueryWithMessageInfo:(NSDictionary *)info {
GDTCORTransport *transport = [[GDTCORTransport alloc] initWithMappingID:@"1249"
transformers:nil
target:kGDTCORTargetCCT];
fm_MessagingClientEventExtension eventExtension = fm_MessagingClientEventExtension_init_default;
fm_MessagingClientEvent clientEvent = fm_MessagingClientEvent_init_default;
if (!info[kFIRMessagingSenderID]) {
FIRMessagingLoggerError(kFIRMessagingServiceExtensionInvalidProjectID,
@"Delivery logging failed: Invalid project ID");
return;
}
clientEvent.project_number = (int64_t)[info[kFIRMessagingSenderID] longLongValue];
if (!info[kFIRMessagingMessageIDKey] ||
![info[kFIRMessagingMessageIDKey] isKindOfClass:NSString.class]) {
FIRMessagingLoggerWarn(kFIRMessagingServiceExtensionInvalidMessageID,
@"Delivery logging failed: Invalid Message ID");
return;
}
clientEvent.message_id = FIRMessagingEncodeString(info[kFIRMessagingMessageIDKey]);
if (!info[kFIRMessagingFID] || ![info[kFIRMessagingFID] isKindOfClass:NSString.class]) {
FIRMessagingLoggerWarn(kFIRMessagingServiceExtensionInvalidInstanceID,
@"Delivery logging failed: Invalid Instance ID");
return;
}
clientEvent.instance_id = FIRMessagingEncodeString(info[kFIRMessagingFID]);
if ([info[@"aps"][kFIRMessagingMessageAPNSContentAvailableKey] intValue] == 1 &&
![GULAppEnvironmentUtil isAppExtension]) {
clientEvent.message_type = fm_MessagingClientEvent_MessageType_DATA_MESSAGE;
} else {
clientEvent.message_type = fm_MessagingClientEvent_MessageType_DISPLAY_NOTIFICATION;
}
clientEvent.sdk_platform = fm_MessagingClientEvent_SDKPlatform_IOS;
NSString *bundleID = [NSBundle mainBundle].bundleIdentifier;
if ([GULAppEnvironmentUtil isAppExtension]) {
bundleID = [[self class] bundleIdentifierByRemovingLastPartFrom:bundleID];
}
if (bundleID) {
clientEvent.package_name = FIRMessagingEncodeString(bundleID);
}
clientEvent.event = fm_MessagingClientEvent_Event_MESSAGE_DELIVERED;
if (info[kFIRMessagingAnalyticsMessageLabel]) {
clientEvent.analytics_label =
FIRMessagingEncodeString(info[kFIRMessagingAnalyticsMessageLabel]);
}
if (info[kFIRMessagingAnalyticsComposerIdentifier]) {
clientEvent.campaign_id =
(int64_t)[info[kFIRMessagingAnalyticsComposerIdentifier] longLongValue];
}
if (info[kFIRMessagingAnalyticsComposerLabel]) {
clientEvent.composer_label =
FIRMessagingEncodeString(info[kFIRMessagingAnalyticsComposerLabel]);
}
eventExtension.messaging_client_event = &clientEvent;
FIRMessagingMetricsLog *log =
[[FIRMessagingMetricsLog alloc] initWithEventExtension:eventExtension];
GDTCOREvent *event;
if (info[kFIRMessagingProductID]) {
int32_t productID = [info[kFIRMessagingProductID] intValue];
GDTCORProductData *productData = [[GDTCORProductData alloc] initWithProductID:productID];
event = [transport eventForTransportWithProductData:productData];
} else {
event = [transport eventForTransport];
}
event.dataObject = log;
event.qosTier = GDTCOREventQoSFast;
// Use this API for SDK service data events.
[transport sendDataEvent:event];
}
+ (NSString *)bundleIdentifierByRemovingLastPartFrom:(NSString *)bundleIdentifier {
NSString *bundleIDComponentsSeparator = @".";
NSMutableArray<NSString *> *bundleIDComponents =
[[bundleIdentifier componentsSeparatedByString:bundleIDComponentsSeparator] mutableCopy];
[bundleIDComponents removeLastObject];
return [bundleIDComponents componentsJoinedByString:bundleIDComponentsSeparator];
}
@end

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/FIRMessagingCode.h"
// The convenience macros are only defined if they haven't already been defined.
#ifndef FIRMessagingLoggerInfo
// Convenience macros that log to the shared FIRMessagingLogger instance. These macros
// are how users should typically log to FIRMessagingLogger.
#define FIRMessagingLoggerDebug(code, ...) \
[FIRMessagingSharedLogger() logFuncDebug:__func__ messageCode:code msg:__VA_ARGS__]
#define FIRMessagingLoggerInfo(code, ...) \
[FIRMessagingSharedLogger() logFuncInfo:__func__ messageCode:code msg:__VA_ARGS__]
#define FIRMessagingLoggerNotice(code, ...) \
[FIRMessagingSharedLogger() logFuncNotice:__func__ messageCode:code msg:__VA_ARGS__]
#define FIRMessagingLoggerWarn(code, ...) \
[FIRMessagingSharedLogger() logFuncWarning:__func__ messageCode:code msg:__VA_ARGS__]
#define FIRMessagingLoggerError(code, ...) \
[FIRMessagingSharedLogger() logFuncError:__func__ messageCode:code msg:__VA_ARGS__]
#endif // !defined(FIRMessagingLoggerInfo)
@interface FIRMessagingLogger : NSObject
- (void)logFuncDebug:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
- (void)logFuncInfo:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
- (void)logFuncNotice:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
- (void)logFuncWarning:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
- (void)logFuncError:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
@end
/**
* Instantiates and/or returns a shared FIRMessagingLogger used exclusively
* for FIRMessaging log messages.
*
* @return the shared FIRMessagingLogger instance
*/
FIRMessagingLogger *FIRMessagingSharedLogger(void);

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
FIRLoggerService kFIRLoggerMessaging = @"[FirebaseMessaging]";
@implementation FIRMessagingLogger
+ (instancetype)standardLogger {
return [[FIRMessagingLogger alloc] init];
}
#pragma mark - Log Helpers
+ (NSString *)formatMessageCode:(FIRMessagingMessageCode)messageCode {
return [NSString stringWithFormat:@"I-FCM%06ld", (long)messageCode];
}
- (void)logFuncDebug:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelDebug, kFIRLoggerMessaging,
[FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
- (void)logFuncInfo:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelInfo, kFIRLoggerMessaging,
[FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
- (void)logFuncNotice:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelNotice, kFIRLoggerMessaging,
[FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
- (void)logFuncWarning:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelWarning, kFIRLoggerMessaging,
[FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
- (void)logFuncError:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelError, kFIRLoggerMessaging,
[FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
@end
FIRMessagingLogger *FIRMessagingSharedLogger(void) {
static dispatch_once_t onceToken;
static FIRMessagingLogger *logger;
dispatch_once(&onceToken, ^{
logger = [FIRMessagingLogger standardLogger];
});
return logger;
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h"
#import "FirebaseMessaging/Sources/FIRMessagingTopicsCommon.h"
NS_ASSUME_NONNULL_BEGIN
/**
* Represents a single batch of topics, with the same action.
*
* Topic operations which have the same action (subscribe or unsubscribe) can be executed
* simultaneously, as the order of operations do not matter with the same action. The set of
* topics is unique, as it doesn't make sense to apply the same action to the same topic
* repeatedly; the result would be the same as the first time.
*/
@interface FIRMessagingTopicBatch : NSObject <NSSecureCoding>
@property(nonatomic, readonly, assign) FIRMessagingTopicAction action;
@property(nonatomic, readonly, copy) NSMutableSet<NSString *> *topics;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithAction:(FIRMessagingTopicAction)action NS_DESIGNATED_INITIALIZER;
@end
@class FIRMessagingPendingTopicsList;
/**
* This delegate must be supplied to the instance of FIRMessagingPendingTopicsList, via the
* @cdelegate property. It lets the
* pending topics list know whether or not it can begin making requests via
* @c-pendingTopicsListCanRequestTopicUpdates:, and handles the request to actually
* perform the topic operation. The delegate also handles when the pending topics list is updated,
* so that it can be archived or persisted.
*
* @see FIRMessagingPendingTopicsList
*/
@protocol FIRMessagingPendingTopicsListDelegate <NSObject>
- (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
requestedUpdateForTopic:(NSString *)topic
action:(FIRMessagingTopicAction)action
completion:(FIRMessagingTopicOperationCompletion)completion;
- (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list;
- (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list;
@end
/**
* FIRMessagingPendingTopicsList manages a list of topic subscription updates, batched by the same
* action (subscribe or unsubscribe). The list roughly maintains the order of the topic operations,
* batched together whenever the topic action (subscribe or unsubscribe) changes.
*
* Topics operations are batched by action because it is safe to perform the same topic action
* (subscribe or unsubscribe) on many topics simultaneously. After each batch is successfully
* completed, the next batch operations can begin.
*
* When asked to resume its operations, FIRMessagingPendingTopicsList will begin performing updates
* of its current batch of topics. For example, it may begin subscription operations for topics
* [A, B, C] simultaneously.
*
* When the current batch is completed, the next batch of operations will be started. For example
* the list may begin unsubscribe operations for [D, A, E]. Note that because A is in both batches,
* A will be correctly subscribed in the first batch, then unsubscribed as part of the second batch
* of operations. Without batching, it would be ambiguous whether A's subscription operation or the
* unsubscription operation would be completed first.
*
* An app can subscribe and unsubscribe from many topics, and this class helps persist the pending
* topics and perform the operation safely and correctly.
*
* When a topic fails to subscribe or unsubscribe due to a network error, it is considered a
* recoverable error, and so it remains in the current batch until it is successfully completed.
* Topic updates are completed when they either (a) succeed, (b) are cancelled, or (c) result in an
* unrecoverable error. Any error outside of `NSURLErrorDomain` is considered an unrecoverable
* error.
*
* In addition to maintaining the list of pending topic updates, FIRMessagingPendingTopicsList also
* can track completion handlers for topic operations.
*
* @discussion Completion handlers for topic updates are not maintained if it was restored from a
* keyed archive. They are only called if the topic operation finished within the same app session.
*
* You must supply an object conforming to FIRMessagingPendingTopicsListDelegate in order for the
* topic operations to execute.
*
* @see FIRMessagingPendingTopicsListDelegate
*/
@interface FIRMessagingPendingTopicsList : NSObject <NSSecureCoding>
@property(nonatomic, weak) NSObject<FIRMessagingPendingTopicsListDelegate> *delegate;
@property(nonatomic, readonly, strong, nullable) NSDate *archiveDate;
@property(nonatomic, readonly) NSUInteger numberOfBatches;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (void)addOperationForTopic:(NSString *)topic
withAction:(FIRMessagingTopicAction)action
completion:(nullable FIRMessagingTopicOperationCompletion)completion;
- (void)resumeOperationsIfNeeded;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,271 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/FIRMessagingPendingTopicsList.h"
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingPubSub.h"
#import "FirebaseMessaging/Sources/FIRMessaging_Private.h"
NSString *const kPendingTopicBatchActionKey = @"action";
NSString *const kPendingTopicBatchTopicsKey = @"topics";
NSString *const kPendingBatchesEncodingKey = @"batches";
NSString *const kPendingTopicsTimestampEncodingKey = @"ts";
#pragma mark - FIRMessagingTopicBatch
@interface FIRMessagingTopicBatch ()
@property(nonatomic, strong, nonnull)
NSMutableDictionary<NSString *, NSMutableArray<FIRMessagingTopicOperationCompletion> *>
*topicHandlers;
@end
@implementation FIRMessagingTopicBatch
- (instancetype)initWithAction:(FIRMessagingTopicAction)action {
if (self = [super init]) {
_action = action;
_topics = [NSMutableSet set];
_topicHandlers = [NSMutableDictionary dictionary];
}
return self;
}
#pragma mark NSSecureCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeInteger:self.action forKey:kPendingTopicBatchActionKey];
[aCoder encodeObject:self.topics forKey:kPendingTopicBatchTopicsKey];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
// Ensure that our integer -> enum casting is safe
NSInteger actionRawValue = [aDecoder decodeIntegerForKey:kPendingTopicBatchActionKey];
FIRMessagingTopicAction action = FIRMessagingTopicActionSubscribe;
if (actionRawValue == FIRMessagingTopicActionUnsubscribe) {
action = FIRMessagingTopicActionUnsubscribe;
}
if (self = [self initWithAction:action]) {
_topics = [aDecoder
decodeObjectOfClasses:[NSSet setWithObjects:NSMutableSet.class, NSString.class, nil]
forKey:kPendingTopicBatchTopicsKey];
_topicHandlers = [NSMutableDictionary dictionary];
}
return self;
}
@end
#pragma mark - FIRMessagingPendingTopicsList
@interface FIRMessagingPendingTopicsList ()
@property(nonatomic, readwrite, strong) NSDate *archiveDate;
@property(nonatomic, strong) NSMutableArray<FIRMessagingTopicBatch *> *topicBatches;
@property(nonatomic, strong) FIRMessagingTopicBatch *currentBatch;
@property(nonatomic, strong) NSMutableSet<NSString *> *topicsInFlight;
@end
@implementation FIRMessagingPendingTopicsList
- (instancetype)init {
if (self = [super init]) {
_topicBatches = [NSMutableArray array];
_topicsInFlight = [NSMutableSet set];
}
return self;
}
+ (void)pruneTopicBatches:(NSMutableArray<FIRMessagingTopicBatch *> *)topicBatches {
// For now, just remove empty batches. In the future we can use this to make the subscriptions
// more efficient, by actually pruning topic actions that cancel each other out, for example.
for (NSInteger i = topicBatches.count - 1; i >= 0; i--) {
FIRMessagingTopicBatch *batch = topicBatches[i];
if (batch.topics.count == 0) {
[topicBatches removeObjectAtIndex:i];
}
}
}
#pragma mark NSSecureCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:[NSDate date] forKey:kPendingTopicsTimestampEncodingKey];
[aCoder encodeObject:self.topicBatches forKey:kPendingBatchesEncodingKey];
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [self init]) {
_archiveDate =
[aDecoder decodeObjectOfClass:NSDate.class forKey:kPendingTopicsTimestampEncodingKey];
_topicBatches =
[aDecoder decodeObjectOfClasses:[NSSet setWithObjects:NSMutableArray.class,
FIRMessagingTopicBatch.class, nil]
forKey:kPendingBatchesEncodingKey];
if (_topicBatches) {
[FIRMessagingPendingTopicsList pruneTopicBatches:_topicBatches];
}
_topicsInFlight = [NSMutableSet set];
}
return self;
}
#pragma mark Getters
- (NSUInteger)numberOfBatches {
return self.topicBatches.count;
}
#pragma mark Adding/Removing topics
- (void)addOperationForTopic:(NSString *)topic
withAction:(FIRMessagingTopicAction)action
completion:(nullable FIRMessagingTopicOperationCompletion)completion {
FIRMessagingTopicBatch *lastBatch = nil;
@synchronized(self) {
lastBatch = self.topicBatches.lastObject;
if (!lastBatch || lastBatch.action != action) {
// There either was no last batch, or our last batch's action was not the same, so we have to
// create a new batch
lastBatch = [[FIRMessagingTopicBatch alloc] initWithAction:action];
[self.topicBatches addObject:lastBatch];
}
BOOL topicExistedBefore = ([lastBatch.topics member:topic] != nil);
if (!topicExistedBefore) {
[lastBatch.topics addObject:topic];
[self.delegate pendingTopicsListDidUpdate:self];
}
// Add the completion handler to the batch
if (completion) {
NSMutableArray *handlers = lastBatch.topicHandlers[topic];
if (!handlers) {
handlers = [[NSMutableArray alloc] init];
}
[handlers addObject:completion];
lastBatch.topicHandlers[topic] = handlers;
}
if (!self.currentBatch) {
self.currentBatch = lastBatch;
}
// This may have been the first topic added, or was added to an ongoing batch
if (self.currentBatch == lastBatch && !topicExistedBefore) {
// Add this topic to our ongoing operations
FIRMessaging_WEAKIFY(self);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
FIRMessaging_STRONGIFY(self);
[self resumeOperationsIfNeeded];
});
}
}
}
- (void)resumeOperationsIfNeeded {
@synchronized(self) {
// If current batch is not set, set it now
if (!self.currentBatch) {
self.currentBatch = self.topicBatches.firstObject;
}
if (self.currentBatch.topics.count == 0) {
return;
}
if (!self.delegate) {
FIRMessagingLoggerError(kFIRMessagingMessageCodePendingTopicsList000,
@"Attempted to update pending topics without a delegate");
return;
}
if (![self.delegate pendingTopicsListCanRequestTopicUpdates:self]) {
return;
}
for (NSString *topic in self.currentBatch.topics) {
if ([self.topicsInFlight member:topic]) {
// This topic is already active, so skip
continue;
}
[self beginUpdateForCurrentBatchTopic:topic];
}
}
}
- (BOOL)subscriptionErrorIsRecoverable:(NSError *)error {
return [error.domain isEqualToString:NSURLErrorDomain];
}
- (void)beginUpdateForCurrentBatchTopic:(NSString *)topic {
@synchronized(self) {
[self.topicsInFlight addObject:topic];
}
FIRMessaging_WEAKIFY(self);
[self.delegate
pendingTopicsList:self
requestedUpdateForTopic:topic
action:self.currentBatch.action
completion:^(NSError *error) {
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
FIRMessaging_STRONGIFY(self);
@synchronized(self) {
[self.topicsInFlight removeObject:topic];
BOOL recoverableError = [self subscriptionErrorIsRecoverable:error];
if (!error || !recoverableError) {
// Notify our handlers and remove the topic from our batch
NSMutableArray *handlers = self.currentBatch.topicHandlers[topic];
if (handlers.count) {
dispatch_async(dispatch_get_main_queue(), ^{
for (FIRMessagingTopicOperationCompletion handler in handlers) {
handler(error);
}
[handlers removeAllObjects];
});
}
[self.currentBatch.topics removeObject:topic];
[self.currentBatch.topicHandlers removeObjectForKey:topic];
if (self.currentBatch.topics.count == 0) {
// All topic updates successfully finished in this batch, move on
// to the next batch
[self.topicBatches removeObject:self.currentBatch];
self.currentBatch = nil;
}
[self.delegate pendingTopicsListDidUpdate:self];
FIRMessaging_WEAKIFY(self);
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),
^{
FIRMessaging_STRONGIFY(self);
[self resumeOperationsIfNeeded];
});
}
}
});
}];
}
@end

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@interface FIRMessagingPersistentSyncMessage : NSObject
@property(nonatomic, readonly, strong) NSString *rmqID;
@property(nonatomic, readwrite, assign) BOOL apnsReceived;
@property(nonatomic, readwrite, assign) BOOL mcsReceived;
@property(nonatomic, readonly, assign) int64_t expirationTime;
- (instancetype)initWithRMQID:(NSString *)rmqID expirationTime:(int64_t)expirationTime;
@end

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/FIRMessagingPersistentSyncMessage.h"
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
@interface FIRMessagingPersistentSyncMessage ()
@property(nonatomic, readwrite, strong) NSString *rmqID;
@property(nonatomic, readwrite, assign) int64_t expirationTime;
@end
@implementation FIRMessagingPersistentSyncMessage
- (instancetype)init {
FIRMessagingInvalidateInitializer();
}
- (instancetype)initWithRMQID:(NSString *)rmqID expirationTime:(int64_t)expirationTime {
self = [super init];
if (self) {
_rmqID = [rmqID copy];
_expirationTime = expirationTime;
}
return self;
}
- (NSString *)description {
NSString *classDescription = NSStringFromClass([self class]);
NSDate *date = [NSDate dateWithTimeIntervalSince1970:self.expirationTime];
return
[NSString stringWithFormat:@"%@: (rmqID: %@, apns: %d, mcs: %d, expiry: %@", classDescription,
self.rmqID, self.mcsReceived, self.apnsReceived, date];
}
- (NSString *)debugDescription {
return [self description];
}
@end

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h"
NS_ASSUME_NONNULL_BEGIN
@class FIRMessagingTokenManager;
/**
* FIRMessagingPubSub provides a publish-subscribe model for sending FIRMessaging topic messages.
*
* An app can subscribe to different topics defined by the
* developer. The app server can then send messages to the subscribed devices
* without having to maintain topic-subscribers mapping. Topics do not
* need to be explicitly created before subscribing or publishing&mdash;they
* are automatically created when publishing or subscribing.
*
* Messages published to the topic will be received as regular FIRMessaging messages
* with `"from"` set to `"/topics/myTopic"`.
*
* Only topic names that match the pattern `"/topics/[a-zA-Z0-9-_.~%]{1,900}"`
* are allowed for subscribing and publishing.
*/
@interface FIRMessagingPubSub : NSObject
- (instancetype)initWithTokenManager:(FIRMessagingTokenManager *)tokenManager;
/**
* Subscribes an app instance to a topic, enabling it to receive messages
* sent to that topic.
*
* This is an asynchronous call. If subscription fails, FIRMessaging
* invokes the completion callback with the appropriate error.
*
* @see FIRMessagingPubSub unsubscribeWithToken:topic:handler:
*
* @param token The registration token as received from the InstanceID
* library for a given `authorizedEntity` and "gcm" scope.
* @param topic The topic to subscribe to. Should be of the form
* `"/topics/<topic-name>"`.
* @param options Unused parameter, please pass nil or empty dictionary.
* @param handler The callback handler invoked when the subscribe call
* ends. In case of success, a nil error is returned. Otherwise,
* an appropriate error object is returned.
* @discussion This method is thread-safe. However, it is not guaranteed to
* return on the main thread.
*/
- (void)subscribeWithToken:(NSString *)token
topic:(NSString *)topic
options:(nullable NSDictionary *)options
handler:(FIRMessagingTopicOperationCompletion)handler;
/**
* Unsubscribes an app instance from a topic, stopping it from receiving
* any further messages sent to that topic.
*
* This is an asynchronous call. If the attempt to unsubscribe fails,
* we invoke the `completion` callback passed in with an appropriate error.
*
* @param token The token used to subscribe to this topic.
* @param topic The topic to unsubscribe from. Should be of the form
* `"/topics/<topic-name>"`.
* @param options Unused parameter, please pass nil or empty dictionary.
* @param handler The handler that is invoked once the unsubscribe call ends.
* In case of success, nil error is returned. Otherwise, an
* appropriate error object is returned.
* @discussion This method is thread-safe. However, it is not guaranteed to
* return on the main thread.
*/
- (void)unsubscribeWithToken:(NSString *)token
topic:(NSString *)topic
options:(nullable NSDictionary *)options
handler:(FIRMessagingTopicOperationCompletion)handler;
/**
* Asynchronously subscribe to the topic. Adds to the pending list of topic operations.
* Retry in case of failures. This makes a repeated attempt to subscribe to the topic
* as compared to the `subscribe` method above which tries once.
*
* @param topic The topic name to subscribe to. Should be of the form `"/topics/<topic-name>"`.
* @param handler The handler that is invoked once the unsubscribe call ends.
* In case of success, nil error is returned. Otherwise, an
* appropriate error object is returned.
*/
- (void)subscribeToTopic:(NSString *)topic
handler:(nullable FIRMessagingTopicOperationCompletion)handler;
/**
* Asynchronously unsubscribe from the topic. Adds to the pending list of topic operations.
* Retry in case of failures. This makes a repeated attempt to unsubscribe from the topic
* as compared to the `unsubscribe` method above which tries once.
*
* @param topic The topic name to unsubscribe from. Should be of the form `"/topics/<topic-name>"`.
* @param handler The handler that is invoked once the unsubscribe call ends.
* In case of success, nil error is returned. Otherwise, an
* appropriate error object is returned.
*/
- (void)unsubscribeFromTopic:(NSString *)topic
handler:(nullable FIRMessagingTopicOperationCompletion)handler;
/**
* Schedule subscriptions sync.
*
* @param immediately YES if the sync should be scheduled immediately else NO if we can delay
* the sync.
*/
- (void)scheduleSync:(BOOL)immediately;
/**
* Adds the "/topics/" prefix to the topic.
*
* @param topic The topic to add the prefix to.
*
* @return The new topic name with the "/topics/" prefix added.
*/
+ (NSString *)addPrefixToTopic:(NSString *)topic;
/**
* Removes the "/topics/" prefix from the topic.
*
* @param topic The topic to remove the prefix from.
*
* @return The new topic name with the "/topics/" prefix removed.
*/
+ (NSString *)removePrefixFromTopic:(NSString *)topic;
/**
* Check if the topic name has "/topics/" prefix.
*
* @param topic The topic name to verify.
*
* @return YES if the topic name has "/topics/" prefix else NO.
*/
+ (BOOL)hasTopicsPrefix:(NSString *)topic;
/**
* Check if it's a valid topic name. This includes "/topics/" prefix in the topic name.
*
* @param topic The topic name to verify.
*
* @return YES if the topic name satisfies the regex "/topics/[a-zA-Z0-9-_.~%]{1,900}".
*/
+ (BOOL)isValidTopicWithPrefix:(NSString *)topic;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,327 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/FIRMessagingPubSub.h"
#import <GoogleUtilities/GULSecureCoding.h>
#import <GoogleUtilities/GULUserDefaults.h>
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingPendingTopicsList.h"
#import "FirebaseMessaging/Sources/FIRMessagingTopicOperation.h"
#import "FirebaseMessaging/Sources/FIRMessagingTopicsCommon.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import "FirebaseMessaging/Sources/FIRMessaging_Private.h"
#import "FirebaseMessaging/Sources/NSDictionary+FIRMessaging.h"
#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenManager.h"
static NSString *const kPendingSubscriptionsListKey =
@"com.firebase.messaging.pending-subscriptions";
@interface FIRMessagingPubSub () <FIRMessagingPendingTopicsListDelegate>
@property(nonatomic, readwrite, strong) FIRMessagingPendingTopicsList *pendingTopicUpdates;
@property(nonatomic, readonly, strong) NSOperationQueue *topicOperations;
// Common errors, instantiated, to avoid generating multiple copies
@property(nonatomic, readwrite, strong) NSError *operationInProgressError;
@property(nonatomic, readwrite, strong) FIRMessagingTokenManager *tokenManager;
@end
@implementation FIRMessagingPubSub
- (instancetype)initWithTokenManager:(FIRMessagingTokenManager *)tokenManager {
self = [super init];
if (self) {
_topicOperations = [[NSOperationQueue alloc] init];
// Do 10 topic operations at a time; it's enough to keep the TCP connection to the host alive,
// saving hundreds of milliseconds on each request (compared to a serial queue).
_topicOperations.maxConcurrentOperationCount = 10;
_tokenManager = tokenManager;
[self restorePendingTopicsList];
}
return self;
}
- (void)subscribeWithToken:(NSString *)token
topic:(NSString *)topic
options:(NSDictionary *)options
handler:(FIRMessagingTopicOperationCompletion)handler {
token = [token copy];
topic = [topic copy];
if (![options count]) {
options = @{};
}
if (![[self class] isValidTopicWithPrefix:topic]) {
NSString *failureReason =
[NSString stringWithFormat:@"Invalid subscription topic :'%@'", topic];
FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub000, @"%@", failureReason);
handler([NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidTopicName
failureReason:failureReason]);
return;
}
if (![self verifyPubSubOptions:options]) {
// we do not want to quit even if options have some invalid values.
FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub001,
@"Invalid options passed to FIRMessagingPubSub with non-string keys or "
"values.");
}
// copy the dictionary would trim non-string keys or values if any.
options = [options fcm_trimNonStringValues];
[self updateSubscriptionWithToken:token
topic:topic
options:options
shouldDelete:NO
handler:handler];
}
- (void)dealloc {
[self.topicOperations cancelAllOperations];
}
#pragma mark - FIRMessaging subscribe
- (void)updateSubscriptionWithToken:(NSString *)token
topic:(NSString *)topic
options:(NSDictionary *)options
shouldDelete:(BOOL)shouldDelete
handler:(FIRMessagingTopicOperationCompletion)handler {
if ([_tokenManager hasValidCheckinInfo]) {
FIRMessagingTopicAction action =
shouldDelete ? FIRMessagingTopicActionUnsubscribe : FIRMessagingTopicActionSubscribe;
FIRMessagingTopicOperation *operation = [[FIRMessagingTopicOperation alloc]
initWithTopic:topic
action:action
tokenManager:_tokenManager
options:options
completion:^(NSError *_Nullable error) {
if (error) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeClient001,
@"Failed to subscribe to topic %@", error);
} else {
if (shouldDelete) {
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient002,
@"Successfully unsubscribed from topic %@", topic);
} else {
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient003,
@"Successfully subscribed to topic %@", topic);
}
}
if (handler) {
handler(error);
}
}];
[self.topicOperations addOperation:operation];
} else {
NSString *failureReason = @"Device ID and checkin info is not found. Will not proceed with "
@"subscription/unsubscription.";
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRegistrar000, @"%@", failureReason);
NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeMissingDeviceID
failureReason:failureReason];
handler(error);
}
}
- (void)unsubscribeWithToken:(NSString *)token
topic:(NSString *)topic
options:(NSDictionary *)options
handler:(FIRMessagingTopicOperationCompletion)handler {
token = [token copy];
topic = [topic copy];
if (![options count]) {
options = @{};
}
if (![[self class] isValidTopicWithPrefix:topic]) {
NSString *failureReason =
[NSString stringWithFormat:@"Invalid topic name : '%@' for unsubscription.", topic];
FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub002, @"%@", failureReason);
handler([NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidTopicName
failureReason:failureReason]);
return;
}
if (![self verifyPubSubOptions:options]) {
// we do not want to quit even if options have some invalid values.
FIRMessagingLoggerError(
kFIRMessagingMessageCodePubSub003,
@"Invalid options passed to FIRMessagingPubSub with non-string keys or values.");
}
// copy the dictionary would trim non-string keys or values if any.
options = [options fcm_trimNonStringValues];
[self updateSubscriptionWithToken:token
topic:topic
options:options
shouldDelete:YES
handler:^void(NSError *error) {
handler(error);
}];
}
- (void)subscribeToTopic:(NSString *)topic
handler:(nullable FIRMessagingTopicOperationCompletion)handler {
[self.pendingTopicUpdates addOperationForTopic:topic
withAction:FIRMessagingTopicActionSubscribe
completion:handler];
}
- (void)unsubscribeFromTopic:(NSString *)topic
handler:(nullable FIRMessagingTopicOperationCompletion)handler {
[self.pendingTopicUpdates addOperationForTopic:topic
withAction:FIRMessagingTopicActionUnsubscribe
completion:handler];
}
- (void)scheduleSync:(BOOL)immediately {
NSString *fcmToken = _tokenManager.defaultFCMToken;
if (fcmToken.length) {
[self.pendingTopicUpdates resumeOperationsIfNeeded];
}
}
#pragma mark - FIRMessagingPendingTopicsListDelegate
- (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
requestedUpdateForTopic:(NSString *)topic
action:(FIRMessagingTopicAction)action
completion:(FIRMessagingTopicOperationCompletion)completion {
NSString *fcmToken = _tokenManager.defaultFCMToken;
if (action == FIRMessagingTopicActionSubscribe) {
[self subscribeWithToken:fcmToken topic:topic options:nil handler:completion];
} else {
[self unsubscribeWithToken:fcmToken topic:topic options:nil handler:completion];
}
}
- (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list {
[self archivePendingTopicsList:list];
}
- (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list {
NSString *fcmToken = _tokenManager.defaultFCMToken;
return (fcmToken.length > 0);
}
#pragma mark - Storing Pending Topics
- (void)archivePendingTopicsList:(FIRMessagingPendingTopicsList *)topicsList {
GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults];
NSError *error;
NSData *pendingData = [GULSecureCoding archivedDataWithRootObject:topicsList error:&error];
if (error) {
FIRMessagingLoggerError(kFIRMessagingMessageCodePubSubArchiveError,
@"Failed to archive topic list data %@", error);
return;
}
[defaults setObject:pendingData forKey:kPendingSubscriptionsListKey];
[defaults synchronize];
}
- (void)restorePendingTopicsList {
GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults];
NSData *pendingData = [defaults objectForKey:kPendingSubscriptionsListKey];
FIRMessagingPendingTopicsList *subscriptions;
if (pendingData) {
NSError *error;
subscriptions = [GULSecureCoding
unarchivedObjectOfClasses:[NSSet setWithObjects:FIRMessagingPendingTopicsList.class, nil]
fromData:pendingData
error:&error];
if (error) {
FIRMessagingLoggerError(kFIRMessagingMessageCodePubSubUnarchiveError,
@"Failed to unarchive topic list data %@", error);
}
}
if (subscriptions) {
self.pendingTopicUpdates = subscriptions;
} else {
self.pendingTopicUpdates = [[FIRMessagingPendingTopicsList alloc] init];
}
self.pendingTopicUpdates.delegate = self;
}
#pragma mark - Private Helpers
- (BOOL)verifyPubSubOptions:(NSDictionary *)options {
return ![options fcm_hasNonStringKeysOrValues];
}
#pragma mark - Topic Name Helpers
static NSString *const kTopicsPrefix = @"/topics/";
static NSString *const kTopicRegexPattern = @"/topics/([a-zA-Z0-9-_.~%]+)";
+ (NSString *)addPrefixToTopic:(NSString *)topic {
if (![self hasTopicsPrefix:topic]) {
return [NSString stringWithFormat:@"%@%@", kTopicsPrefix, topic];
} else {
return [topic copy];
}
}
+ (NSString *)removePrefixFromTopic:(NSString *)topic {
if ([self hasTopicsPrefix:topic]) {
return [topic substringFromIndex:kTopicsPrefix.length];
} else {
return [topic copy];
}
}
+ (BOOL)hasTopicsPrefix:(NSString *)topic {
return [topic hasPrefix:kTopicsPrefix];
}
/**
* Returns a regular expression for matching a topic sender.
*
* @return The topic matching regular expression
*/
+ (NSRegularExpression *)topicRegex {
// Since this is a static regex pattern, we only only need to declare it once.
static NSRegularExpression *topicRegex;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSError *error;
topicRegex =
[NSRegularExpression regularExpressionWithPattern:kTopicRegexPattern
options:NSRegularExpressionAnchorsMatchLines
error:&error];
});
return topicRegex;
}
/**
* Gets the class describing occurences of topic names and sender IDs in the sender.
*
* @param topic The topic expression used to generate a pubsub topic
*
* @return Representation of captured subexpressions in topic regular expression
*/
+ (BOOL)isValidTopicWithPrefix:(NSString *)topic {
NSRange topicRange = NSMakeRange(0, topic.length);
NSRange regexMatchRange = [[self topicRegex] rangeOfFirstMatchInString:topic
options:NSMatchingAnchored
range:topicRange];
return NSEqualRanges(topicRange, regexMatchRange);
}
@end

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
/**
* Swizzle remote-notification callbacks to invoke FIRMessaging methods
* before calling original implementations.
*/
@interface FIRMessagingRemoteNotificationsProxy : NSObject
/**
* Checks the `FirebaseAppDelegateProxyEnabled` key in the App's Info.plist. If the key is
* missing or incorrectly formatted, returns `YES`.
*
* @return YES if the Application Delegate and User Notification Center methods can be swizzled.
* Otherwise, returns NO.
*/
+ (BOOL)canSwizzleMethods;
/**
* A shared instance of `FIRMessagingRemoteNotificationsProxy`
*/
+ (instancetype)sharedProxy;
/**
* Swizzles Application Delegate's remote-notification callbacks and User Notification Center
* delegate callback, and invokes the original selectors once done.
*/
- (void)swizzleMethodsIfPossible;
@end

View File

@@ -0,0 +1,592 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/FIRMessagingRemoteNotificationsProxy.h"
#import <objc/runtime.h>
#import <GoogleUtilities/GULAppDelegateSwizzler.h>
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import "FirebaseMessaging/Sources/FIRMessaging_Private.h"
static void *UserNotificationObserverContext = &UserNotificationObserverContext;
static NSString *kUserNotificationWillPresentSelectorString =
@"userNotificationCenter:willPresentNotification:withCompletionHandler:";
static NSString *kUserNotificationDidReceiveResponseSelectorString =
@"userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:";
@interface FIRMessagingRemoteNotificationsProxy () <GULApplicationDelegate>
@property(strong, nonatomic) NSMutableDictionary<NSString *, NSValue *> *originalAppDelegateImps;
@property(strong, nonatomic) NSMutableDictionary<NSString *, NSArray *> *swizzledSelectorsByClass;
@property(nonatomic) BOOL didSwizzleMethods;
@property(nonatomic) BOOL hasSwizzledUserNotificationDelegate;
@property(nonatomic) BOOL isObservingUserNotificationDelegateChanges;
@property(strong, nonatomic) id userNotificationCenter;
@property(strong, nonatomic) id currentUserNotificationCenterDelegate;
@property(strong, nonatomic) GULAppDelegateInterceptorID appDelegateInterceptorID;
@end
@implementation FIRMessagingRemoteNotificationsProxy
+ (BOOL)canSwizzleMethods {
return [GULAppDelegateSwizzler isAppDelegateProxyEnabled];
}
+ (instancetype)sharedProxy {
static FIRMessagingRemoteNotificationsProxy *proxy;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
proxy = [[FIRMessagingRemoteNotificationsProxy alloc] init];
});
return proxy;
}
- (instancetype)init {
self = [super init];
if (self) {
_originalAppDelegateImps = [[NSMutableDictionary alloc] init];
_swizzledSelectorsByClass = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)dealloc {
[self unswizzleAllMethods];
self.swizzledSelectorsByClass = nil;
[self.originalAppDelegateImps removeAllObjects];
self.originalAppDelegateImps = nil;
[self removeUserNotificationCenterDelegateObserver];
}
- (void)swizzleMethodsIfPossible {
// Already swizzled.
if (self.didSwizzleMethods) {
return;
}
[GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods];
self.appDelegateInterceptorID = [GULAppDelegateSwizzler registerAppDelegateInterceptor:self];
// Add KVO listener on [UNUserNotificationCenter currentNotificationCenter]'s delegate property
Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
if (notificationCenterClass) {
// We are linked against iOS 10 SDK or above
id notificationCenter = FIRMessagingPropertyNameFromObject(
notificationCenterClass, @"currentNotificationCenter", notificationCenterClass);
if (notificationCenter) {
[self listenForDelegateChangesInUserNotificationCenter:notificationCenter];
}
}
self.didSwizzleMethods = YES;
}
- (void)unswizzleAllMethods {
if (self.appDelegateInterceptorID) {
[GULAppDelegateSwizzler unregisterAppDelegateInterceptorWithID:self.appDelegateInterceptorID];
}
for (NSString *className in self.swizzledSelectorsByClass) {
Class klass = NSClassFromString(className);
NSArray *selectorStrings = self.swizzledSelectorsByClass[className];
for (NSString *selectorString in selectorStrings) {
SEL selector = NSSelectorFromString(selectorString);
[self unswizzleSelector:selector inClass:klass];
}
}
[self.swizzledSelectorsByClass removeAllObjects];
}
- (void)listenForDelegateChangesInUserNotificationCenter:(id)notificationCenter {
Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
if (![notificationCenter isKindOfClass:notificationCenterClass]) {
return;
}
id delegate = FIRMessagingPropertyNameFromObject(notificationCenter, @"delegate", nil);
Protocol *delegateProtocol = NSProtocolFromString(@"UNUserNotificationCenterDelegate");
if ([delegate conformsToProtocol:delegateProtocol]) {
// Swizzle this object now, if available
[self swizzleUserNotificationCenterDelegate:delegate];
}
// Add KVO observer for "delegate" keyPath for future changes
[self addDelegateObserverToUserNotificationCenter:notificationCenter];
}
#pragma mark - UNNotificationCenter Swizzling
- (void)swizzleUserNotificationCenterDelegate:(id _Nonnull)delegate {
if (self.currentUserNotificationCenterDelegate == delegate) {
// Via pointer-check, compare if we have already swizzled this item.
return;
}
Protocol *userNotificationCenterProtocol =
NSProtocolFromString(@"UNUserNotificationCenterDelegate");
if ([delegate conformsToProtocol:userNotificationCenterProtocol]) {
SEL willPresentNotificationSelector =
NSSelectorFromString(kUserNotificationWillPresentSelectorString);
// Swizzle the optional method
// "userNotificationCenter:willPresentNotification:withCompletionHandler:", if it is
// implemented. Do not swizzle otherwise, as an implementation *will* be created, which will
// fool iOS into thinking that this method is implemented, and therefore not send notifications
// to the fallback method in the app delegate
// "application:didReceiveRemoteNotification:fetchCompletionHandler:".
if ([delegate respondsToSelector:willPresentNotificationSelector]) {
[self swizzleSelector:willPresentNotificationSelector
inClass:[delegate class]
withImplementation:(IMP)FCMSwizzleWillPresentNotificationWithHandler
inProtocol:userNotificationCenterProtocol];
}
SEL didReceiveNotificationResponseSelector =
NSSelectorFromString(kUserNotificationDidReceiveResponseSelectorString);
if ([delegate respondsToSelector:didReceiveNotificationResponseSelector]) {
[self swizzleSelector:didReceiveNotificationResponseSelector
inClass:[delegate class]
withImplementation:(IMP)FCMSwizzleDidReceiveNotificationResponseWithHandler
inProtocol:userNotificationCenterProtocol];
}
self.currentUserNotificationCenterDelegate = delegate;
self.hasSwizzledUserNotificationDelegate = YES;
}
}
- (void)unswizzleUserNotificationCenterDelegate:(id _Nonnull)delegate {
if (self.currentUserNotificationCenterDelegate != delegate) {
// We aren't swizzling this delegate, so don't do anything.
return;
}
SEL willPresentNotificationSelector =
NSSelectorFromString(kUserNotificationWillPresentSelectorString);
// Call unswizzle methods, even if the method was not implemented (it will fail gracefully).
[self unswizzleSelector:willPresentNotificationSelector
inClass:[self.currentUserNotificationCenterDelegate class]];
SEL didReceiveNotificationResponseSelector =
NSSelectorFromString(kUserNotificationDidReceiveResponseSelectorString);
[self unswizzleSelector:didReceiveNotificationResponseSelector
inClass:[self.currentUserNotificationCenterDelegate class]];
self.currentUserNotificationCenterDelegate = nil;
self.hasSwizzledUserNotificationDelegate = NO;
}
#pragma mark - KVO for UNUserNotificationCenter
- (void)addDelegateObserverToUserNotificationCenter:(id)userNotificationCenter {
[self removeUserNotificationCenterDelegateObserver];
@try {
[userNotificationCenter addObserver:self
forKeyPath:NSStringFromSelector(@selector(delegate))
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:UserNotificationObserverContext];
self.userNotificationCenter = userNotificationCenter;
self.isObservingUserNotificationDelegateChanges = YES;
} @catch (NSException *exception) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxy000,
@"Encountered exception trying to add a KVO observer for "
@"UNUserNotificationCenter's 'delegate' property: %@",
exception);
} @finally {
}
}
- (void)removeUserNotificationCenterDelegateObserver {
if (!self.userNotificationCenter) {
return;
}
@try {
[self.userNotificationCenter removeObserver:self
forKeyPath:NSStringFromSelector(@selector(delegate))
context:UserNotificationObserverContext];
self.userNotificationCenter = nil;
self.isObservingUserNotificationDelegateChanges = NO;
} @catch (NSException *exception) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxy001,
@"Encountered exception trying to remove a KVO observer for "
@"UNUserNotificationCenter's 'delegate' property: %@",
exception);
} @finally {
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context {
if (context == UserNotificationObserverContext) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(delegate))]) {
id oldDelegate = change[NSKeyValueChangeOldKey];
if (oldDelegate && oldDelegate != [NSNull null]) {
[self unswizzleUserNotificationCenterDelegate:oldDelegate];
}
id newDelegate = change[NSKeyValueChangeNewKey];
if (newDelegate && newDelegate != [NSNull null]) {
[self swizzleUserNotificationCenterDelegate:newDelegate];
}
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark - NSProxy methods
- (void)saveOriginalImplementation:(IMP)imp forSelector:(SEL)selector {
if (imp && selector) {
NSValue *IMPValue = [NSValue valueWithPointer:imp];
NSString *selectorString = NSStringFromSelector(selector);
self.originalAppDelegateImps[selectorString] = IMPValue;
}
}
- (IMP)originalImplementationForSelector:(SEL)selector {
NSString *selectorString = NSStringFromSelector(selector);
NSValue *implementationValue = self.originalAppDelegateImps[selectorString];
if (!implementationValue) {
return nil;
}
IMP imp;
[implementationValue getValue:&imp];
return imp;
}
- (void)trackSwizzledSelector:(SEL)selector ofClass:(Class)klass {
NSString *className = NSStringFromClass(klass);
NSString *selectorString = NSStringFromSelector(selector);
NSArray *selectors = self.swizzledSelectorsByClass[selectorString];
if (selectors) {
selectors = [selectors arrayByAddingObject:selectorString];
} else {
selectors = @[ selectorString ];
}
self.swizzledSelectorsByClass[className] = selectors;
}
- (void)removeImplementationForSelector:(SEL)selector {
NSString *selectorString = NSStringFromSelector(selector);
[self.originalAppDelegateImps removeObjectForKey:selectorString];
}
- (void)swizzleSelector:(SEL)originalSelector
inClass:(Class)klass
withImplementation:(IMP)swizzledImplementation
inProtocol:(Protocol *)protocol {
Method originalMethod = class_getInstanceMethod(klass, originalSelector);
if (originalMethod) {
// This class implements this method, so replace the original implementation
// with our new implementation and save the old implementation.
IMP originalMethodImplementation =
method_setImplementation(originalMethod, swizzledImplementation);
IMP nonexistantMethodImplementation = [self nonExistantMethodImplementationForClass:klass];
if (originalMethodImplementation &&
originalMethodImplementation != nonexistantMethodImplementation &&
originalMethodImplementation != swizzledImplementation) {
[self saveOriginalImplementation:originalMethodImplementation forSelector:originalSelector];
}
} else {
// The class doesn't have this method, so add our swizzled implementation as the
// original implementation of the original method.
struct objc_method_description methodDescription =
protocol_getMethodDescription(protocol, originalSelector, NO, YES);
BOOL methodAdded =
class_addMethod(klass, originalSelector, swizzledImplementation, methodDescription.types);
if (!methodAdded) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxyMethodNotAdded,
@"Could not add method for %@ to class %@",
NSStringFromSelector(originalSelector), NSStringFromClass(klass));
}
}
[self trackSwizzledSelector:originalSelector ofClass:klass];
}
- (void)unswizzleSelector:(SEL)selector inClass:(Class)klass {
Method swizzledMethod = class_getInstanceMethod(klass, selector);
if (!swizzledMethod) {
// This class doesn't seem to have this selector as an instance method? Bail out.
return;
}
IMP originalImp = [self originalImplementationForSelector:selector];
if (originalImp) {
// Restore the original implementation as the current implementation
method_setImplementation(swizzledMethod, originalImp);
[self removeImplementationForSelector:selector];
} else {
// This class originally did not have an implementation for this selector.
// We can't actually remove methods in Objective-C 2.0, but we could set
// its method to something non-existent. This should give us the same
// behavior as if the method was not implemented.
// See: http://stackoverflow.com/a/8276527/9849
IMP nonExistantMethodImplementation = [self nonExistantMethodImplementationForClass:klass];
method_setImplementation(swizzledMethod, nonExistantMethodImplementation);
}
}
#pragma mark - Reflection Helpers
// This is useful to generate from a stable, "known missing" selector, as the IMP can be compared
// in case we are setting an implementation for a class that was previously "unswizzled" into a
// non-existant implementation.
- (IMP)nonExistantMethodImplementationForClass:(Class)klass {
SEL nonExistantSelector = NSSelectorFromString(@"aNonExistantMethod");
IMP nonExistantMethodImplementation = class_getMethodImplementation(klass, nonExistantSelector);
return nonExistantMethodImplementation;
}
// A safe, non-leaky way return a property object by its name
id FIRMessagingPropertyNameFromObject(id object, NSString *propertyName, Class klass) {
SEL selector = NSSelectorFromString(propertyName);
if (![object respondsToSelector:selector]) {
return nil;
}
if (!klass) {
klass = [NSObject class];
}
// Suppress clang warning about leaks in performSelector
// The alternative way to perform this is to invoke
// the method as a block (see http://stackoverflow.com/a/20058585),
// but this approach sometimes returns incomplete objects.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
id property = [object performSelector:selector];
#pragma clang diagnostic pop
if (![property isKindOfClass:klass]) {
return nil;
}
return property;
}
#pragma mark - GULApplicationDelegate
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (void)application:(GULApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo {
[[FIRMessaging messaging] appDidReceiveMessage:userInfo];
}
#pragma clang diagnostic pop
#if TARGET_OS_IOS || TARGET_OS_TV
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
[[FIRMessaging messaging] appDidReceiveMessage:userInfo];
completionHandler(UIBackgroundFetchResultNoData);
}
- (void)application:(UIApplication *)application
didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
// Log the fact that we failed to register for remote notifications
FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxyAPNSFailed,
@"Error in "
@"application:didFailToRegisterForRemoteNotificationsWithError: %@",
error.localizedDescription);
}
#endif
- (void)application:(GULApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
[FIRMessaging messaging].APNSToken = deviceToken;
}
#pragma mark - Swizzled Methods
/**
* Swizzle the notification handler for iOS 10+ devices.
* Signature of original handler is as below:
* - (void)userNotificationCenter:(UNUserNotificationCenter *)center
* willPresentNotification:(UNNotification *)notification
* withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
* In order to make FCM SDK compile and compatible with iOS SDKs before iOS 10, hide the
* parameter types from the swizzling implementation.
*/
static void FCMSwizzleWillPresentNotificationWithHandler(
id self, SEL cmd, id center, id notification, void (^handler)(NSUInteger)) {
FIRMessagingRemoteNotificationsProxy *proxy = [FIRMessagingRemoteNotificationsProxy sharedProxy];
IMP originalImp = [proxy originalImplementationForSelector:cmd];
void (^callOriginalMethodIfAvailable)(void) = ^{
if (originalImp) {
((void (*)(id, SEL, id, id, void (^)(NSUInteger)))originalImp)(self, cmd, center,
notification, handler);
}
return;
};
Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
Class notificationClass = NSClassFromString(@"UNNotification");
if (!notificationCenterClass || !notificationClass) {
// Can't find UserNotifications framework. Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
}
if (!center || ![center isKindOfClass:[notificationCenterClass class]]) {
// Invalid parameter type from the original method.
// Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
return;
}
if (!notification || ![notification isKindOfClass:[notificationClass class]]) {
// Invalid parameter type from the original method.
// Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
return;
}
if (!handler) {
// Invalid parameter type from the original method.
// Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
return;
}
// Attempt to access the user info
id notificationUserInfo = FIRMessagingUserInfoFromNotification(notification);
if (!notificationUserInfo) {
// Could not access notification.request.content.userInfo.
callOriginalMethodIfAvailable();
return;
}
[[FIRMessaging messaging] appDidReceiveMessage:notificationUserInfo];
// Execute the original implementation.
callOriginalMethodIfAvailable();
}
/**
* Swizzle the notification handler for iOS 10+ devices.
* Signature of original handler is as below:
* - (void)userNotificationCenter:(UNUserNotificationCenter *)center
* didReceiveNotificationResponse:(UNNotificationResponse *)response
* withCompletionHandler:(void (^)(void))completionHandler
* In order to make FCM SDK compile and compatible with iOS SDKs before iOS 10, hide the
* parameter types from the swizzling implementation.
*/
static void FCMSwizzleDidReceiveNotificationResponseWithHandler(
id self, SEL cmd, id center, id response, void (^handler)(void)) {
FIRMessagingRemoteNotificationsProxy *proxy = [FIRMessagingRemoteNotificationsProxy sharedProxy];
IMP originalImp = [proxy originalImplementationForSelector:cmd];
void (^callOriginalMethodIfAvailable)(void) = ^{
if (originalImp) {
((void (*)(id, SEL, id, id, void (^)(void)))originalImp)(self, cmd, center, response,
handler);
}
return;
};
Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
Class responseClass = NSClassFromString(@"UNNotificationResponse");
if (!center || ![center isKindOfClass:[notificationCenterClass class]]) {
// Invalid parameter type from the original method.
// Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
return;
}
if (!response || ![response isKindOfClass:[responseClass class]]) {
// Invalid parameter type from the original method.
// Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
return;
}
if (!handler) {
// Invalid parameter type from the original method.
// Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
return;
}
// Try to access the response.notification property
SEL notificationSelector = NSSelectorFromString(@"notification");
if (![response respondsToSelector:notificationSelector]) {
// Cannot access the .notification property.
callOriginalMethodIfAvailable();
return;
}
id notificationClass = NSClassFromString(@"UNNotification");
id notification =
FIRMessagingPropertyNameFromObject(response, @"notification", notificationClass);
// With a notification object, use the common code to reach deep into notification
// (notification.request.content.userInfo)
id notificationUserInfo = FIRMessagingUserInfoFromNotification(notification);
if (!notificationUserInfo) {
// Could not access notification.request.content.userInfo.
callOriginalMethodIfAvailable();
return;
}
[[FIRMessaging messaging] appDidReceiveMessage:notificationUserInfo];
// Execute the original implementation.
callOriginalMethodIfAvailable();
}
static id FIRMessagingUserInfoFromNotification(id notification) {
// Select the userInfo field from UNNotification.request.content.userInfo.
SEL requestSelector = NSSelectorFromString(@"request");
if (![notification respondsToSelector:requestSelector]) {
// Cannot access the request property.
return nil;
}
Class requestClass = NSClassFromString(@"UNNotificationRequest");
id notificationRequest =
FIRMessagingPropertyNameFromObject(notification, @"request", requestClass);
SEL notificationContentSelector = NSSelectorFromString(@"content");
if (!notificationRequest ||
![notificationRequest respondsToSelector:notificationContentSelector]) {
// Cannot access the content property.
return nil;
}
Class contentClass = NSClassFromString(@"UNNotificationContent");
id notificationContent =
FIRMessagingPropertyNameFromObject(notificationRequest, @"content", contentClass);
SEL notificationUserInfoSelector = NSSelectorFromString(@"userInfo");
if (!notificationContent ||
![notificationContent respondsToSelector:notificationUserInfoSelector]) {
// Cannot access the userInfo property.
return nil;
}
id notificationUserInfo =
FIRMessagingPropertyNameFromObject(notificationContent, @"userInfo", [NSDictionary class]);
if (!notificationUserInfo) {
// This is not the expected notification handler.
return nil;
}
return notificationUserInfo;
}
@end

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class FIRMessagingPersistentSyncMessage;
/**
* This manages the RMQ persistent store.
*
* The store is used to store all the S2D id's that were received by the client and were ACK'ed
* by us but the server hasn't confirmed the ACK. We don't delete these id's until the server
* ACK's us that they have received them.
*
* We also store the upstream messages(d2s) that were sent by the client.
*
* Also store the lastRMQId that was sent by us so that for a new connection being setup we don't
* duplicate RMQ Id's for the new messages.
*/
@interface FIRMessagingRmqManager : NSObject
// designated initializer
- (instancetype)initWithDatabaseName:(NSString *)databaseName;
- (void)loadRmqId;
/**
* Save Server to device message with the given RMQ-ID.
*
* @param rmqID The rmqID of the s2d message to save.
*
*/
- (void)saveS2dMessageWithRmqId:(NSString *)rmqID;
#pragma mark - Sync Messages
/**
* Get persisted sync message with rmqID.
*
* @param rmqID The rmqID of the persisted sync message.
*
* @return A valid persistent sync message with the given rmqID if found in the RMQ else nil.
*/
- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID;
/**
* Delete the expired sync messages from persisten store. Also deletes messages that have been
* delivered both via APNS and MCS.
*/
- (void)deleteExpiredOrFinishedSyncMessages;
/**
* Save sync message received by the device.
*
* @param rmqID The rmqID of the message received.
* @param expirationTime The expiration time of the sync message received.
*
*/
- (void)saveSyncMessageWithRmqID:(NSString *)rmqID expirationTime:(int64_t)expirationTime;
/**
* Update sync message received via APNS.
*
* @param rmqID The rmqID of the received message.
*
*/
- (void)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID;
/**
* Returns path for database with specified name.
* @param databaseName The database name without extension: "<databaseName>.sqlite".
* @return Path to the database with the specified name.
*/
+ (NSString *)pathForDatabaseWithName:(NSString *)databaseName;
@end

View File

@@ -0,0 +1,685 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/FIRMessagingRmqManager.h"
#import <sqlite3.h>
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingPersistentSyncMessage.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
#ifndef _FIRMessagingRmqLogAndExit
#define _FIRMessagingRmqLogAndExit(stmt, return_value) \
do { \
[self logErrorAndFinalizeStatement:stmt]; \
return return_value; \
} while (0)
#endif
#ifndef FIRMessagingRmqLogAndReturn
#define FIRMessagingRmqLogAndReturn(stmt) \
do { \
[self logErrorAndFinalizeStatement:stmt]; \
return; \
} while (0)
#endif
#ifndef FIRMessaging_MUST_NOT_BE_MAIN_THREAD
#define FIRMessaging_MUST_NOT_BE_MAIN_THREAD() \
do { \
NSAssert(![NSThread isMainThread], @"Must not be executing on the main thread."); \
} while (0);
#endif
// table names
NSString *const kTableOutgoingRmqMessages = @"outgoingRmqMessages";
NSString *const kTableLastRmqId = @"lastrmqid";
NSString *const kOldTableS2DRmqIds = @"s2dRmqIds";
NSString *const kTableS2DRmqIds = @"s2dRmqIds_1";
// Used to prevent de-duping of sync messages received both via APNS and MCS.
NSString *const kTableSyncMessages = @"incomingSyncMessages";
static NSString *const kTablePrefix = @"";
// create tables
static NSString *const kCreateTableOutgoingRmqMessages = @"create TABLE IF NOT EXISTS %@%@ "
@"(_id INTEGER PRIMARY KEY, "
@"rmq_id INTEGER, "
@"type INTEGER, "
@"ts INTEGER, "
@"data BLOB)";
static NSString *const kCreateTableLastRmqId = @"create TABLE IF NOT EXISTS %@%@ "
@"(_id INTEGER PRIMARY KEY, "
@"rmq_id INTEGER)";
static NSString *const kCreateTableS2DRmqIds = @"create TABLE IF NOT EXISTS %@%@ "
@"(_id INTEGER PRIMARY KEY, "
@"rmq_id TEXT)";
static NSString *const kCreateTableSyncMessages = @"create TABLE IF NOT EXISTS %@%@ "
@"(_id INTEGER PRIMARY KEY, "
@"rmq_id TEXT, "
@"expiration_ts INTEGER, "
@"apns_recv INTEGER, "
@"mcs_recv INTEGER)";
static NSString *const kDropTableCommand = @"drop TABLE if exists %@%@";
// table infos
static NSString *const kRmqIdColumn = @"rmq_id";
static NSString *const kDataColumn = @"data";
static NSString *const kProtobufTagColumn = @"type";
static NSString *const kIdColumn = @"_id";
static NSString *const kOutgoingRmqMessagesColumns = @"rmq_id, type, data";
// Sync message columns
static NSString *const kSyncMessagesColumns = @"rmq_id, expiration_ts, apns_recv, mcs_recv";
// Message time expiration in seconds since 1970
static NSString *const kSyncMessageExpirationTimestampColumn = @"expiration_ts";
static NSString *const kSyncMessageAPNSReceivedColumn = @"apns_recv";
static NSString *const kSyncMessageMCSReceivedColumn = @"mcs_recv";
// Utility to create an NSString from a sqlite3 result code
NSString *_Nonnull FIRMessagingStringFromSQLiteResult(int result) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
const char *errorStr = sqlite3_errstr(result);
#pragma clang diagnostic pop
NSString *errorString = [NSString stringWithFormat:@"%d - %s", result, errorStr];
return errorString;
}
@interface FIRMessagingRmqManager () {
sqlite3 *_database;
/// Serial queue for database read/write operations.
dispatch_queue_t _databaseOperationQueue;
}
@property(nonatomic, readwrite, strong) NSString *databaseName;
// map the category of an outgoing message with the number of messages for that category
// should always have two keys -- the app, gcm
@property(nonatomic, readwrite, strong) NSMutableDictionary *outstandingMessages;
// Outgoing RMQ persistent id
@property(nonatomic, readwrite, assign) int64_t rmqId;
@end
@implementation FIRMessagingRmqManager
- (instancetype)initWithDatabaseName:(NSString *)databaseName {
self = [super init];
if (self) {
_databaseOperationQueue =
dispatch_queue_create("com.google.firebase.messaging.database.rmq", DISPATCH_QUEUE_SERIAL);
_databaseName = [databaseName copy];
[self openDatabase];
_outstandingMessages = [NSMutableDictionary dictionaryWithCapacity:2];
_rmqId = -1;
}
return self;
}
- (void)dealloc {
sqlite3_close(_database);
}
#pragma mark - RMQ ID
- (void)loadRmqId {
if (self.rmqId >= 0) {
return; // already done
}
[self loadInitialOutgoingPersistentId];
if (self.outstandingMessages.count) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmqManager000, @"Outstanding categories %ld",
_FIRMessaging_UL(self.outstandingMessages.count));
}
}
/**
* Initialize the 'initial RMQ':
* - max ID of any message in the queue
* - if the queue is empty, stored value in separate DB.
*
* Stream acks will remove from RMQ, when we remove the highest message we keep track
* of its ID.
*/
- (void)loadInitialOutgoingPersistentId {
// we shouldn't always trust the lastRmqId stored in the LastRmqId table, because
// we only save to the LastRmqId table once in a while (after getting the lastRmqId sent
// by the server after reconnect, and after getting a rmq ack from the server). The
// rmq message with the highest rmq id tells the real story, so check against that first.
__block int64_t rmqId;
dispatch_sync(_databaseOperationQueue, ^{
rmqId = [self queryHighestRmqId];
});
if (rmqId == 0) {
dispatch_sync(_databaseOperationQueue, ^{
rmqId = [self queryLastRmqId];
});
}
self.rmqId = rmqId + 1;
}
/**
* This is called when we delete the largest outgoing message from queue.
*/
- (void)saveLastOutgoingRmqId:(int64_t)rmqID {
dispatch_async(_databaseOperationQueue, ^{
NSString *queryFormat = @"INSERT OR REPLACE INTO %@ (%@, %@) VALUES (?, ?)";
NSString *query = [NSString stringWithFormat:queryFormat,
kTableLastRmqId, // table
kIdColumn, kRmqIdColumn]; // columns
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(self->_database, [query UTF8String], -1, &statement, NULL) !=
SQLITE_OK) {
FIRMessagingRmqLogAndReturn(statement);
}
if (sqlite3_bind_int(statement, 1, 1) != SQLITE_OK) {
FIRMessagingRmqLogAndReturn(statement);
}
if (sqlite3_bind_int64(statement, 2, rmqID) != SQLITE_OK) {
FIRMessagingRmqLogAndReturn(statement);
}
if (sqlite3_step(statement) != SQLITE_DONE) {
FIRMessagingRmqLogAndReturn(statement);
}
sqlite3_finalize(statement);
});
}
- (void)saveS2dMessageWithRmqId:(NSString *)rmqId {
dispatch_async(_databaseOperationQueue, ^{
NSString *insertFormat = @"INSERT INTO %@ (%@) VALUES (?)";
NSString *insertSQL = [NSString stringWithFormat:insertFormat, kTableS2DRmqIds, kRmqIdColumn];
sqlite3_stmt *insert_statement;
if (sqlite3_prepare_v2(self->_database, [insertSQL UTF8String], -1, &insert_statement, NULL) !=
SQLITE_OK) {
FIRMessagingRmqLogAndReturn(insert_statement);
}
if (sqlite3_bind_text(insert_statement, 1, [rmqId UTF8String], (int)[rmqId length],
SQLITE_STATIC) != SQLITE_OK) {
FIRMessagingRmqLogAndReturn(insert_statement);
}
if (sqlite3_step(insert_statement) != SQLITE_DONE) {
FIRMessagingRmqLogAndReturn(insert_statement);
}
sqlite3_finalize(insert_statement);
});
}
#pragma mark - Query
- (int64_t)queryHighestRmqId {
NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
NSString *query = [NSString stringWithFormat:queryFormat,
kRmqIdColumn, // column
kTableOutgoingRmqMessages, // table
kRmqIdColumn, // order by column
1]; // limit
sqlite3_stmt *statement;
int64_t highestRmqId = 0;
if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(statement, highestRmqId);
}
if (sqlite3_step(statement) == SQLITE_ROW) {
highestRmqId = sqlite3_column_int64(statement, 0);
}
sqlite3_finalize(statement);
return highestRmqId;
}
- (int64_t)queryLastRmqId {
NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
NSString *query = [NSString stringWithFormat:queryFormat,
kRmqIdColumn, // column
kTableLastRmqId, // table
kRmqIdColumn, // order by column
1]; // limit
sqlite3_stmt *statement;
int64_t lastRmqId = 0;
if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(statement, lastRmqId);
}
if (sqlite3_step(statement) == SQLITE_ROW) {
lastRmqId = sqlite3_column_int64(statement, 0);
}
sqlite3_finalize(statement);
return lastRmqId;
}
#pragma mark - Sync Messages
- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID {
__block FIRMessagingPersistentSyncMessage *persistentMessage;
dispatch_sync(_databaseOperationQueue, ^{
NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ = '%@'";
NSString *query =
[NSString stringWithFormat:queryFormat,
kSyncMessagesColumns, // SELECT (rmq_id, expiration_ts,
// apns_recv, mcs_recv)
kTableSyncMessages, // FROM sync_rmq
kRmqIdColumn, // WHERE rmq_id
rmqID];
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(self->_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
[self logError];
sqlite3_finalize(stmt);
return;
}
const int rmqIDColumn = 0;
const int expirationTimestampColumn = 1;
const int apnsReceivedColumn = 2;
const int mcsReceivedColumn = 3;
int count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
NSString *rmqID =
[NSString stringWithUTF8String:(char *)sqlite3_column_text(stmt, rmqIDColumn)];
int64_t expirationTimestamp = sqlite3_column_int64(stmt, expirationTimestampColumn);
BOOL apnsReceived = sqlite3_column_int(stmt, apnsReceivedColumn);
BOOL mcsReceived = sqlite3_column_int(stmt, mcsReceivedColumn);
// create a new persistent message
persistentMessage =
[[FIRMessagingPersistentSyncMessage alloc] initWithRMQID:rmqID
expirationTime:expirationTimestamp];
persistentMessage.apnsReceived = apnsReceived;
persistentMessage.mcsReceived = mcsReceived;
count++;
}
sqlite3_finalize(stmt);
});
return persistentMessage;
}
- (void)deleteExpiredOrFinishedSyncMessages {
dispatch_async(_databaseOperationQueue, ^{
int64_t now = FIRMessagingCurrentTimestampInSeconds();
NSString *deleteSQL = @"DELETE FROM %@ "
@"WHERE %@ < %lld OR " // expirationTime < now
@"(%@ = 1 AND %@ = 1)"; // apns_received = 1 AND mcs_received = 1
NSString *query = [NSString
stringWithFormat:deleteSQL, kTableSyncMessages, kSyncMessageExpirationTimestampColumn, now,
kSyncMessageAPNSReceivedColumn, kSyncMessageMCSReceivedColumn];
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(self->_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
FIRMessagingRmqLogAndReturn(stmt);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
FIRMessagingRmqLogAndReturn(stmt);
}
sqlite3_finalize(stmt);
int deleteCount = sqlite3_changes(self->_database);
if (deleteCount > 0) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSyncMessageManager001,
@"Successfully deleted %d sync messages from store", deleteCount);
}
});
}
- (void)saveSyncMessageWithRmqID:(NSString *)rmqID expirationTime:(int64_t)expirationTime {
BOOL apnsReceived = YES;
BOOL mcsReceived = NO;
dispatch_async(_databaseOperationQueue, ^{
NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@, %@) VALUES (?, ?, ?, ?)";
NSString *insertSQL =
[NSString stringWithFormat:insertFormat,
kTableSyncMessages, // Table name
kRmqIdColumn, // rmq_id
kSyncMessageExpirationTimestampColumn, // expiration_ts
kSyncMessageAPNSReceivedColumn, // apns_recv
kSyncMessageMCSReceivedColumn /* mcs_recv */];
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(self->_database, [insertSQL UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
FIRMessagingRmqLogAndReturn(stmt);
}
if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
FIRMessagingRmqLogAndReturn(stmt);
}
if (sqlite3_bind_int64(stmt, 2, expirationTime) != SQLITE_OK) {
FIRMessagingRmqLogAndReturn(stmt);
}
if (sqlite3_bind_int(stmt, 3, apnsReceived ? 1 : 0) != SQLITE_OK) {
FIRMessagingRmqLogAndReturn(stmt);
}
if (sqlite3_bind_int(stmt, 4, mcsReceived ? 1 : 0) != SQLITE_OK) {
FIRMessagingRmqLogAndReturn(stmt);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
FIRMessagingRmqLogAndReturn(stmt);
}
sqlite3_finalize(stmt);
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeSyncMessageManager004,
@"Added sync message to cache: %@", rmqID);
});
}
- (void)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID {
dispatch_async(_databaseOperationQueue, ^{
if (![self updateSyncMessageWithRmqID:rmqID column:kSyncMessageAPNSReceivedColumn value:YES]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager005,
@"Failed to update APNS state for sync message %@", rmqID);
}
});
}
- (BOOL)updateSyncMessageWithRmqID:(NSString *)rmqID column:(NSString *)column value:(BOOL)value {
FIRMessaging_MUST_NOT_BE_MAIN_THREAD();
NSString *queryFormat = @"UPDATE %@ " // Table name
@"SET %@ = %d " // column=value
@"WHERE %@ = ?"; // condition
NSString *query = [NSString
stringWithFormat:queryFormat, kTableSyncMessages, column, value ? 1 : 0, kRmqIdColumn];
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(stmt, NO);
}
if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(stmt, NO);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
_FIRMessagingRmqLogAndExit(stmt, NO);
}
sqlite3_finalize(stmt);
return YES;
}
#pragma mark - Database
- (NSString *)pathForDatabase {
return [[self class] pathForDatabaseWithName:_databaseName];
}
+ (NSString *)pathForDatabaseWithName:(NSString *)databaseName {
NSString *dbNameWithExtension = [NSString stringWithFormat:@"%@.sqlite", databaseName];
NSArray *paths =
NSSearchPathForDirectoriesInDomains(FIRMessagingSupportedDirectory(), NSUserDomainMask, YES);
NSArray *components = @[ paths.lastObject, kFIRMessagingSubDirectoryName, dbNameWithExtension ];
return [NSString pathWithComponents:components];
}
- (void)createTableWithName:(NSString *)tableName command:(NSString *)command {
FIRMessaging_MUST_NOT_BE_MAIN_THREAD();
char *error = NULL;
NSString *createDatabase = [NSString stringWithFormat:command, kTablePrefix, tableName];
if (sqlite3_exec(self->_database, [createDatabase UTF8String], NULL, NULL, &error) != SQLITE_OK) {
// remove db before failing
[self removeDatabase];
NSString *sqlError;
if (error != NULL) {
sqlError = [NSString stringWithCString:error encoding:NSUTF8StringEncoding];
sqlite3_free(error);
} else {
sqlError = @"(null)";
}
NSString *errorMessage =
[NSString stringWithFormat:@"Couldn't create table: %@ with command: %@ error: %@",
kCreateTableOutgoingRmqMessages, createDatabase, sqlError];
FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingTable, @"%@",
errorMessage);
NSAssert(NO, errorMessage);
}
}
- (void)dropTableWithName:(NSString *)tableName {
FIRMessaging_MUST_NOT_BE_MAIN_THREAD();
char *error;
NSString *dropTableSQL = [NSString stringWithFormat:kDropTableCommand, kTablePrefix, tableName];
if (sqlite3_exec(self->_database, [dropTableSQL UTF8String], NULL, NULL, &error) != SQLITE_OK) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore002,
@"Failed to remove table %@", tableName);
}
}
- (void)removeDatabase {
// Ensure database is removed in a sync queue as this sometimes makes test have race conditions.
dispatch_async(_databaseOperationQueue, ^{
NSString *path = [self pathForDatabase];
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
});
}
- (void)openDatabase {
dispatch_async(_databaseOperationQueue, ^{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *path = [self pathForDatabase];
BOOL didOpenDatabase = YES;
if (![fileManager fileExistsAtPath:path]) {
// We've to separate between different versions here because of backwards compatbility issues.
int result = sqlite3_open_v2(
[path UTF8String], &self -> _database,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FILEPROTECTION_NONE, NULL);
if (result != SQLITE_OK) {
NSString *errorString = FIRMessagingStringFromSQLiteResult(result);
NSString *errorMessage = [NSString
stringWithFormat:@"Could not open existing RMQ database at path %@, error: %@", path,
errorString];
FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorOpeningDatabase,
@"%@", errorMessage);
NSAssert(NO, errorMessage);
return;
}
[self createTableWithName:kTableOutgoingRmqMessages command:kCreateTableOutgoingRmqMessages];
[self createTableWithName:kTableLastRmqId command:kCreateTableLastRmqId];
[self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
} else {
// Calling sqlite3_open should create the database, since the file doesn't exist.
int result = sqlite3_open_v2(
[path UTF8String], &self -> _database,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FILEPROTECTION_NONE, NULL);
if (result != SQLITE_OK) {
NSString *errorString = FIRMessagingStringFromSQLiteResult(result);
NSString *errorMessage =
[NSString stringWithFormat:@"Could not create RMQ database at path %@, error: %@", path,
errorString];
FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingDatabase,
@"%@", errorMessage);
NSAssert(NO, errorMessage);
didOpenDatabase = NO;
} else {
[self updateDBWithStringRmqID];
}
}
if (didOpenDatabase) {
[self createTableWithName:kTableSyncMessages command:kCreateTableSyncMessages];
}
});
}
- (void)updateDBWithStringRmqID {
dispatch_async(_databaseOperationQueue, ^{
[self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
[self dropTableWithName:kOldTableS2DRmqIds];
});
}
#pragma mark - Private
- (BOOL)saveMessageWithRmqId:(int64_t)rmqId tag:(int8_t)tag data:(NSData *)data {
FIRMessaging_MUST_NOT_BE_MAIN_THREAD();
NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@) VALUES (?, ?, ?)";
NSString *insertSQL =
[NSString stringWithFormat:insertFormat,
kTableOutgoingRmqMessages, // table
kRmqIdColumn, kProtobufTagColumn, kDataColumn /* columns */];
sqlite3_stmt *insert_statement;
if (sqlite3_prepare_v2(self->_database, [insertSQL UTF8String], -1, &insert_statement, NULL) !=
SQLITE_OK) {
_FIRMessagingRmqLogAndExit(insert_statement, NO);
}
if (sqlite3_bind_int64(insert_statement, 1, rmqId) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(insert_statement, NO);
}
if (sqlite3_bind_int(insert_statement, 2, tag) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(insert_statement, NO);
}
if (sqlite3_bind_blob(insert_statement, 3, [data bytes], (int)[data length], NULL) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(insert_statement, NO);
}
if (sqlite3_step(insert_statement) != SQLITE_DONE) {
_FIRMessagingRmqLogAndExit(insert_statement, NO);
}
sqlite3_finalize(insert_statement);
return YES;
}
- (void)deleteMessagesFromTable:(NSString *)tableName withRmqIds:(NSArray *)rmqIds {
dispatch_async(_databaseOperationQueue, ^{
BOOL isRmqIDString = NO;
// RmqID is a string only for outgoing messages
if ([tableName isEqualToString:kTableS2DRmqIds] ||
[tableName isEqualToString:kTableSyncMessages]) {
isRmqIDString = YES;
}
NSMutableString *delete =
[NSMutableString stringWithFormat:@"DELETE FROM %@ WHERE ", tableName];
NSString *toDeleteArgument = [NSString stringWithFormat:@"%@ = ? OR ", kRmqIdColumn];
int toDelete = (int)[rmqIds count];
if (toDelete == 0) {
return;
}
int maxBatchSize = 100;
int start = 0;
int deleteCount = 0;
while (start < toDelete) {
// construct the WHERE argument
int end = MIN(start + maxBatchSize, toDelete);
NSMutableString *whereArgument = [NSMutableString string];
for (int i = start; i < end; i++) {
[whereArgument appendString:toDeleteArgument];
}
// remove the last * OR * from argument
NSRange range = NSMakeRange([whereArgument length] - 4, 4);
[whereArgument deleteCharactersInRange:range];
NSString *deleteQuery = [NSString stringWithFormat:@"%@ %@", delete, whereArgument];
// sqlite update
sqlite3_stmt *delete_statement;
if (sqlite3_prepare_v2(self->_database, [deleteQuery UTF8String], -1, &delete_statement,
NULL) != SQLITE_OK) {
FIRMessagingRmqLogAndReturn(delete_statement);
}
// bind values
int rmqIndex = 0;
int placeholderIndex = 1; // placeholders in sqlite3 start with 1
for (NSString *rmqId in rmqIds) { // objectAtIndex: is O(n) -- would make it slow
if (rmqIndex < start) {
rmqIndex++;
continue;
} else if (rmqIndex >= end) {
break;
} else {
if (isRmqIDString) {
if (sqlite3_bind_text(delete_statement, placeholderIndex, [rmqId UTF8String],
(int)[rmqId length], SQLITE_STATIC) != SQLITE_OK) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore003,
@"Failed to bind rmqID %@", rmqId);
FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager007,
@"Failed to delete sync message %@", rmqId);
continue;
}
} else {
int64_t rmqIdValue = [rmqId longLongValue];
sqlite3_bind_int64(delete_statement, placeholderIndex, rmqIdValue);
}
placeholderIndex++;
}
rmqIndex++;
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeSyncMessageManager008,
@"Successfully deleted sync message from cache %@", rmqId);
}
if (sqlite3_step(delete_statement) != SQLITE_DONE) {
FIRMessagingRmqLogAndReturn(delete_statement);
}
sqlite3_finalize(delete_statement);
deleteCount += sqlite3_changes(self->_database);
start = end;
}
// if we are here all of our sqlite queries should have succeeded
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore004,
@"Trying to delete %d s2D ID's, successfully deleted %d", toDelete,
deleteCount);
});
}
- (int64_t)nextRmqId {
return ++self.rmqId;
}
- (NSString *)lastErrorMessage {
return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)];
}
- (int)lastErrorCode {
return sqlite3_errcode(_database);
}
- (void)logError {
FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore006,
@"Error: code (%d) message: %@", [self lastErrorCode],
[self lastErrorMessage]);
}
- (void)logErrorAndFinalizeStatement:(sqlite3_stmt *)stmt {
[self logError];
sqlite3_finalize(stmt);
}
- (dispatch_queue_t)databaseOperationQueue {
return _databaseOperationQueue;
}
@end

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class FIRMessagingRmqManager;
/**
* Handle sync messages being received via APNS.
*/
@interface FIRMessagingSyncMessageManager : NSObject
/**
* Initialize sync message manager.
*
* @param rmqManager The RMQ manager on the client.
*
* @return Sync message manager.
*/
- (instancetype)initWithRmqManager:(FIRMessagingRmqManager *)rmqManager;
/**
* Remove expired sync message from persistent store. Also removes messages that have
* been received via APNS.
*/
- (void)removeExpiredSyncMessages;
/**
* App did recive a sync message via APNS.
*
* @param message The sync message received.
*
* @return YES if the message is a duplicate of an already received sync message else NO.
*/
- (BOOL)didReceiveAPNSSyncMessage:(NSDictionary *)message;
@end

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/FIRMessagingSyncMessageManager.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingPersistentSyncMessage.h"
#import "FirebaseMessaging/Sources/FIRMessagingRmqManager.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
static const int64_t kDefaultSyncMessageTTL = 4 * 7 * 24 * 60 * 60; // 4 weeks
@interface FIRMessagingSyncMessageManager ()
@property(nonatomic, readwrite, strong) FIRMessagingRmqManager *rmqManager;
@end
@implementation FIRMessagingSyncMessageManager
- (instancetype)init {
FIRMessagingInvalidateInitializer();
}
- (instancetype)initWithRmqManager:(FIRMessagingRmqManager *)rmqManager {
self = [super init];
if (self) {
_rmqManager = rmqManager;
}
return self;
}
- (void)removeExpiredSyncMessages {
[self.rmqManager deleteExpiredOrFinishedSyncMessages];
}
- (BOOL)didReceiveAPNSSyncMessage:(NSDictionary *)message {
NSString *rmqID = message[kFIRMessagingMessageIDKey];
if (![rmqID length]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager002,
@"Invalid nil rmqID for sync message.");
return NO;
}
FIRMessagingPersistentSyncMessage *persistentMessage =
[self.rmqManager querySyncMessageWithRmqID:rmqID];
if (!persistentMessage) {
int64_t expirationTime = [[self class] expirationTimeForSyncMessage:message];
[self.rmqManager saveSyncMessageWithRmqID:rmqID expirationTime:expirationTime];
return NO;
}
if (!persistentMessage.apnsReceived) {
persistentMessage.apnsReceived = YES;
[self.rmqManager updateSyncMessageViaAPNSWithRmqID:rmqID];
}
// Already received this message either via MCS or APNS.
return YES;
}
+ (int64_t)expirationTimeForSyncMessage:(NSDictionary *)message {
int64_t ttl = kDefaultSyncMessageTTL;
if (message[kFIRMessagingMessageSyncMessageTTLKey]) {
ttl = [message[kFIRMessagingMessageSyncMessageTTLKey] longLongValue];
}
int64_t currentTime = FIRMessagingCurrentTimestampInSeconds();
return currentTime + ttl;
}
@end

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h"
#import "FirebaseMessaging/Sources/FIRMessagingTopicsCommon.h"
NS_ASSUME_NONNULL_BEGIN
@class FIRMessagingTokenManager;
/**
* An asynchronous NSOperation subclass which performs a single network request for a topic
* subscription operation. Once completed, it calls its provided completion handler.
*/
@interface FIRMessagingTopicOperation : NSOperation
@property(nonatomic, readonly, copy) NSString *topic;
@property(nonatomic, readonly, assign) FIRMessagingTopicAction action;
@property(nonatomic, readonly, copy) NSString *token;
@property(nonatomic, readonly, copy, nullable) NSDictionary *options;
- (instancetype)initWithTopic:(NSString *)topic
action:(FIRMessagingTopicAction)action
tokenManager:(FIRMessagingTokenManager *)tokenManager
options:(nullable NSDictionary *)options
completion:(FIRMessagingTopicOperationCompletion)completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,245 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/FIRMessagingTopicOperation.h"
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenManager.h"
static NSString *const kFIRMessagingSubscribeServerHost =
@"https://iid.googleapis.com/iid/register";
NSString *FIRMessagingSubscriptionsServer(void) {
static NSString *serverHost = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSDictionary *environment = [[NSProcessInfo processInfo] environment];
NSString *customServerHost = environment[@"FCM_SERVER_ENDPOINT"];
if (customServerHost.length) {
serverHost = customServerHost;
} else {
serverHost = kFIRMessagingSubscribeServerHost;
}
});
return serverHost;
}
@interface FIRMessagingTopicOperation () {
BOOL _isFinished;
BOOL _isExecuting;
}
@property(nonatomic, readwrite, copy) NSString *topic;
@property(nonatomic, readwrite, assign) FIRMessagingTopicAction action;
@property(nonatomic, readwrite, strong) FIRMessagingTokenManager *tokenManager;
@property(nonatomic, readwrite, copy) NSDictionary *options;
@property(nonatomic, readwrite, copy) FIRMessagingTopicOperationCompletion completion;
@property(atomic, strong) NSURLSessionDataTask *dataTask;
@end
@implementation FIRMessagingTopicOperation
+ (NSURLSession *)sharedSession {
static NSURLSession *subscriptionOperationSharedSession;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.timeoutIntervalForResource = 60.0f; // 1 minute
subscriptionOperationSharedSession = [NSURLSession sessionWithConfiguration:config];
subscriptionOperationSharedSession.sessionDescription = @"com.google.fcm.topics.session";
});
return subscriptionOperationSharedSession;
}
- (instancetype)initWithTopic:(NSString *)topic
action:(FIRMessagingTopicAction)action
tokenManager:(FIRMessagingTokenManager *)tokenManager
options:(NSDictionary *)options
completion:(FIRMessagingTopicOperationCompletion)completion {
if (self = [super init]) {
_topic = topic;
_action = action;
_tokenManager = tokenManager;
_options = options;
_completion = completion;
_isExecuting = NO;
_isFinished = NO;
}
return self;
}
- (void)dealloc {
_topic = nil;
_completion = nil;
}
- (BOOL)isAsynchronous {
return YES;
}
- (BOOL)isExecuting {
return _isExecuting;
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_isExecuting = executing;
[self didChangeValueForKey:@"isExecuting"];
}
- (BOOL)isFinished {
return _isFinished;
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_isFinished = finished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)start {
if (self.isCancelled) {
NSError *error = [NSError
messagingErrorWithCode:kFIRMessagingErrorCodePubSubOperationIsCancelled
failureReason:
@"Failed to start the pubsub service as the topic operation is cancelled."];
[self finishWithError:error];
return;
}
[self setExecuting:YES];
[self performSubscriptionChange];
}
- (void)finishWithError:(NSError *)error {
// Add a check to prevent this finish from being called more than once.
if (self.isFinished) {
return;
}
self.dataTask = nil;
if (self.completion) {
self.completion(error);
}
[self setExecuting:NO];
[self setFinished:YES];
}
- (void)cancel {
[super cancel];
[self.dataTask cancel];
NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodePubSubOperationIsCancelled
failureReason:@"The topic operation is cancelled."];
[self finishWithError:error];
}
- (void)performSubscriptionChange {
NSURL *url = [NSURL URLWithString:FIRMessagingSubscriptionsServer()];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSString *appIdentifier = FIRMessagingAppIdentifier();
NSString *authString = [NSString
stringWithFormat:@"AidLogin %@:%@", _tokenManager.deviceAuthID, _tokenManager.secretToken];
[request setValue:authString forHTTPHeaderField:@"Authorization"];
[request setValue:appIdentifier forHTTPHeaderField:@"app"];
[request setValue:_tokenManager.versionInfo forHTTPHeaderField:@"info"];
// Topic can contain special characters (like `%`) so encode the value.
NSCharacterSet *characterSet = [NSCharacterSet URLQueryAllowedCharacterSet];
NSString *encodedTopic =
[self.topic stringByAddingPercentEncodingWithAllowedCharacters:characterSet];
if (encodedTopic == nil) {
// The transformation was somehow not possible, so use the original topic.
FIRMessagingLoggerWarn(kFIRMessagingMessageCodeTopicOptionTopicEncodingFailed,
@"Unable to encode the topic '%@' during topic subscription change. "
@"Please ensure that the topic name contains only valid characters.",
self.topic);
encodedTopic = self.topic;
}
NSMutableString *content = [NSMutableString
stringWithFormat:@"sender=%@&app=%@&device=%@&"
@"app_ver=%@&X-gcm.topic=%@&X-scope=%@",
_tokenManager.defaultFCMToken, appIdentifier, _tokenManager.deviceAuthID,
FIRMessagingCurrentAppVersion(), encodedTopic, encodedTopic];
if (self.action == FIRMessagingTopicActionUnsubscribe) {
[content appendString:@"&delete=true"];
}
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeTopicOption000, @"Topic subscription request: %@",
content);
request.HTTPBody = [content dataUsingEncoding:NSUTF8StringEncoding];
[request setHTTPMethod:@"POST"];
FIRMessaging_WEAKIFY(self) void (^requestHandler)(NSData *, NSURLResponse *, NSError *) =
^(NSData *data, NSURLResponse *URLResponse, NSError *error) {
FIRMessaging_STRONGIFY(self) if (error) {
// Our operation could have been cancelled, which would result in our data task's error
// being NSURLErrorCancelled
if (error.code == NSURLErrorCancelled) {
// We would only have been cancelled in the -cancel method, which will call finish for
// us so just return and do nothing.
return;
}
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOption001,
@"Device registration HTTP fetch error. Error Code: %ld",
(long)error.code);
[self finishWithError:error];
return;
}
NSString *response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (response.length == 0) {
NSString *failureReason = @"Invalid registration response - zero length.";
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOperationEmptyResponse, @"%@",
failureReason);
[self finishWithError:[NSError messagingErrorWithCode:kFIRMessagingErrorCodeUnknown
failureReason:failureReason]];
return;
}
NSArray *parts = [response componentsSeparatedByString:@"="];
if (![parts[0] isEqualToString:@"token"] || parts.count <= 1) {
NSString *failureReason = [NSString
stringWithFormat:@"Invalid registration response :'%@'. It is missing 'token' field.",
response];
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOption002, @"%@", failureReason);
[self finishWithError:[NSError messagingErrorWithCode:kFIRMessagingErrorCodeUnknown
failureReason:failureReason]];
return;
}
[self finishWithError:nil];
};
NSURLSession *urlSession = [FIRMessagingTopicOperation sharedSession];
self.dataTask = [urlSession dataTaskWithRequest:request completionHandler:requestHandler];
NSString *description;
if (_action == FIRMessagingTopicActionSubscribe) {
description = [NSString stringWithFormat:@"com.google.fcm.topics.subscribe: %@", _topic];
} else {
description = [NSString stringWithFormat:@"com.google.fcm.topics.unsubscribe: %@", _topic];
}
self.dataTask.taskDescription = description;
[self.dataTask resume];
}
@end

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Represents the action taken on a subscription topic.
*/
typedef NS_ENUM(NSInteger, FIRMessagingTopicAction) {
FIRMessagingTopicActionSubscribe,
FIRMessagingTopicActionUnsubscribe
};
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#pragma mark - URL Helpers
FOUNDATION_EXPORT NSString *FIRMessagingTokenRegisterServer(void);
#pragma mark - Time
FOUNDATION_EXPORT int64_t FIRMessagingCurrentTimestampInSeconds(void);
FOUNDATION_EXPORT int64_t FIRMessagingCurrentTimestampInMilliseconds(void);
#pragma mark - App Info
FOUNDATION_EXPORT NSString *FIRMessagingCurrentAppVersion(void);
FOUNDATION_EXPORT NSString *FIRMessagingAppIdentifier(void);
FOUNDATION_EXPORT NSString *FIRMessagingFirebaseAppID(void);
FOUNDATION_EXPORT BOOL FIRMessagingIsWatchKitExtension(void);
#pragma mark - Others
FOUNDATION_EXPORT NSSearchPathDirectory FIRMessagingSupportedDirectory(void);
#pragma mark - Device Info
FOUNDATION_EXPORT NSString *FIRMessagingCurrentLocale(void);
FOUNDATION_EXPORT BOOL FIRMessagingHasLocaleChanged(void);
/// locale key stored in GULUserDefaults
FOUNDATION_EXPORT NSString *const kFIRMessagingInstanceIDUserDefaultsKeyLocale;
FOUNDATION_EXPORT NSString *FIRMessagingStringForAPNSDeviceToken(NSData *deviceToken);
FOUNDATION_EXPORT NSString *FIRMessagingAPNSTupleStringForTokenAndServerType(NSData *deviceToken,
BOOL isSandbox);
FOUNDATION_EXPORT BOOL FIRMessagingIsSandboxApp(void);

View File

@@ -0,0 +1,425 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import <GoogleUtilities/GULAppEnvironmentUtil.h>
#import <GoogleUtilities/GULUserDefaults.h>
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
NSString *const kFIRMessagingInstanceIDUserDefaultsKeyLocale =
@"com.firebase.instanceid.user_defaults.locale"; // locale key stored in GULUserDefaults
static NSString *const kFIRMessagingAPNSSandboxPrefix = @"s_";
static NSString *const kFIRMessagingAPNSProdPrefix = @"p_";
static NSString *const kFIRMessagingWatchKitExtensionPoint = @"com.apple.watchkit";
#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH
static NSString *const kEntitlementsAPSEnvironmentKey = @"Entitlements.aps-environment";
#else
static NSString *const kEntitlementsAPSEnvironmentKey =
@"Entitlements.com.apple.developer.aps-environment";
#endif
static NSString *const kAPSEnvironmentDevelopmentValue = @"development";
#pragma mark - URL Helpers
NSString *FIRMessagingTokenRegisterServer(void) {
return @"https://fcmtoken.googleapis.com/register";
}
#pragma mark - Time
int64_t FIRMessagingCurrentTimestampInSeconds(void) {
return (int64_t)[[NSDate date] timeIntervalSince1970];
}
int64_t FIRMessagingCurrentTimestampInMilliseconds(void) {
return (int64_t)(FIRMessagingCurrentTimestampInSeconds() * 1000.0);
}
#pragma mark - App Info
NSString *FIRMessagingCurrentAppVersion(void) {
NSString *version = [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"];
if (![version length]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeUtilities000,
@"Could not find current app version");
return @"";
}
return version;
}
NSString *FIRMessagingBundleIDByRemovingLastPartFrom(NSString *bundleID) {
NSString *bundleIDComponentsSeparator = @".";
NSMutableArray<NSString *> *bundleIDComponents =
[[bundleID componentsSeparatedByString:bundleIDComponentsSeparator] mutableCopy];
[bundleIDComponents removeLastObject];
return [bundleIDComponents componentsJoinedByString:bundleIDComponentsSeparator];
}
NSString *FIRMessagingAppIdentifier(void) {
NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier];
#if TARGET_OS_WATCH
if (FIRMessagingIsWatchKitExtension()) {
// The code is running in watchKit extension target but the actually bundleID is in the watchKit
// target. So we need to remove the last part of the bundle ID in watchKit extension to match
// the one in watchKit target.
return FIRMessagingBundleIDByRemovingLastPartFrom(bundleID);
} else {
return bundleID;
}
#else
return bundleID;
#endif
}
NSString *FIRMessagingFirebaseAppID(void) {
return [FIROptions defaultOptions].googleAppID;
}
BOOL FIRMessagingIsWatchKitExtension(void) {
#if TARGET_OS_WATCH
NSDictionary<NSString *, id> *infoDict = [[NSBundle mainBundle] infoDictionary];
NSDictionary<NSString *, id> *extensionAttrDict = infoDict[@"NSExtension"];
if (!extensionAttrDict) {
return NO;
}
NSString *extensionPointId = extensionAttrDict[@"NSExtensionPointIdentifier"];
if (extensionPointId) {
return [extensionPointId isEqualToString:kFIRMessagingWatchKitExtensionPoint];
} else {
return NO;
}
#else
return NO;
#endif
}
NSSearchPathDirectory FIRMessagingSupportedDirectory(void) {
#if TARGET_OS_TV
return NSCachesDirectory;
#else
return NSApplicationSupportDirectory;
#endif
}
#pragma mark - Locales
NSDictionary *FIRMessagingFirebaselocalesMap(void) {
return @{
// Albanian
@"sq" : @[ @"sq_AL" ],
// Belarusian
@"be" : @[ @"be_BY" ],
// Bulgarian
@"bg" : @[ @"bg_BG" ],
// Catalan
@"ca" : @[ @"ca", @"ca_ES" ],
// Croatian
@"hr" : @[ @"hr", @"hr_HR" ],
// Czech
@"cs" : @[ @"cs", @"cs_CZ" ],
// Danish
@"da" : @[ @"da", @"da_DK" ],
// Estonian
@"et" : @[ @"et_EE" ],
// Finnish
@"fi" : @[ @"fi", @"fi_FI" ],
// Hebrew
@"he" : @[ @"he", @"iw_IL" ],
// Hindi
@"hi" : @[ @"hi_IN" ],
// Hungarian
@"hu" : @[ @"hu", @"hu_HU" ],
// Icelandic
@"is" : @[ @"is_IS" ],
// Indonesian
@"id" : @[ @"id", @"in_ID", @"id_ID" ],
// Irish
@"ga" : @[ @"ga_IE" ],
// Korean
@"ko" : @[ @"ko", @"ko_KR", @"ko-KR" ],
// Latvian
@"lv" : @[ @"lv_LV" ],
// Lithuanian
@"lt" : @[ @"lt_LT" ],
// Macedonian
@"mk" : @[ @"mk_MK" ],
// Malay
@"ms" : @[ @"ms_MY" ],
// Maltese
@"mt" : @[ @"mt_MT" ],
// Polish
@"pl" : @[ @"pl", @"pl_PL", @"pl-PL" ],
// Romanian
@"ro" : @[ @"ro", @"ro_RO" ],
// Russian
@"ru" : @[ @"ru_RU", @"ru", @"ru_BY", @"ru_KZ", @"ru-RU" ],
// Slovak
@"sk" : @[ @"sk", @"sk_SK" ],
// Slovenian
@"sl" : @[ @"sl_SI" ],
// Swedish
@"sv" : @[ @"sv", @"sv_SE", @"sv-SE" ],
// Turkish
@"tr" : @[ @"tr", @"tr-TR", @"tr_TR" ],
// Ukrainian
@"uk" : @[ @"uk", @"uk_UA" ],
// Vietnamese
@"vi" : @[ @"vi", @"vi_VN" ],
// The following are groups of locales or locales that sub-divide a
// language).
// Arabic
@"ar" : @[
@"ar", @"ar_DZ", @"ar_BH", @"ar_EG", @"ar_IQ", @"ar_JO", @"ar_KW",
@"ar_LB", @"ar_LY", @"ar_MA", @"ar_OM", @"ar_QA", @"ar_SA", @"ar_SD",
@"ar_SY", @"ar_TN", @"ar_AE", @"ar_YE", @"ar_GB", @"ar-IQ", @"ar_US"
],
// Simplified Chinese
@"zh_Hans" : @[ @"zh_CN", @"zh_SG", @"zh-Hans" ],
// Traditional Chinese
@"zh_Hant" : @[ @"zh_HK", @"zh_TW", @"zh-Hant", @"zh-HK", @"zh-TW" ],
// Dutch
@"nl" : @[ @"nl", @"nl_BE", @"nl_NL", @"nl-NL" ],
// English
@"en" : @[
@"en", @"en_AU", @"en_CA", @"en_IN", @"en_IE", @"en_MT", @"en_NZ", @"en_PH",
@"en_SG", @"en_ZA", @"en_GB", @"en_US", @"en_AE", @"en-AE", @"en_AS", @"en-AU",
@"en_BD", @"en-CA", @"en_EG", @"en_ES", @"en_GB", @"en-GB", @"en_HK", @"en_ID",
@"en-IN", @"en_NG", @"en-PH", @"en_PK", @"en-SG", @"en-US"
],
// French
@"fr" :
@[ @"fr", @"fr_BE", @"fr_CA", @"fr_FR", @"fr_LU", @"fr_CH", @"fr-CA", @"fr-FR", @"fr_MA" ],
// German
@"de" : @[ @"de", @"de_AT", @"de_DE", @"de_LU", @"de_CH", @"de-DE" ],
// Greek
@"el" : @[ @"el", @"el_CY", @"el_GR" ],
// Italian
@"it" : @[ @"it", @"it_IT", @"it_CH", @"it-IT" ],
// Japanese
@"ja" : @[ @"ja", @"ja_JP", @"ja_JP_JP", @"ja-JP" ],
// Norwegian
@"no" : @[ @"nb", @"no_NO", @"no_NO_NY", @"nb_NO" ],
// Brazilian Portuguese
@"pt_BR" : @[ @"pt_BR", @"pt-BR" ],
// European Portuguese
@"pt_PT" : @[ @"pt", @"pt_PT", @"pt-PT" ],
// Serbian
@"sr" : @[ @"sr_BA", @"sr_ME", @"sr_RS", @"sr_Latn_BA", @"sr_Latn_ME", @"sr_Latn_RS" ],
// European Spanish
@"es_ES" : @[ @"es", @"es_ES", @"es-ES" ],
// Mexican Spanish
@"es_MX" : @[ @"es-MX", @"es_MX", @"es_US", @"es-US" ],
// Latin American Spanish
@"es_419" : @[
@"es_AR", @"es_BO", @"es_CL", @"es_CO", @"es_CR", @"es_DO", @"es_EC",
@"es_SV", @"es_GT", @"es_HN", @"es_NI", @"es_PA", @"es_PY", @"es_PE",
@"es_PR", @"es_UY", @"es_VE", @"es-AR", @"es-CL", @"es-CO"
],
// Thai
@"th" : @[ @"th", @"th_TH", @"th_TH_TH" ],
};
}
NSArray *FIRMessagingFirebaseLocales(void) {
NSMutableArray *locales = [NSMutableArray array];
NSDictionary *localesMap = FIRMessagingFirebaselocalesMap();
for (NSString *key in localesMap) {
[locales addObjectsFromArray:localesMap[key]];
}
return locales;
}
NSString *FIRMessagingCurrentLocale(void) {
NSArray *locales = FIRMessagingFirebaseLocales();
NSArray *preferredLocalizations =
[NSBundle preferredLocalizationsFromArray:locales
forPreferences:[NSLocale preferredLanguages]];
NSString *legalDocsLanguage = [preferredLocalizations firstObject];
// Use en as the default language
return legalDocsLanguage ? legalDocsLanguage : @"en";
}
BOOL FIRMessagingHasLocaleChanged(void) {
NSString *lastLocale = [[GULUserDefaults standardUserDefaults]
stringForKey:kFIRMessagingInstanceIDUserDefaultsKeyLocale];
NSString *currentLocale = FIRMessagingCurrentLocale();
if (lastLocale) {
if ([currentLocale isEqualToString:lastLocale]) {
return NO;
}
}
return YES;
}
NSString *FIRMessagingStringForAPNSDeviceToken(NSData *deviceToken) {
NSMutableString *APNSToken = [NSMutableString string];
unsigned char *bytes = (unsigned char *)[deviceToken bytes];
for (int i = 0; i < (int)deviceToken.length; i++) {
[APNSToken appendFormat:@"%02x", bytes[i]];
}
return APNSToken;
}
NSString *FIRMessagingAPNSTupleStringForTokenAndServerType(NSData *deviceToken, BOOL isSandbox) {
if (deviceToken == nil) {
// A nil deviceToken leads to an invalid tuple string, so return nil.
return nil;
}
NSString *prefix = isSandbox ? kFIRMessagingAPNSSandboxPrefix : kFIRMessagingAPNSProdPrefix;
NSString *APNSString = FIRMessagingStringForAPNSDeviceToken(deviceToken);
NSString *APNSTupleString = [NSString stringWithFormat:@"%@%@", prefix, APNSString];
return APNSTupleString;
}
BOOL FIRMessagingIsProductionApp(void) {
const BOOL defaultAppTypeProd = YES;
NSError *error = nil;
if ([GULAppEnvironmentUtil isSimulator]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeInstanceID014,
@"Running InstanceID on a simulator doesn't have APNS. "
@"Use prod profile by default.");
return defaultAppTypeProd;
}
if ([GULAppEnvironmentUtil isFromAppStore]) {
// Apps distributed via AppStore or TestFlight use the Production APNS certificates.
return defaultAppTypeProd;
}
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
NSString *path = [[[[NSBundle mainBundle] resourcePath] stringByDeletingLastPathComponent]
stringByAppendingPathComponent:@"embedded.provisionprofile"];
#elif TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH || \
(defined(TARGET_OS_VISION) && TARGET_OS_VISION)
NSString *path = [[[NSBundle mainBundle] bundlePath]
stringByAppendingPathComponent:@"embedded.mobileprovision"];
#endif
if ([GULAppEnvironmentUtil isAppStoreReceiptSandbox] && !path.length) {
// Distributed via TestFlight
return defaultAppTypeProd;
}
NSMutableData *profileData = [NSMutableData dataWithContentsOfFile:path options:0 error:&error];
if (!profileData.length || error) {
NSString *errorString =
[NSString stringWithFormat:@"Error while reading embedded mobileprovision %@", error];
FIRMessagingLoggerError(kFIRMessagingMessageCodeInstanceID014, @"%@", errorString);
return defaultAppTypeProd;
}
// The "embedded.mobileprovision" sometimes contains characters with value 0, which signals the
// end of a c-string and halts the ASCII parser, or with value > 127, which violates strict 7-bit
// ASCII. Replace any 0s or invalid characters in the input.
uint8_t *profileBytes = (uint8_t *)profileData.bytes;
for (int i = 0; i < profileData.length; i++) {
uint8_t currentByte = profileBytes[i];
if (!currentByte || currentByte > 127) {
profileBytes[i] = '.';
}
}
NSString *embeddedProfile = [[NSString alloc] initWithBytesNoCopy:profileBytes
length:profileData.length
encoding:NSASCIIStringEncoding
freeWhenDone:NO];
if (error || !embeddedProfile.length) {
NSString *errorString =
[NSString stringWithFormat:@"Error while reading embedded mobileprovision %@", error];
FIRMessagingLoggerError(kFIRMessagingMessageCodeInstanceID014, @"%@", errorString);
return defaultAppTypeProd;
}
NSScanner *scanner = [NSScanner scannerWithString:embeddedProfile];
NSString *plistContents;
if ([scanner scanUpToString:@"<plist" intoString:nil]) {
if ([scanner scanUpToString:@"</plist>" intoString:&plistContents]) {
plistContents = [plistContents stringByAppendingString:@"</plist>"];
}
}
if (!plistContents.length) {
return defaultAppTypeProd;
}
NSData *data = [plistContents dataUsingEncoding:NSUTF8StringEncoding];
if (!data.length) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeInstanceID014,
@"Couldn't read plist fetched from embedded mobileprovision");
return defaultAppTypeProd;
}
NSError *plistMapError;
id plistData = [NSPropertyListSerialization propertyListWithData:data
options:NSPropertyListImmutable
format:nil
error:&plistMapError];
if (plistMapError || ![plistData isKindOfClass:[NSDictionary class]]) {
NSString *errorString =
[NSString stringWithFormat:@"Error while converting assumed plist to dict %@",
plistMapError.localizedDescription];
FIRMessagingLoggerError(kFIRMessagingMessageCodeInstanceID014, @"%@", errorString);
return defaultAppTypeProd;
}
NSDictionary *plistMap = (NSDictionary *)plistData;
if ([plistMap valueForKeyPath:@"ProvisionedDevices"]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeInstanceID012,
@"Provisioning profile has specifically provisioned devices, "
@"most likely a Dev profile.");
}
NSString *apsEnvironment = [plistMap valueForKeyPath:kEntitlementsAPSEnvironmentKey];
NSString *debugString __unused =
[NSString stringWithFormat:@"APNS Environment in profile: %@", apsEnvironment];
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeInstanceID013, @"%@", debugString);
// No aps-environment in the profile.
if (!apsEnvironment.length) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeInstanceID014,
@"No aps-environment set. If testing on a device APNS is not "
@"correctly configured. Please recheck your provisioning "
@"profiles. If testing on a simulator this is fine since APNS "
@"doesn't work on the simulator.");
return defaultAppTypeProd;
}
if ([apsEnvironment isEqualToString:kAPSEnvironmentDevelopmentValue]) {
return NO;
}
return defaultAppTypeProd;
}
BOOL FIRMessagingIsSandboxApp(void) {
static BOOL isSandboxApp = YES;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
isSandboxApp = !FIRMessagingIsProductionApp();
});
return isSandboxApp;
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h"
@class FIRMessagingClient;
@class FIRMessagingPubSub;
typedef NS_ENUM(int8_t, FIRMessagingNetworkStatus) {
kFIRMessagingReachabilityNotReachable = 0,
kFIRMessagingReachabilityReachableViaWiFi,
kFIRMessagingReachabilityReachableViaWWAN,
};
FOUNDATION_EXPORT NSString *const kFIRMessagingPlistAutoInitEnabled;
FOUNDATION_EXPORT NSString *const kFIRMessagingUserDefaultsKeyAutoInitEnabled;
FOUNDATION_EXPORT NSString *const kFIRMessagingUserDefaultsKeyUseMessagingDelegate;
FOUNDATION_EXPORT NSString *const kFIRMessagingPlistUseMessagingDelegate;
@interface FIRMessaging ()
#pragma mark - Private API
- (FIRMessagingPubSub *)pubsub;
- (BOOL)isNetworkAvailable;
- (FIRMessagingNetworkStatus)networkType;
+ (NSString *)FIRMessagingSDKVersion;
@end

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h"

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@interface NSDictionary (FIRMessaging)
/**
* Returns a string representation for the given dictionary. Assumes that all
* keys and values are strings.
*
* @return A string representation of all keys and values in the dictionary.
* The returned string is not pretty-printed.
*/
- (NSString *)fcm_string;
/**
* Check if the dictionary has any non-string keys or values.
*
* @return YES if the dictionary has any non-string keys or values else NO.
*/
- (BOOL)fcm_hasNonStringKeysOrValues;
/**
* Trims all (key, value) pair in a dictionary that are not strings.
*
* @return A new copied dictionary with all the non-string keys or values
* removed from the original dictionary.
*/
- (NSDictionary *)fcm_trimNonStringValues;
@end

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/NSDictionary+FIRMessaging.h"
@implementation NSDictionary (FIRMessaging)
- (NSString *)fcm_string {
NSMutableString *dictAsString = [NSMutableString string];
NSString *separator = @"|";
for (id key in self) {
id value = self[key];
if ([key isKindOfClass:[NSString class]] && [value isKindOfClass:[NSString class]]) {
[dictAsString appendFormat:@"%@:%@%@", key, value, separator];
}
}
// remove the last separator
if ([dictAsString length]) {
[dictAsString deleteCharactersInRange:NSMakeRange(dictAsString.length - 1, 1)];
}
return [dictAsString copy];
}
- (BOOL)fcm_hasNonStringKeysOrValues {
for (id key in self) {
id value = self[key];
if (![key isKindOfClass:[NSString class]] || ![value isKindOfClass:[NSString class]]) {
return YES;
}
}
return NO;
}
- (NSDictionary *)fcm_trimNonStringValues {
NSMutableDictionary *trimDictionary = [NSMutableDictionary dictionaryWithCapacity:self.count];
for (id key in self) {
id value = self[key];
if ([key isKindOfClass:[NSString class]] && [value isKindOfClass:[NSString class]]) {
trimDictionary[(NSString *)key] = value;
}
}
return trimDictionary;
}
@end

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
// FIRMessaging Internal Error Code
typedef NS_ENUM(NSUInteger, FIRMessagingErrorCode) {
kFIRMessagingErrorCodeUnknown = 0,
kFIRMessagingErrorCodeInternal = 1,
kFIRMessagingErrorCodeNetwork = 4,
// Failed to perform device check in.
kFIRMessagingErrorCodeRegistrarFailedToCheckIn = 6,
kFIRMessagingErrorCodeInvalidRequest = 7,
kFIRMessagingErrorCodeInvalidTopicName = 8,
// FIRMessaging generic errors
kFIRMessagingErrorCodeMissingDeviceID = 501,
kFIRMessagingErrorCodeMissingAuthorizedEntity = 502,
kFIRMessagingErrorCodeMissingScope = 503,
kFIRMessagingErrorCodeMissingFid = 504,
kFIRMessagingErrorCodeMissingDeviceToken = 505,
// Upstream send errors
kFIRMessagingErrorCodeServiceNotAvailable = 1001,
kFIRMessagingErrorCodeMissingTo = 1003,
kFIRMessagingErrorCodeSave = 1004,
kFIRMessagingErrorCodeSizeExceeded = 1005,
kFIRMessagingErrorCodeInvalidIdentity = 2001,
// PubSub errors
kFIRMessagingErrorCodePubSubOperationIsCancelled = 3005,
};
@interface NSError (FIRMessaging)
+ (NSError *)messagingErrorWithCode:(FIRMessagingErrorCode)fcmErrorCode
failureReason:(NSString *)failureReason;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h"
@implementation NSError (FIRMessaging)
+ (NSError *)messagingErrorWithCode:(FIRMessagingErrorCode)errorCode
failureReason:(NSString *)failureReason {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[NSLocalizedFailureReasonErrorKey] = failureReason;
return [NSError errorWithDomain:FIRMessagingErrorDomain code:errorCode userInfo:userInfo];
}
@end

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Automatically generated nanopb constant definitions */
/* Generated by nanopb-0.3.9.9 */
#include "FirebaseMessaging/Sources/Protogen/nanopb/me.nanopb.h"
/* @@protoc_insertion_point(includes) */
#if PB_PROTO_HEADER_VERSION != 30
#error Regenerate this file with the current version of nanopb generator.
#endif
const pb_field_t fm_MessagingClientEvent_fields[11] = {
PB_FIELD( 1, INT64 , SINGULAR, STATIC , FIRST, fm_MessagingClientEvent, project_number, project_number, 0),
PB_FIELD( 2, BYTES , SINGULAR, POINTER , OTHER, fm_MessagingClientEvent, message_id, project_number, 0),
PB_FIELD( 3, BYTES , SINGULAR, POINTER , OTHER, fm_MessagingClientEvent, instance_id, message_id, 0),
PB_FIELD( 4, UENUM , SINGULAR, STATIC , OTHER, fm_MessagingClientEvent, message_type, instance_id, 0),
PB_FIELD( 5, UENUM , SINGULAR, STATIC , OTHER, fm_MessagingClientEvent, sdk_platform, message_type, 0),
PB_FIELD( 6, BYTES , SINGULAR, POINTER , OTHER, fm_MessagingClientEvent, package_name, sdk_platform, 0),
PB_FIELD( 12, UENUM , SINGULAR, STATIC , OTHER, fm_MessagingClientEvent, event, package_name, 0),
PB_FIELD( 13, BYTES , SINGULAR, POINTER , OTHER, fm_MessagingClientEvent, analytics_label, event, 0),
PB_FIELD( 14, INT64 , SINGULAR, STATIC , OTHER, fm_MessagingClientEvent, campaign_id, analytics_label, 0),
PB_FIELD( 15, BYTES , SINGULAR, POINTER , OTHER, fm_MessagingClientEvent, composer_label, campaign_id, 0),
PB_LAST_FIELD
};
const pb_field_t fm_MessagingClientEventExtension_fields[2] = {
PB_FIELD( 1, MESSAGE , SINGULAR, POINTER , FIRST, fm_MessagingClientEventExtension, messaging_client_event, messaging_client_event, &fm_MessagingClientEvent_fields),
PB_LAST_FIELD
};
/* @@protoc_insertion_point(eof) */

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Automatically generated nanopb header */
/* Generated by nanopb-0.3.9.9 */
#ifndef PB_FM_ME_NANOPB_H_INCLUDED
#define PB_FM_ME_NANOPB_H_INCLUDED
#include <nanopb/pb.h>
/* @@protoc_insertion_point(includes) */
#if PB_PROTO_HEADER_VERSION != 30
#error Regenerate this file with the current version of nanopb generator.
#endif
/* Enum definitions */
typedef enum _fm_MessagingClientEvent_MessageType {
fm_MessagingClientEvent_MessageType_UNKNOWN = 0,
fm_MessagingClientEvent_MessageType_DATA_MESSAGE = 1,
fm_MessagingClientEvent_MessageType_TOPIC = 2,
fm_MessagingClientEvent_MessageType_DISPLAY_NOTIFICATION = 3
} fm_MessagingClientEvent_MessageType;
#define _fm_MessagingClientEvent_MessageType_MIN fm_MessagingClientEvent_MessageType_UNKNOWN
#define _fm_MessagingClientEvent_MessageType_MAX fm_MessagingClientEvent_MessageType_DISPLAY_NOTIFICATION
#define _fm_MessagingClientEvent_MessageType_ARRAYSIZE ((fm_MessagingClientEvent_MessageType)(fm_MessagingClientEvent_MessageType_DISPLAY_NOTIFICATION+1))
typedef enum _fm_MessagingClientEvent_SDKPlatform {
fm_MessagingClientEvent_SDKPlatform_UNKNOWN_OS = 0,
fm_MessagingClientEvent_SDKPlatform_ANDROID = 1,
fm_MessagingClientEvent_SDKPlatform_IOS = 2,
fm_MessagingClientEvent_SDKPlatform_WEB = 3
} fm_MessagingClientEvent_SDKPlatform;
#define _fm_MessagingClientEvent_SDKPlatform_MIN fm_MessagingClientEvent_SDKPlatform_UNKNOWN_OS
#define _fm_MessagingClientEvent_SDKPlatform_MAX fm_MessagingClientEvent_SDKPlatform_WEB
#define _fm_MessagingClientEvent_SDKPlatform_ARRAYSIZE ((fm_MessagingClientEvent_SDKPlatform)(fm_MessagingClientEvent_SDKPlatform_WEB+1))
typedef enum _fm_MessagingClientEvent_Event {
fm_MessagingClientEvent_Event_UNKNOWN_EVENT = 0,
fm_MessagingClientEvent_Event_MESSAGE_DELIVERED = 1,
fm_MessagingClientEvent_Event_MESSAGE_OPEN = 2
} fm_MessagingClientEvent_Event;
#define _fm_MessagingClientEvent_Event_MIN fm_MessagingClientEvent_Event_UNKNOWN_EVENT
#define _fm_MessagingClientEvent_Event_MAX fm_MessagingClientEvent_Event_MESSAGE_OPEN
#define _fm_MessagingClientEvent_Event_ARRAYSIZE ((fm_MessagingClientEvent_Event)(fm_MessagingClientEvent_Event_MESSAGE_OPEN+1))
/* Struct definitions */
typedef struct _fm_MessagingClientEventExtension {
struct _fm_MessagingClientEvent *messaging_client_event;
/* @@protoc_insertion_point(struct:fm_MessagingClientEventExtension) */
} fm_MessagingClientEventExtension;
typedef struct _fm_MessagingClientEvent {
int64_t project_number;
pb_bytes_array_t *message_id;
pb_bytes_array_t *instance_id;
fm_MessagingClientEvent_MessageType message_type;
fm_MessagingClientEvent_SDKPlatform sdk_platform;
pb_bytes_array_t *package_name;
fm_MessagingClientEvent_Event event;
pb_bytes_array_t *analytics_label;
int64_t campaign_id;
pb_bytes_array_t *composer_label;
/* @@protoc_insertion_point(struct:fm_MessagingClientEvent) */
} fm_MessagingClientEvent;
/* Default values for struct fields */
/* Initializer values for message structs */
#define fm_MessagingClientEvent_init_default {0, NULL, NULL, _fm_MessagingClientEvent_MessageType_MIN, _fm_MessagingClientEvent_SDKPlatform_MIN, NULL, _fm_MessagingClientEvent_Event_MIN, NULL, 0, NULL}
#define fm_MessagingClientEventExtension_init_default {NULL}
#define fm_MessagingClientEvent_init_zero {0, NULL, NULL, _fm_MessagingClientEvent_MessageType_MIN, _fm_MessagingClientEvent_SDKPlatform_MIN, NULL, _fm_MessagingClientEvent_Event_MIN, NULL, 0, NULL}
#define fm_MessagingClientEventExtension_init_zero {NULL}
/* Field tags (for use in manual encoding/decoding) */
#define fm_MessagingClientEventExtension_messaging_client_event_tag 1
#define fm_MessagingClientEvent_project_number_tag 1
#define fm_MessagingClientEvent_message_id_tag 2
#define fm_MessagingClientEvent_instance_id_tag 3
#define fm_MessagingClientEvent_message_type_tag 4
#define fm_MessagingClientEvent_sdk_platform_tag 5
#define fm_MessagingClientEvent_package_name_tag 6
#define fm_MessagingClientEvent_event_tag 12
#define fm_MessagingClientEvent_analytics_label_tag 13
#define fm_MessagingClientEvent_campaign_id_tag 14
#define fm_MessagingClientEvent_composer_label_tag 15
/* Struct field encoding specification for nanopb */
extern const pb_field_t fm_MessagingClientEvent_fields[11];
extern const pb_field_t fm_MessagingClientEventExtension_fields[2];
/* Maximum encoded size of messages (where known) */
/* fm_MessagingClientEvent_size depends on runtime parameters */
/* fm_MessagingClientEventExtension_size depends on runtime parameters */
/* Message IDs (where set with "msgid" option) */
#ifdef PB_MSGID
#define ME_MESSAGES \
#endif
/* @@protoc_insertion_point(eof) */
#endif

View File

@@ -0,0 +1,406 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* @related FIRMessaging
*
* The completion handler invoked when the registration token returns.
* If the call fails we return the appropriate `error code`, described by
* `FIRMessagingError`.
*
* @param FCMToken The valid registration token returned by FCM.
* @param error The error describing why a token request failed. The error code
* will match a value from the FIRMessagingError enumeration.
*/
typedef void (^FIRMessagingFCMTokenFetchCompletion)(NSString *_Nullable FCMToken,
NSError *_Nullable error)
NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.");
/**
* @related FIRMessaging
*
* The completion handler invoked when the registration token deletion request is
* completed. If the call fails we return the appropriate `error code`, described
* by `FIRMessagingError`.
*
* @param error The error describing why a token deletion failed. The error code
* will match a value from the FIRMessagingError enumeration.
*/
typedef void (^FIRMessagingDeleteFCMTokenCompletion)(NSError *_Nullable error)
NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.");
/**
* Callback to invoke once the HTTP call to FIRMessaging backend for updating
* subscription finishes.
*
* @param error The error which occurred while updating the subscription topic
* on the FIRMessaging server. This will be nil in case the operation
* was successful, or if the operation was cancelled.
*/
typedef void (^FIRMessagingTopicOperationCompletion)(NSError *_Nullable error)
NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.");
/**
* Notification sent when the FCM registration token has been refreshed. Please use the
* FIRMessaging delegate method `messaging:didReceiveRegistrationToken:` to receive current and
* updated tokens.
*/
// clang-format off
// clang-format12 merges the next two lines.
FOUNDATION_EXPORT const NSNotificationName FIRMessagingRegistrationTokenRefreshedNotification
NS_SWIFT_NAME(MessagingRegistrationTokenRefreshed);
// clang-format on
/**
* The domain used for all errors in Messaging.
*/
FOUNDATION_EXPORT NSString *const FIRMessagingErrorDomain NS_SWIFT_NAME(MessagingErrorDomain);
/**
* @enum FIRMessagingError
*/
typedef NS_ERROR_ENUM(FIRMessagingErrorDomain, FIRMessagingError){
/// Unknown error.
FIRMessagingErrorUnknown = 0,
/// FIRMessaging couldn't validate request from this client.
FIRMessagingErrorAuthentication = 1,
/// InstanceID service cannot be accessed.
FIRMessagingErrorNoAccess = 2,
/// Request to InstanceID backend timed out.
FIRMessagingErrorTimeout = 3,
/// No network available to reach the servers.
FIRMessagingErrorNetwork = 4,
/// Another similar operation in progress, bailing this one.
FIRMessagingErrorOperationInProgress = 5,
/// Some parameters of the request were invalid.
FIRMessagingErrorInvalidRequest = 7,
/// Topic name is invalid for subscription/unsubscription.
FIRMessagingErrorInvalidTopicName = 8,
} NS_SWIFT_NAME(MessagingError);
/// Status for the downstream message received by the app.
typedef NS_ENUM(NSInteger, FIRMessagingMessageStatus) {
/// Unknown status.
FIRMessagingMessageStatusUnknown,
/// New downstream message received by the app.
FIRMessagingMessageStatusNew,
} NS_SWIFT_NAME(MessagingMessageStatus);
/**
* The APNs token type for the app. If the token type is set to `UNKNOWN`
* Firebase Messaging will implicitly try to figure out what the actual token type
* is from the provisioning profile.
* Unless you really need to specify the type, you should use the `APNSToken`
* property instead.
*/
typedef NS_ENUM(NSInteger, FIRMessagingAPNSTokenType) {
/// Unknown token type.
FIRMessagingAPNSTokenTypeUnknown,
/// Sandbox token type.
FIRMessagingAPNSTokenTypeSandbox,
/// Production token type.
FIRMessagingAPNSTokenTypeProd,
} NS_SWIFT_NAME(MessagingAPNSTokenType);
/// Information about a downstream message received by the app.
NS_SWIFT_NAME(MessagingMessageInfo)
@interface FIRMessagingMessageInfo : NSObject
/// The status of the downstream message
@property(nonatomic, readonly, assign) FIRMessagingMessageStatus status;
@end
@class FIRMessaging;
@class FIRMessagingExtensionHelper;
/**
* A protocol to handle token update or data message delivery from FCM.
*
*/
NS_SWIFT_NAME(MessagingDelegate)
@protocol FIRMessagingDelegate <NSObject>
@optional
/// This method will be called once a token is available, or has been refreshed. Typically it
/// will be called once per app start, but may be called more often, if token is invalidated or
/// updated. In this method, you should perform operations such as:
///
/// * Uploading the FCM token to your application server, so targeted notifications can be sent.
///
/// * Subscribing to any topics.
- (void)messaging:(FIRMessaging *)messaging
didReceiveRegistrationToken:(nullable NSString *)fcmToken
NS_SWIFT_NAME(messaging(_:didReceiveRegistrationToken:));
@end
/**
* Firebase Messaging lets you reliably deliver messages at no cost.
*
* To send or receive messages, the app must get a
* registration token. This token authorizes an
* app server to send messages to an app instance.
*
* In order to handle incoming Messaging messages, set the
* `UNUserNotificationCenter`'s `delegate` property
* and implement the appropriate methods.
*/
NS_SWIFT_NAME(Messaging)
@interface FIRMessaging : NSObject
/**
* Delegate to handle FCM token refreshes, and remote data messages received via FCM direct channel.
*/
@property(nonatomic, weak, nullable) id<FIRMessagingDelegate> delegate;
/**
* FIRMessaging
*
* @return An instance of Messaging.
*/
+ (instancetype)messaging NS_SWIFT_NAME(messaging());
/**
* Use the MessagingExtensionHelper to populate rich UI content for your notifications.
* For example, if an image URL is set in your notification payload or on the console,
* you can use the MessagingExtensionHelper instance returned from this method to render
* the image in your notification.
*
* @return An instance of MessagingExtensionHelper that handles the extensions API.
*/
+ (FIRMessagingExtensionHelper *)extensionHelper NS_SWIFT_NAME(serviceExtension())
NS_AVAILABLE(10.14, 10.0);
/**
* Unavailable. Use +messaging instead.
*/
- (instancetype)init __attribute__((unavailable("Use +messaging instead.")));
#pragma mark - APNs
/**
* This property is used to set the APNs Token received by the application delegate.
*
* Messaging uses method swizzling to ensure that the APNs token is set
* automatically. However, if you have disabled swizzling by setting
* `FirebaseAppDelegateProxyEnabled` to `NO` in your app's
* Info.plist, you should manually set the APNs token in your application
* delegate's `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`
* method.
*
* If you would like to set the type of the APNs token, rather than relying on
* automatic detection, see `setAPNSToken(_:type:)`.
*/
@property(nonatomic, copy, nullable) NSData *APNSToken NS_SWIFT_NAME(apnsToken);
/**
* Set the APNs token for the application. This token will be used to register
* with Firebase Messaging, and will be associated with the app's installation ID
* in the form of an FCM token.
*
* @param apnsToken The APNs token for the application.
* @param type The type of APNs token. Debug builds should use
* `MessagingAPNSTokenTypeSandbox`. Alternatively, you can supply
* `MessagingAPNSTokenTypeUnknown` to have the type automatically
* detected based on your provisioning profile.
*/
- (void)setAPNSToken:(NSData *)apnsToken type:(FIRMessagingAPNSTokenType)type;
#pragma mark - FCM Tokens
/**
* Is Firebase Messaging token auto generation enabled? If this flag is disabled, Firebase
* Messaging will not generate an FCM token automatically for message delivery.
*
* If this flag is disabled, Firebase Messaging does not generate new tokens automatically for
* message delivery. If this flag is enabled, FCM generates a registration token on application
* start when there is no existing valid token and periodically refreshes the token and sends
* data to the Firebase backend.
*
* This setting is persisted, and is applied on future invocations of your application. Once
* explicitly set, it overrides any settings in your Info.plist.
*
* By default, FCM automatic initialization is enabled. If you need to change the
* default (for example, because you want to prompt the user before getting a token),
* set `FirebaseMessagingAutoInitEnabled` to NO in your application's Info.plist.
*/
@property(nonatomic, assign, getter=isAutoInitEnabled) BOOL autoInitEnabled;
/**
* The FCM registration token is used to identify this device so that FCM can send notifications to
* it. It is associated with your APNs token when the APNs token is supplied, so messages sent to
* the FCM token will be delivered over APNs.
*
* The FCM registration token is sometimes refreshed automatically. In your Messaging delegate,
* the delegate method `messaging(_:didReceiveRegistrationToken:)` will be called once a token is
* available, or has been refreshed. Typically it should be called once per app start, but
* may be called more often if the token is invalidated or updated.
*
* Once you have an FCM registration token, you should send it to your application server, where
* it can be used to send notifications to your device.
*/
@property(nonatomic, readonly, nullable) NSString *FCMToken NS_SWIFT_NAME(fcmToken);
/**
* Asynchronously gets the default FCM registration token.
*
* This creates a Firebase Installations ID, if one does not exist, and sends information about the
* application and the device to the Firebase backend. A network connection is required for the
* method to succeed. To stop this, see `Messaging.isAutoInitEnabled`,
* `Messaging.delete(completion:)` and `Installations.delete(completion:)`.
*
* @param completion The completion handler to handle the token request.
*/
- (void)tokenWithCompletion:(void (^)(NSString *_Nullable token,
NSError *_Nullable error))completion;
/**
* Asynchronously deletes the default FCM registration token.
*
* This does not delete all tokens for non-default sender IDs, See `Messaging.delete(completion:)`
* for deleting all of them. To prevent token auto generation, see `Messaging.isAutoInitEnabled`.
*
* @param completion The completion handler to handle the token deletion.
*/
- (void)deleteTokenWithCompletion:(void (^)(NSError *_Nullable error))completion;
/**
* Retrieves an FCM registration token for a particular Sender ID. This can be used to allow
* multiple senders to send notifications to the same device. By providing a different Sender
* ID than your default when fetching a token, you can create a new FCM token which you can
* give to a different sender. Both tokens will deliver notifications to your device, and you
* can revoke a token when you need to.
*
* This registration token is not cached by FIRMessaging. FIRMessaging should have an APNs
* token set before calling this to ensure that notifications can be delivered via APNs using
* this FCM token. You may re-retrieve the FCM token once you have the APNs token set, to
* associate it with the FCM token. The default FCM token is automatically associated with
* the APNs token, if the APNs token data is available.
*
* This creates a Firebase Installations ID, if one does not exist, and sends information
* about the application and the device to the Firebase backend.
*
* @param senderID The Sender ID for a particular Firebase project.
* @param completion The completion handler to handle the token request.
*/
- (void)retrieveFCMTokenForSenderID:(NSString *)senderID
completion:(void (^)(NSString *_Nullable FCMToken,
NSError *_Nullable error))completion
NS_SWIFT_NAME(retrieveFCMToken(forSenderID:completion:));
/**
* Invalidates an FCM token for a particular Sender ID. That Sender ID cannot no longer send
* notifications to that FCM token. This does not delete the Firebase Installations ID that may have
* been created when generating the token. See `Installations.delete(completion:)`.
*
* @param senderID The senderID for a particular Firebase project.
* @param completion The completion handler to handle the token deletion.
*/
- (void)deleteFCMTokenForSenderID:(NSString *)senderID
completion:(void (^)(NSError *_Nullable error))completion
NS_SWIFT_NAME(deleteFCMToken(forSenderID:completion:));
#pragma mark - Topics
/**
* Asynchronously subscribes to a topic. This uses the default FCM registration token to identify
* the app instance and periodically sends data to the Firebase backend. To stop this, see
* `Messaging.delete(completion:)` and `Installations.delete(completion:)`.
*
* @param topic The name of the topic, for example, @"sports".
*/
- (void)subscribeToTopic:(NSString *)topic NS_SWIFT_NAME(subscribe(toTopic:));
/**
* Asynchronously subscribe to the provided topic, retrying on failure. This uses the default FCM
* registration token to identify the app instance and periodically sends data to the Firebase
* backend. To stop this, see `Messaging.delete(completion:)` and
* `Installations.delete(completion:)`.
*
* @param topic The topic name to subscribe to, for example, @"sports".
* @param completion The completion that is invoked once the subscribe call ends.
* On success, the error parameter is always `nil`. Otherwise, an
* appropriate error object is returned.
*/
- (void)subscribeToTopic:(nonnull NSString *)topic
completion:(void (^_Nullable)(NSError *_Nullable error))completion;
/**
* Asynchronously unsubscribe from a topic. This uses a FCM Token
* to identify the app instance and periodically sends data to the Firebase backend. To stop this,
* see `Messaging.delete(completion:)` and `Installations.delete(completion:)`.
*
* @param topic The name of the topic, for example @"sports".
*/
- (void)unsubscribeFromTopic:(NSString *)topic NS_SWIFT_NAME(unsubscribe(fromTopic:));
/**
* Asynchronously unsubscribe from the provided topic, retrying on failure. This uses a FCM Token
* to identify the app instance and periodically sends data to the Firebase backend. To stop this,
* see `Messaging.delete(completion:)` and `Installations.delete(completion:)`.
*
* @param topic The topic name to unsubscribe from, for example @"sports".
* @param completion The completion that is invoked once the unsubscribe call ends.
* In case of success, nil error is returned. Otherwise, an
* appropriate error object is returned.
*/
- (void)unsubscribeFromTopic:(nonnull NSString *)topic
completion:(void (^_Nullable)(NSError *_Nullable error))completion;
#pragma mark - Analytics
/**
* Use this to track message delivery and analytics for messages, typically
* when you receive a notification in `application:didReceiveRemoteNotification:`.
* However, you only need to call this if you set the `FirebaseAppDelegateProxyEnabled`
* flag to `NO` in your Info.plist. If `FirebaseAppDelegateProxyEnabled` is either missing
* or set to `YES` in your Info.plist, the library will call this automatically.
*
* @param message The downstream message received by the application.
*
* @return Information about the downstream message.
*/
- (FIRMessagingMessageInfo *)appDidReceiveMessage:(NSDictionary *)message;
#pragma mark - GDPR
/**
* Deletes all the tokens and checkin data of the Firebase project and related data on the server
* side. A network connection is required for the method to succeed.
*
* This does not delete the Firebase Installations ID. See `Installations.delete(completion:)`.
* To prevent token auto generation, see `Messaging.isAutoInitEnabled`.
*
* @param completion A completion handler which is invoked when the operation completes. `error ==
* nil` indicates success.
*/
- (void)deleteDataWithCompletion:(void (^)(NSError *__nullable error))completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class UNMutableNotificationContent, UNNotificationContent;
#if __has_include(<UserNotifications/UserNotifications.h>)
#import <UserNotifications/UserNotifications.h>
#endif
NS_ASSUME_NONNULL_BEGIN
/// This class is used to automatically populate a notification with an image if it is
/// specified in the notification body via the `image` parameter. Images and other
/// rich content can be populated manually without the use of this class. See the
/// `UNNotificationServiceExtension` type for more details.
__OSX_AVAILABLE(10.14) @interface FIRMessagingExtensionHelper : NSObject
/// Call this API to complete your notification content modification. If you like to
/// overwrite some properties of the content instead of using the default payload,
/// make sure to make your customized motification to the content before passing it to
/// this call.
- (void)populateNotificationContent:(UNMutableNotificationContent *)content
withContentHandler:(void (^)(UNNotificationContent *_Nonnull))contentHandler;
/// Exports delivery metrics to BigQuery. Call this API to enable logging delivery of alert
/// notification or background notification and export to BigQuery.
/// If you log alert notifications, enable Notification Service Extension and calls this API
/// under `UNNotificationServiceExtension didReceiveNotificationRequest: withContentHandler:`.
/// If you log background notifications, call the API under `UIApplicationDelegate
/// application:didReceiveRemoteNotification:fetchCompletionHandler:`.
- (void)exportDeliveryMetricsToBigQueryWithMessageInfo:(NSDictionary *)info;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessaging.h"
#import "FIRMessagingExtensionHelper.h"

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeDeviceID</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeOtherDataTypes</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeOtherDiagnosticData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Represents an APNS device token and whether its environment is for sandbox.
* It can read from and write to an NSDictionary for simple serialization.
*/
@interface FIRMessagingAPNSInfo : NSObject <NSSecureCoding, NSCopying>
/// The APNs device token, provided by the OS to the application delegate
@property(nonatomic, readonly, copy) NSData *deviceToken;
/// Represents whether or not this is deviceToken is for the sandbox
/// environment, or production.
@property(nonatomic, readonly, getter=isSandbox) BOOL sandbox;
/**
* Initializes the receiver with an APNs device token, and boolean
* representing whether that token is for the sandbox environment.
*
* @param deviceToken The APNs device token typically provided by the
* operating system.
* @param isSandbox YES if the APNs device token is for the sandbox
* environment, or NO if it is for production.
* @return An instance of FIRInstanceIDAPNSInfo.
*/
- (instancetype)initWithDeviceToken:(NSData *)deviceToken isSandbox:(BOOL)isSandbox;
/**
* Initializes the receiver from a token options dictionary containing data
* within the `kFIRInstanceIDTokenOptionsAPNSKey` and
* `kFIRInstanceIDTokenOptionsAPNSIsSandboxKey` keys. The token should be an
* NSData blob, and the sandbox value should be an NSNumber
* representing a boolean value.
*
* @param dictionary A dictionary containing values under the keys
* `kFIRInstanceIDTokenOptionsAPNSKey` and
* `kFIRInstanceIDTokenOptionsAPNSIsSandboxKey`.
* @return An instance of FIRInstanceIDAPNSInfo, or nil if the
* dictionary data was invalid or missing.
*/
- (nullable instancetype)initWithTokenOptionsDictionary:(NSDictionary *)dictionary;
- (BOOL)isEqualToAPNSInfo:(FIRMessagingAPNSInfo *)otherInfo;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingAPNSInfo.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
/// The key used to find the APNs device token in an archive.
static NSString *const kFIRInstanceIDAPNSInfoTokenKey = @"device_token";
/// The key used to find the sandbox value in an archive.
static NSString *const kFIRInstanceIDAPNSInfoSandboxKey = @"sandbox";
@interface FIRMessagingAPNSInfo ()
/// The APNs device token, provided by the OS to the application delegate
@property(nonatomic, copy) NSData *deviceToken;
/// Represents whether or not this is deviceToken is for the sandbox
/// environment, or production.
@property(nonatomic, getter=isSandbox) BOOL sandbox;
@end
@implementation FIRMessagingAPNSInfo
- (instancetype)initWithDeviceToken:(NSData *)deviceToken isSandbox:(BOOL)isSandbox {
self = [super init];
if (self) {
_deviceToken = [deviceToken copy];
_sandbox = isSandbox;
}
return self;
}
- (instancetype)initWithTokenOptionsDictionary:(NSDictionary *)dictionary {
id deviceToken = dictionary[kFIRMessagingTokenOptionsAPNSKey];
if (![deviceToken isKindOfClass:[NSData class]]) {
return nil;
}
id isSandbox = dictionary[kFIRMessagingTokenOptionsAPNSIsSandboxKey];
if (![isSandbox isKindOfClass:[NSNumber class]]) {
return nil;
}
self = [super init];
if (self) {
_deviceToken = (NSData *)deviceToken;
_sandbox = ((NSNumber *)isSandbox).boolValue;
}
return self;
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
FIRMessagingAPNSInfo *clone = [[FIRMessagingAPNSInfo alloc] init];
clone.deviceToken = [_deviceToken copy];
clone.sandbox = _sandbox;
return clone;
}
#pragma mark - NSCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
id deviceToken = [aDecoder decodeObjectForKey:kFIRInstanceIDAPNSInfoTokenKey];
if (![deviceToken isKindOfClass:[NSData class]]) {
return nil;
}
BOOL isSandbox = [aDecoder decodeBoolForKey:kFIRInstanceIDAPNSInfoSandboxKey];
return [self initWithDeviceToken:(NSData *)deviceToken isSandbox:isSandbox];
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.deviceToken forKey:kFIRInstanceIDAPNSInfoTokenKey];
[aCoder encodeBool:self.sandbox forKey:kFIRInstanceIDAPNSInfoSandboxKey];
}
- (BOOL)isEqualToAPNSInfo:(FIRMessagingAPNSInfo *)otherInfo {
return ([self.deviceToken isEqualToData:otherInfo.deviceToken] &&
self.isSandbox == otherInfo.isSandbox);
}
@end

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
extern NSString *__nonnull const kFIRMessagingKeychainWildcardIdentifier;
NS_ASSUME_NONNULL_BEGIN
/**
* Wrapper around storing FCM auth data in iOS keychain.
*/
@interface FIRMessagingAuthKeychain : NSObject
/**
* Designated Initializer. Init a generic `SecClassGenericPassword` keychain with `identifier`
* as the `kSecAttrGeneric`.
*
* @param identifier The generic attribute to be used by the keychain.
*
* @return A Keychain object with `kSecAttrGeneric` attribute set to identifier.
*/
- (instancetype)initWithIdentifier:(NSString *)identifier;
/**
* Get keychain items matching the given service and account. The service and/or account
* can be a wildcard (`kFIRMessagingKeychainWildcardIdentifier`), which case the query
* will include all items matching any services and/or accounts.
*
* @param service The kSecAttrService used to save the password. Can be wildcard.
* @param account The kSecAttrAccount used to save the password. Can be wildcard.
*
* @return An array of |NSData|s matching the provided inputs.
*/
- (NSArray<NSData *> *)itemsMatchingService:(NSString *)service account:(NSString *)account;
/**
* Get keychain item for a given service and account.
*
* @param service The kSecAttrService used to save the password.
* @param account The kSecAttrAccount used to save the password.
*
* @return A cached keychain item for a given account and service, or nil if it was not
* found or could not be retrieved.
*/
- (NSData *)dataForService:(NSString *)service account:(NSString *)account;
/**
* Remove the cached items from the keychain matching the service, account and access group.
* In case the items do not exist, YES is returned but with a valid error object with code
* `errSecItemNotFound`.
*
* @param service The kSecAttrService used to save the password.
* @param account The kSecAttrAccount used to save the password.
* @param handler The callback handler which is invoked when the remove operation is complete, with
* an error if there is any.
*/
- (void)removeItemsMatchingService:(NSString *)service
account:(NSString *)account
handler:(nullable void (^)(NSError *error))handler;
/**
* Set the data for a given service and account.
* We use `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` which
* prevents backup and restore to iCloud, and works for app extension that can
* execute right after a device is restarted (and not unlocked).
*
* @param data The data to save.
* @param service The `kSecAttrService` used to save the password.
* @param account The `kSecAttrAccount` used to save the password.
* @param handler The callback handler which is invoked when the add operation is complete,
* with an error if there is any.
*
*/
- (void)setData:(NSData *)data
forService:(NSString *)service
account:(NSString *)account
handler:(nullable void (^)(NSError *))handler;
/*
* This method only sets the cache data of token.
* It is only used when users still use InstanceID to update token info
* After token refreshed by InstanceID, the storage is already updated but not the cache.
* use this method to update the cache.
*/
- (void)setCacheData:(NSData *)data forService:(NSString *)service account:(NSString *)account;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,234 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingAuthKeychain.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingKeychain.h"
/**
* The error type representing why we couldn't read data from the keychain.
*/
typedef NS_ENUM(int, FIRMessagingKeychainErrorType) {
kFIRMessagingKeychainErrorBadArguments = -1301,
};
NSString *const kFIRMessagingKeychainWildcardIdentifier = @"*";
@interface FIRMessagingAuthKeychain ()
@property(nonatomic, copy) NSString *generic;
// cachedKeychainData is keyed by service and account, the value is an array of NSData.
// It is used to cache the tokens per service, per account, as well as checkin data per service,
// per account inside the keychain.
@property(nonatomic, strong)
NSMutableDictionary<NSString *, NSMutableDictionary<NSString *, NSArray<NSData *> *> *>
*cachedKeychainData;
@end
@implementation FIRMessagingAuthKeychain
- (instancetype)initWithIdentifier:(NSString *)identifier {
self = [super init];
if (self) {
_generic = [identifier copy];
_cachedKeychainData = [[NSMutableDictionary alloc] init];
}
return self;
}
+ (NSMutableDictionary *)keychainQueryForService:(NSString *)service
account:(NSString *)account
generic:(NSString *)generic {
NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword};
NSMutableDictionary *finalQuery = [NSMutableDictionary dictionaryWithDictionary:query];
if ([generic length] && ![kFIRMessagingKeychainWildcardIdentifier isEqualToString:generic]) {
finalQuery[(__bridge NSString *)kSecAttrGeneric] = generic;
}
if ([account length] && ![kFIRMessagingKeychainWildcardIdentifier isEqualToString:account]) {
finalQuery[(__bridge NSString *)kSecAttrAccount] = account;
}
if ([service length] && ![kFIRMessagingKeychainWildcardIdentifier isEqualToString:service]) {
finalQuery[(__bridge NSString *)kSecAttrService] = service;
}
if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, *)) {
// Ensures that the keychain query behaves the same across all platforms.
// See go/firebase-macos-keychain-popups for details.
finalQuery[(__bridge id)kSecUseDataProtectionKeychain] = (__bridge id)kCFBooleanTrue;
}
return finalQuery;
}
- (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account {
return [[self class] keychainQueryForService:service account:account generic:self.generic];
}
- (NSArray<NSData *> *)itemsMatchingService:(NSString *)service account:(NSString *)account {
// If query wildcard service, it asks for all the results, which always query from keychain.
if (![service isEqualToString:kFIRMessagingKeychainWildcardIdentifier] &&
![account isEqualToString:kFIRMessagingKeychainWildcardIdentifier] &&
_cachedKeychainData[service][account]) {
// As long as service, account array exist, even it's empty, it means we've queried it before,
// returns the cache value.
return _cachedKeychainData[service][account];
}
NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
NSMutableArray<NSData *> *results;
keychainQuery[(__bridge id)kSecReturnData] = (__bridge id)kCFBooleanTrue;
#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION)
keychainQuery[(__bridge id)kSecReturnAttributes] = (__bridge id)kCFBooleanTrue;
keychainQuery[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
// FIRMessagingKeychain should only take a query and return a result, will handle the query here.
NSArray *passwordInfos =
CFBridgingRelease([[FIRMessagingKeychain sharedInstance] itemWithQuery:keychainQuery]);
#elif TARGET_OS_OSX || TARGET_OS_WATCH
keychainQuery[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
NSData *passwordInfos =
CFBridgingRelease([[FIRMessagingKeychain sharedInstance] itemWithQuery:keychainQuery]);
#endif
if (!passwordInfos) {
// Nothing was found, simply return from this sync block.
// Make sure to label the cache entry empty, signaling that we've queried this entry.
if ([service isEqualToString:kFIRMessagingKeychainWildcardIdentifier] ||
[account isEqualToString:kFIRMessagingKeychainWildcardIdentifier]) {
// Do not update cache if it's wildcard query.
return @[];
} else if (_cachedKeychainData[service]) {
[_cachedKeychainData[service] setObject:@[] forKey:account];
} else {
[_cachedKeychainData setObject:[@{account : @[]} mutableCopy] forKey:service];
}
return @[];
}
results = [[NSMutableArray alloc] init];
#if TARGET_OS_IOS || TARGET_OS_TV
NSInteger numPasswords = passwordInfos.count;
for (NSUInteger i = 0; i < numPasswords; i++) {
NSDictionary *passwordInfo = [passwordInfos objectAtIndex:i];
if (passwordInfo[(__bridge id)kSecValueData]) {
[results addObject:passwordInfo[(__bridge id)kSecValueData]];
}
}
#elif TARGET_OS_OSX || TARGET_OS_WATCH
[results addObject:passwordInfos];
#endif
// We query the keychain because it didn't exist in cache, now query is done, update the result in
// the cache.
if ([service isEqualToString:kFIRMessagingKeychainWildcardIdentifier] ||
[account isEqualToString:kFIRMessagingKeychainWildcardIdentifier]) {
// Do not update cache if it's wildcard query.
return [results copy];
} else if (_cachedKeychainData[service]) {
[_cachedKeychainData[service] setObject:[results copy] forKey:account];
} else {
NSMutableDictionary *entry = [@{account : [results copy]} mutableCopy];
[_cachedKeychainData setObject:entry forKey:service];
}
return [results copy];
}
- (NSData *)dataForService:(NSString *)service account:(NSString *)account {
NSArray<NSData *> *items = [self itemsMatchingService:service account:account];
// If items is nil or empty, nil will be returned.
return items.firstObject;
}
- (void)removeItemsMatchingService:(NSString *)service
account:(NSString *)account
handler:(void (^)(NSError *error))handler {
if ([service isEqualToString:kFIRMessagingKeychainWildcardIdentifier]) {
// Delete all keychain items.
_cachedKeychainData = [[NSMutableDictionary alloc] init];
} else if ([account isEqualToString:kFIRMessagingKeychainWildcardIdentifier]) {
// Delete all entries under service,
if (_cachedKeychainData[service]) {
_cachedKeychainData[service] = [[NSMutableDictionary alloc] init];
}
} else if (_cachedKeychainData[service]) {
// We should keep the service/account entry instead of nil so we know
// it's "empty entry" instead of "not query from keychain yet".
[_cachedKeychainData[service] setObject:@[] forKey:account];
} else {
[_cachedKeychainData setObject:[@{account : @[]} mutableCopy] forKey:service];
}
NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
[[FIRMessagingKeychain sharedInstance] removeItemWithQuery:keychainQuery handler:handler];
}
- (void)setData:(NSData *)data
forService:(NSString *)service
account:(NSString *)account
handler:(void (^)(NSError *))handler {
if ([service isEqualToString:kFIRMessagingKeychainWildcardIdentifier] ||
[account isEqualToString:kFIRMessagingKeychainWildcardIdentifier]) {
if (handler) {
handler([NSError errorWithDomain:kFIRMessagingKeychainErrorDomain
code:kFIRMessagingKeychainErrorBadArguments
userInfo:nil]);
}
return;
}
[self removeItemsMatchingService:service
account:account
handler:^(NSError *error) {
if (error) {
if (handler) {
handler(error);
}
return;
}
if (data.length > 0) {
NSMutableDictionary *keychainQuery =
[self keychainQueryForService:service account:account];
keychainQuery[(__bridge id)kSecValueData] = data;
keychainQuery[(__bridge id)kSecAttrAccessible] =
(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
[[FIRMessagingKeychain sharedInstance] addItemWithQuery:keychainQuery
handler:handler];
}
}];
// Set the cache value. This must happen after removeItemsMatchingService:account:handler was
// called, so the cache value was reset before setting a new value.
if (_cachedKeychainData[service]) {
if (_cachedKeychainData[service][account]) {
_cachedKeychainData[service][account] = @[ data ];
} else {
[_cachedKeychainData[service] setObject:@[ data ] forKey:account];
}
} else {
[_cachedKeychainData setObject:[@{account : @[ data ]} mutableCopy] forKey:service];
}
}
- (void)setCacheData:(NSData *)data forService:(NSString *)service account:(NSString *)account {
if (_cachedKeychainData[service]) {
if (_cachedKeychainData[service][account]) {
_cachedKeychainData[service][account] = @[ data ];
} else {
[_cachedKeychainData[service] setObject:@[ data ] forKey:account];
}
} else {
[_cachedKeychainData setObject:[@{account : @[ data ]} mutableCopy] forKey:service];
}
}
@end

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinService.h"
NS_ASSUME_NONNULL_BEGIN
@class FIRMessagingCheckinPreferences;
/**
* @related FIRInstanceIDCheckinService
*
* The completion handler invoked once the fetch from Checkin server finishes.
* For successful fetches we returned checkin information by the checkin service
* and `nil` error, else we return the appropriate error object as reported by the
* Checkin Service.
*
* @param checkinPreferences The checkin preferences as fetched from the server.
* @param error The error object which fetching GServices data.
*/
typedef void (^FIRMessagingDeviceCheckinCompletion)(
FIRMessagingCheckinPreferences *_Nullable checkinPreferences, NSError *_Nullable error);
/**
* FIRMessagingAuthService is responsible for retrieving, caching, and supplying checkin info
* for the rest of Instance ID. A checkin can be scheduled, meaning that it will keep retrying the
* checkin request until it is successful. A checkin can also be requested directly, with a
* completion handler.
*/
@interface FIRMessagingAuthService : NSObject
#pragma mark - Checkin Service
- (BOOL)hasCheckinPlist;
/**
* Checks if the current deviceID and secret are valid or not.
*
* @return YES if the checkin credentials are valid else NO.
*/
- (BOOL)hasValidCheckinInfo;
/**
* Fetch checkin info from the server. This would usually refresh the existing
* checkin credentials for the current app.
*
* @param handler The completion handler to invoke once the checkin info has been
* refreshed.
*/
- (void)fetchCheckinInfoWithHandler:(nullable FIRMessagingDeviceCheckinCompletion)handler;
/**
* Schedule checkin. Will hit the network only if the currently loaded checkin
* preferences are stale.
*
* @param immediately YES if we want it to be scheduled immediately else NO.
*/
- (void)scheduleCheckin:(BOOL)immediately;
/**
* Returns the checkin preferences currently loaded in memory. The Checkin preferences
* can be either valid or invalid.
*
* @return The checkin preferences loaded in memory.
*/
- (FIRMessagingCheckinPreferences *)checkinPreferences;
/**
* Cancels any ongoing checkin fetch, if any.
*/
- (void)stopCheckinRequest;
/**
* Resets the checkin information.
*
* @param handler The callback handler which is invoked when checkin reset is complete,
* with an error if there is any.
*/
- (void)resetCheckinWithHandler:(void (^)(NSError *error))handler;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,301 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingAuthService.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinPreferences.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinStore.h"
// Max time interval between checkin retry in seconds.
static const int64_t kMaxCheckinRetryIntervalInSeconds = 1 << 5;
@interface FIRMessagingAuthService ()
// Used to retrieve and cache the checkin info to disk and Keychain.
@property(nonatomic, readwrite, strong) FIRMessagingCheckinStore *checkinStore;
// Used to perform single checkin fetches.
@property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
// The current checkin info. It will be compared to what is retrieved to determine whether it is
// different than what is in the cache.
@property(nonatomic, readwrite, strong) FIRMessagingCheckinPreferences *checkinPreferences;
// This array will track multiple handlers waiting for checkin to be performed. When a checkin
// request completes, all the handlers will be notified.
// Changes to the checkinHandlers array should happen in a thread-safe manner.
@property(nonatomic, readonly, strong)
NSMutableArray<FIRMessagingDeviceCheckinCompletion> *checkinHandlers;
// This is set to true if there is a checkin request in-flight.
@property(atomic, readwrite, assign) BOOL isCheckinInProgress;
// This timer is used a perform checkin retries. It is cancellable.
@property(atomic, readwrite, strong) NSTimer *scheduledCheckinTimer;
// The number of times checkin has been retried during a scheduled checkin.
@property(atomic, readwrite, assign) int checkinRetryCount;
@end
@implementation FIRMessagingAuthService
- (instancetype)init {
self = [super init];
if (self) {
_checkinStore = [[FIRMessagingCheckinStore alloc] init];
_checkinPreferences = [_checkinStore cachedCheckinPreferences];
_checkinService = [[FIRMessagingCheckinService alloc] init];
_checkinHandlers = [[NSMutableArray alloc] init];
}
return self;
}
- (void)dealloc {
[_scheduledCheckinTimer invalidate];
}
#pragma mark - Schedule Checkin
- (BOOL)hasCheckinPlist {
return [_checkinStore hasCheckinPlist];
}
- (void)scheduleCheckin:(BOOL)immediately {
// Checkin is still valid, so a remote checkin is not required.
if ([self.checkinPreferences hasValidCheckinInfo]) {
return;
}
// Checkin is already scheduled, so this (non-immediate) request can be ignored.
if (!immediately && [self.scheduledCheckinTimer isValid]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAuthService000,
@"Checkin sync already scheduled. Will not schedule.");
return;
}
if (immediately) {
[self performScheduledCheckin];
} else {
int64_t checkinRetryDuration = [self calculateNextCheckinRetryIntervalInSeconds];
[self startCheckinTimerWithDuration:(NSTimeInterval)checkinRetryDuration];
}
}
- (void)startCheckinTimerWithDuration:(NSTimeInterval)timerDuration {
self.scheduledCheckinTimer =
[NSTimer scheduledTimerWithTimeInterval:timerDuration
target:self
selector:@selector(onScheduledCheckinTimerFired:)
userInfo:nil
repeats:NO];
// Add some tolerance to the timer, to allow iOS to be more flexible with this timer
self.scheduledCheckinTimer.tolerance = 0.5;
}
- (void)clearScheduledCheckinTimer {
[self.scheduledCheckinTimer invalidate];
self.scheduledCheckinTimer = nil;
}
- (void)onScheduledCheckinTimerFired:(NSTimer *)timer {
[self performScheduledCheckin];
}
- (void)performScheduledCheckin {
// No checkin scheduled as of now.
[self clearScheduledCheckinTimer];
// Checkin is still valid, so a remote checkin is not required.
if ([self.checkinPreferences hasValidCheckinInfo]) {
return;
}
FIRMessaging_WEAKIFY(self);
[self fetchCheckinInfoWithHandler:^(FIRMessagingCheckinPreferences *_Nullable checkinPreferences,
NSError *_Nullable error) {
FIRMessaging_STRONGIFY(self);
self.checkinRetryCount++;
if (error) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAuthService001, @"Checkin error %@.", error);
dispatch_async(dispatch_get_main_queue(), ^{
// Schedule another checkin
[self scheduleCheckin:NO];
});
} else {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAuthService002, @"Checkin success.");
}
}];
}
- (int64_t)calculateNextCheckinRetryIntervalInSeconds {
// persistent failures can lead to overflow prevent that.
if (self.checkinRetryCount >= 10) {
return kMaxCheckinRetryIntervalInSeconds;
}
return MIN(1 << self.checkinRetryCount, kMaxCheckinRetryIntervalInSeconds);
}
#pragma mark - Checkin Service
- (BOOL)hasValidCheckinInfo {
return [self.checkinPreferences hasValidCheckinInfo];
}
- (void)fetchCheckinInfoWithHandler:(nullable FIRMessagingDeviceCheckinCompletion)handler {
// Perform any changes to self.checkinHandlers and _isCheckinInProgress in a thread-safe way.
@synchronized(self) {
[self.checkinHandlers addObject:[handler copy]];
if (_isCheckinInProgress) {
// Nothing more to do until our checkin request is done
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAuthServiceCheckinInProgress,
@"Checkin is in progress\n");
return;
}
}
// Checkin is still valid, so a remote checkin is not required.
if ([self.checkinPreferences hasValidCheckinInfo]) {
[self notifyCheckinHandlersWithCheckin:self.checkinPreferences error:nil];
return;
}
@synchronized(self) {
_isCheckinInProgress = YES;
}
[self.checkinService
checkinWithExistingCheckin:self.checkinPreferences
completion:^(FIRMessagingCheckinPreferences *checkinPreferences,
NSError *error) {
@synchronized(self) {
self->_isCheckinInProgress = NO;
}
if (error) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAuthService003,
@"Failed to checkin device %@", error);
[self notifyCheckinHandlersWithCheckin:nil error:error];
return;
}
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAuthService004,
@"Successfully got checkin credentials");
BOOL hasSameCachedPreferences =
[self cachedCheckinMatchesCheckin:checkinPreferences];
checkinPreferences.hasPreCachedAuthCredentials = hasSameCachedPreferences;
// Update to the most recent checkin preferences
self.checkinPreferences = checkinPreferences;
// Save the checkin info to disk
// Keychain might not be accessible, so confirm that checkin preferences can
// be saved
[self->_checkinStore
saveCheckinPreferences:checkinPreferences
handler:^(NSError *checkinSaveError) {
if (checkinSaveError && !hasSameCachedPreferences) {
// The checkin info was new, but it couldn't be
// written to the Keychain. Delete any stuff that was
// cached in memory. This doesn't delete any
// previously persisted preferences.
FIRMessagingLoggerError(
kFIRMessagingMessageCodeService004,
@"Unable to save checkin info, resetting "
@"checkin preferences "
"in memory.");
[checkinPreferences reset];
[self
notifyCheckinHandlersWithCheckin:nil
error:
checkinSaveError];
} else {
// The checkin is either new, or it was the same (and
// it couldn't be saved). Either way, report that the
// checkin preferences were received successfully.
[self notifyCheckinHandlersWithCheckin:
checkinPreferences
error:nil];
if (!hasSameCachedPreferences) {
// Checkin is new.
// Notify any listeners that might be waiting for
// checkin to be fetched, such as Firebase
// Messaging (for its MCS connection).
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter]
postNotificationName:
kFIRMessagingCheckinFetchedNotification
object:nil];
});
}
}
}];
}];
}
- (FIRMessagingCheckinPreferences *)checkinPreferences {
return _checkinPreferences;
}
- (void)stopCheckinRequest {
[self.checkinService stopFetching];
}
- (void)resetCheckinWithHandler:(void (^)(NSError *error))handler {
[_checkinStore removeCheckinPreferencesWithHandler:^(NSError *error) {
if (!error) {
self.checkinPreferences = nil;
}
if (handler) {
handler(error);
}
}];
}
#pragma mark - Private
/**
* Goes through the current list of checkin handlers and fires them with the same checkin and/or
* error info. The checkin handlers will get cleared after.
*/
- (void)notifyCheckinHandlersWithCheckin:(nullable FIRMessagingCheckinPreferences *)checkin
error:(nullable NSError *)error {
@synchronized(self) {
for (FIRMessagingDeviceCheckinCompletion handler in self.checkinHandlers) {
handler(checkin, error);
}
[self.checkinHandlers removeAllObjects];
}
}
- (void)setCheckinHandlers:(NSMutableArray<FIRMessagingDeviceCheckinCompletion> *)checkinHandlers {
NSLog(@"%lu", (unsigned long)self.checkinHandlers.count);
}
/**
* Given a |checkin|, it will compare it to the current checkinPreferences to see if the
* deviceID and secretToken are the same.
*/
- (BOOL)cachedCheckinMatchesCheckin:(FIRMessagingCheckinPreferences *)checkin {
if (self.checkinPreferences && checkin) {
return ([self.checkinPreferences.deviceID isEqualToString:checkin.deviceID] &&
[self.checkinPreferences.secretToken isEqualToString:checkin.secretToken]);
}
return NO;
}
@end

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@interface FIRMessagingBackupExcludedPlist : NSObject
/**
* Caches the plist contents in memory so we don't hit the disk each time we want
* to query something in the plist. This is loaded lazily i.e. if you write to the
* plist the contents you want to write will be stored here if the write was
* successful. The other case where it is loaded is if you read the plist contents
* by calling `contentAsDictionary`.
*
* In case you write to the plist and then try to read the file using
* `contentAsDictionary` we would just return the cachedPlistContents since it would
* represent the disk contents.
*/
@property(nonatomic, readonly, strong) NSDictionary *cachedPlistContents;
/**
* Init a backup excluded plist file.
*
* @param fileName The filename for the plist file.
* @param subDirectory The subdirectory in Application Support to save the plist.
*
* @return Helper which allows to read write data to a backup excluded plist.
*/
- (instancetype)initWithFileName:(NSString *)fileName subDirectory:(NSString *)subDirectory;
/**
* Write dictionary data to the backup excluded plist file. If the file does not exist
* it would be created before writing to it.
*
* @param dict The data to be written to the plist.
* @param error The error object if any while writing the data.
*
* @return YES if the write was successful else NO.
*/
- (BOOL)writeDictionary:(NSDictionary *)dict error:(NSError **)error;
/**
* Delete the backup excluded plist created with the above filename.
*
* @param error The error object if any while deleting the file.
*
* @return YES If the delete was successful else NO.
*/
- (BOOL)deleteFile:(NSError **)error;
/**
* The contents of the plist file. We also store the contents of the file in-memory.
* If the in-memory contents are valid we return the in-memory contents else we read
* the file from disk.
*
* @return A dictionary object that contains the contents of the plist file if the file
* exists else nil.
*/
- (NSDictionary *)contentAsDictionary;
/**
* Check if the plist exists on the disk or not.
*
* @return YES if the file exists on the disk else NO.
*/
- (BOOL)doesFileExist;
@end

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingBackupExcludedPlist.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
@interface FIRMessagingBackupExcludedPlist ()
@property(nonatomic, readwrite, copy) NSString *fileName;
@property(nonatomic, readwrite, copy) NSString *subDirectoryName;
@property(nonatomic, readwrite, strong) NSDictionary *cachedPlistContents;
@end
@implementation FIRMessagingBackupExcludedPlist
- (instancetype)initWithFileName:(NSString *)fileName subDirectory:(NSString *)subDirectory {
self = [super init];
if (self) {
_fileName = [fileName copy];
_subDirectoryName = [subDirectory copy];
}
return self;
}
- (BOOL)writeDictionary:(NSDictionary *)dict error:(NSError **)error {
NSString *path = [self plistPathInDirectory];
if (![dict writeToFile:path atomically:YES]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeBackupExcludedPlist000,
@"Failed to write to %@.plist", self.fileName);
return NO;
}
// Successfully wrote contents -- change the in-memory contents
self.cachedPlistContents = [dict copy];
NSURL *URL = [NSURL fileURLWithPath:path];
if (error) {
*error = nil;
}
NSDictionary *preferences = [URL resourceValuesForKeys:@[ NSURLIsExcludedFromBackupKey ]
error:error];
if ([preferences[NSURLIsExcludedFromBackupKey] boolValue]) {
return YES;
}
BOOL success = [URL setResourceValue:@(YES) forKey:NSURLIsExcludedFromBackupKey error:error];
if (!success) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeBackupExcludedPlist001,
@"Error excluding %@ from backup, %@", [URL lastPathComponent],
error ? *error : @"");
}
return success;
}
- (BOOL)deleteFile:(NSError **)error {
BOOL success = YES;
NSString *path = [self plistPathInDirectory];
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
success = [[NSFileManager defaultManager] removeItemAtPath:path error:error];
}
// remove the in-memory contents
self.cachedPlistContents = nil;
return success;
}
- (NSDictionary *)contentAsDictionary {
if (!self.cachedPlistContents) {
NSString *path = [self plistPathInDirectory];
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
self.cachedPlistContents = [[NSDictionary alloc] initWithContentsOfFile:path];
}
}
return self.cachedPlistContents;
}
- (BOOL)doesFileExist {
NSString *path = [self plistPathInDirectory];
return [[NSFileManager defaultManager] fileExistsAtPath:path];
}
#pragma mark - Private
- (NSString *)plistPathInDirectory {
NSArray *directoryPaths;
NSString *plistNameWithExtension = [NSString stringWithFormat:@"%@.plist", self.fileName];
directoryPaths =
NSSearchPathForDirectoriesInDomains([self supportedDirectory], NSUserDomainMask, YES);
NSArray *components = @[ directoryPaths.lastObject, _subDirectoryName, plistNameWithExtension ];
return [NSString pathWithComponents:components];
}
- (NSSearchPathDirectory)supportedDirectory {
#if TARGET_OS_TV
return NSCachesDirectory;
#else
return NSApplicationSupportDirectory;
#endif
}
@end

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
FOUNDATION_EXPORT const NSTimeInterval kFIRMessagingDefaultCheckinInterval;
/**
* The preferences InstanceID loads from checkin server. The deviceID and secret that checkin
* provides is used to authenticate all future requests to the server. Besides the deviceID
* and secret the other information that checkin provides is stored in a plist on the device.
* The deviceID and secret are persisted in the device keychain.
*/
@interface FIRMessagingCheckinPreferences : NSObject
/**
* DeviceID and secretToken are the checkin auth credentials and are stored in the Keychain.
*/
@property(nonatomic, readonly, copy) NSString *deviceID;
@property(nonatomic, readonly, copy) NSString *secretToken;
/**
* All the other checkin preferences other than deviceID and secret are stored in a plist.
*/
@property(nonatomic, readonly, copy) NSString *deviceDataVersion;
@property(nonatomic, readonly, copy) NSString *digest;
@property(nonatomic, readonly, copy) NSString *versionInfo;
@property(nonatomic, readonly, assign) int64_t lastCheckinTimestampMillis;
/**
* The content retrieved from checkin server that should be persisted in a plist. This
* doesn't contain the deviceID and secret which are stored in the Keychain since they
* should be more private.
*
* @return The checkin preferences that should be persisted in a plist.
*/
- (NSDictionary *)checkinPlistContents;
/**
* Return whether checkin info exists, valid or not.
*/
- (BOOL)hasCheckinInfo;
/**
* Verify if checkin preferences are valid or not.
*
* @return YES if valid checkin preferences else NO.
*/
- (BOOL)hasValidCheckinInfo;
- (BOOL)hasPreCachedAuthCredentials;
- (void)setHasPreCachedAuthCredentials:(BOOL)hasPreCachedAuthCredentials;
/**
* Parse the checkin auth credentials saved in the Keychain to initialize checkin
* preferences.
*
* @param keychainContent The checkin auth credentials saved in the Keychain.
*
* @return A valid checkin preferences object if the checkin auth credentials in the
* keychain can be parsed successfully else nil.
*/
+ (FIRMessagingCheckinPreferences *)preferencesFromKeychainContents:(NSString *)keychainContent;
/**
* Default initializer for InstanceID checkin preferences.
*
* @param deviceID The deviceID for the app.
* @param secretToken The secret token the app uses to authenticate with the server.
*
* @return A checkin preferences object with given deviceID and secretToken.
*/
- (instancetype)initWithDeviceID:(NSString *)deviceID secretToken:(NSString *)secretToken;
/**
* Update checkin preferences from the preferences dict persisted as a plist. The dict contains
* all the checkin preferences retrieved from the server except the deviceID and secret which
* are stored in the Keychain.
*
* @param checkinPlistContent The checkin preferences saved in a plist on the disk.
*/
- (void)updateWithCheckinPlistContents:(NSDictionary *)checkinPlistContent;
/**
* Reset the current checkin preferences object.
*/
- (void)reset;
/**
* The string that contains the checkin auth credentials i.e. deviceID and secret. This
* needs to be stored in the Keychain.
*
* @return The checkin auth credential string containing the deviceID and secret.
*/
- (NSString *)checkinKeychainContent;
@end

View File

@@ -0,0 +1,168 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinPreferences.h"
#import <GoogleUtilities/GULUserDefaults.h>
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinService.h"
const NSTimeInterval kFIRMessagingDefaultCheckinInterval = 7 * 24 * 60 * 60; // 7 days.
static NSString *const kCheckinKeychainContentSeparatorString = @"|";
@interface FIRMessagingCheckinPreferences ()
@property(nonatomic, readwrite, copy) NSString *deviceID;
@property(nonatomic, readwrite, copy) NSString *secretToken;
@property(nonatomic, readwrite, copy) NSString *digest;
@property(nonatomic, readwrite, copy) NSString *versionInfo;
@property(nonatomic, readwrite, copy) NSString *deviceDataVersion;
@property(nonatomic, readwrite, strong) NSMutableDictionary *gServicesData;
@property(nonatomic, readwrite, assign) int64_t lastCheckinTimestampMillis;
// This flag indicates that we have already saved the above deviceID and secret
// to our keychain and hence we don't need to save again. This is helpful since
// on checkin refresh we can avoid writing to the Keychain which can sometimes
// be very buggy. For info check this https://forums.developer.apple.com/thread/4743
@property(nonatomic, readwrite, assign) BOOL hasPreCachedAuthCredentials;
@end
@implementation FIRMessagingCheckinPreferences
+ (FIRMessagingCheckinPreferences *)preferencesFromKeychainContents:(NSString *)keychainContent {
NSString *deviceID = [self checkinDeviceIDFromKeychainContent:keychainContent];
NSString *secret = [self checkinSecretFromKeychainContent:keychainContent];
if ([deviceID length] && [secret length]) {
return [[FIRMessagingCheckinPreferences alloc] initWithDeviceID:deviceID secretToken:secret];
} else {
return nil;
}
}
- (instancetype)initWithDeviceID:(NSString *)deviceID secretToken:(NSString *)secretToken {
self = [super init];
if (self) {
self.deviceID = [deviceID copy];
self.secretToken = [secretToken copy];
}
return self;
}
- (void)reset {
self.deviceID = nil;
self.secretToken = nil;
self.digest = nil;
self.versionInfo = nil;
self.gServicesData = nil;
self.deviceDataVersion = nil;
self.lastCheckinTimestampMillis = 0;
}
- (NSDictionary *)checkinPlistContents {
NSMutableDictionary *checkinPlistContents = [NSMutableDictionary dictionary];
checkinPlistContents[kFIRMessagingDigestStringKey] = self.digest ?: @"";
checkinPlistContents[kFIRMessagingVersionInfoStringKey] = self.versionInfo ?: @"";
checkinPlistContents[kFIRMessagingDeviceDataVersionKey] = self.deviceDataVersion ?: @"";
checkinPlistContents[kFIRMessagingLastCheckinTimeKey] = @(self.lastCheckinTimestampMillis);
checkinPlistContents[kFIRMessagingGServicesDictionaryKey] =
[self.gServicesData count] ? self.gServicesData : @{};
return checkinPlistContents;
}
- (BOOL)hasCheckinInfo {
return (self.deviceID.length && self.secretToken.length);
}
- (BOOL)hasValidCheckinInfo {
int64_t currentTimestampInMillis = FIRMessagingCurrentTimestampInMilliseconds();
int64_t timeSinceLastCheckinInMillis = currentTimestampInMillis - self.lastCheckinTimestampMillis;
BOOL hasCheckinInfo = [self hasCheckinInfo];
NSString *lastLocale = [[GULUserDefaults standardUserDefaults]
stringForKey:kFIRMessagingInstanceIDUserDefaultsKeyLocale];
// If it's app's first time open and checkin is already fetched and no locale information is
// stored, then checkin info is valid. We should not checkin again because locale is considered
// "changed".
if (hasCheckinInfo && !lastLocale) {
NSString *currentLocale = FIRMessagingCurrentLocale();
[[GULUserDefaults standardUserDefaults] setObject:currentLocale
forKey:kFIRMessagingInstanceIDUserDefaultsKeyLocale];
return YES;
}
// If locale has changed, checkin info is no longer valid.
// Also update locale information if changed. (Only do it here not in token refresh)
if (FIRMessagingHasLocaleChanged()) {
NSString *currentLocale = FIRMessagingCurrentLocale();
[[GULUserDefaults standardUserDefaults] setObject:currentLocale
forKey:kFIRMessagingInstanceIDUserDefaultsKeyLocale];
return NO;
}
return (hasCheckinInfo &&
(timeSinceLastCheckinInMillis / 1000.0 < kFIRMessagingDefaultCheckinInterval));
}
- (void)setHasPreCachedAuthCredentials:(BOOL)hasPreCachedAuthCredentials {
_hasPreCachedAuthCredentials = hasPreCachedAuthCredentials;
}
- (NSString *)checkinKeychainContent {
if ([self.deviceID length] && [self.secretToken length]) {
return [NSString stringWithFormat:@"%@%@%@", self.deviceID,
kCheckinKeychainContentSeparatorString, self.secretToken];
} else {
return nil;
}
}
- (void)updateWithCheckinPlistContents:(NSDictionary *)checkinPlistContent {
for (NSString *key in checkinPlistContent) {
if ([kFIRMessagingDigestStringKey isEqualToString:key]) {
self.digest = [checkinPlistContent[key] copy];
} else if ([kFIRMessagingVersionInfoStringKey isEqualToString:key]) {
self.versionInfo = [checkinPlistContent[key] copy];
} else if ([kFIRMessagingLastCheckinTimeKey isEqualToString:key]) {
self.lastCheckinTimestampMillis = [checkinPlistContent[key] longLongValue];
} else if ([kFIRMessagingGServicesDictionaryKey isEqualToString:key]) {
self.gServicesData = [checkinPlistContent[key] mutableCopy];
} else if ([kFIRMessagingDeviceDataVersionKey isEqualToString:key]) {
self.deviceDataVersion = [checkinPlistContent[key] copy];
}
// Otherwise we have some keys we don't care about
}
}
+ (NSString *)checkinDeviceIDFromKeychainContent:(NSString *)keychainContent {
return [self checkinKeychainContent:keychainContent forIndex:0];
}
+ (NSString *)checkinSecretFromKeychainContent:(NSString *)keychainContent {
return [self checkinKeychainContent:keychainContent forIndex:1];
}
+ (NSString *)checkinKeychainContent:(NSString *)keychainContent forIndex:(int)index {
NSArray *keychainComponents =
[keychainContent componentsSeparatedByString:kCheckinKeychainContentSeparatorString];
if (index >= 0 && index < 2 && [keychainComponents count] == 2) {
return keychainComponents[index];
} else {
return nil;
}
}
@end

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
NS_ASSUME_NONNULL_BEGIN
// keys in Checkin preferences
FOUNDATION_EXPORT NSString *const kFIRMessagingDeviceAuthIdKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingSecretTokenKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingDigestStringKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingLastCheckinTimeKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingVersionInfoStringKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingGServicesDictionaryKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingDeviceDataVersionKey;
@class FIRMessagingCheckinPreferences;
/**
* Register the device with Checkin Service and get back the `authID`, `secret
* token` etc. for the client. Checkin results are cached in the
* `FIRMessagingCache` and periodically refreshed to prevent them from being stale.
* Each client needs to register with checkin before registering with InstanceID.
*/
@interface FIRMessagingCheckinService : NSObject
/**
* Execute a device checkin request to obtain an deviceID, secret token,
* gService data.
*
* @param existingCheckin An existing checkin preference object, if available.
* @param completion Completion hander called on success or failure of device checkin.
*/
- (void)checkinWithExistingCheckin:(nullable FIRMessagingCheckinPreferences *)existingCheckin
completion:
(void (^)(FIRMessagingCheckinPreferences *_Nullable checkinPreferences,
NSError *_Nullable error))completion;
/**
* This would stop any request that the service made to the checkin backend and also
* release any callback handlers that it holds.
*/
- (void)stopFetching;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,227 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinService.h"
#import <GoogleUtilities/GULAppEnvironmentUtil.h>
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingAuthService.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinPreferences.h"
static NSString *const kDeviceCheckinURL = @"https://device-provisioning.googleapis.com/checkin";
// keys in Checkin preferences
NSString *const kFIRMessagingDeviceAuthIdKey = @"GMSInstanceIDDeviceAuthIdKey";
NSString *const kFIRMessagingSecretTokenKey = @"GMSInstanceIDSecretTokenKey";
NSString *const kFIRMessagingDigestStringKey = @"GMSInstanceIDDigestKey";
NSString *const kFIRMessagingLastCheckinTimeKey = @"GMSInstanceIDLastCheckinTimestampKey";
NSString *const kFIRMessagingVersionInfoStringKey = @"GMSInstanceIDVersionInfo";
NSString *const kFIRMessagingGServicesDictionaryKey = @"GMSInstanceIDGServicesData";
NSString *const kFIRMessagingDeviceDataVersionKey = @"GMSInstanceIDDeviceDataVersion";
static NSUInteger const kCheckinType = 2; // DeviceType IOS in l/w/a/_checkin.proto
static NSUInteger const kCheckinVersion = 2;
static NSUInteger const kFragment = 0;
@interface FIRMessagingCheckinService ()
@property(nonatomic, readwrite, strong) NSURLSession *session;
@end
@implementation FIRMessagingCheckinService
- (instancetype)init {
self = [super init];
if (self) {
// Create an URLSession once, even though checkin should happen about once a day
NSURLSessionConfiguration *config = NSURLSessionConfiguration.defaultSessionConfiguration;
config.timeoutIntervalForResource = 60.0f; // 1 minute
config.allowsCellularAccess = YES;
self.session = [NSURLSession sessionWithConfiguration:config];
self.session.sessionDescription = @"com.google.iid-checkin";
}
return self;
}
- (void)dealloc {
[self.session invalidateAndCancel];
}
- (void)checkinWithExistingCheckin:(FIRMessagingCheckinPreferences *)existingCheckin
completion:(FIRMessagingDeviceCheckinCompletion)completion {
if (self.session == nil) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeService005,
@"Inconsistent state: NSURLSession has been invalidated");
NSError *error =
[NSError messagingErrorWithCode:kFIRMessagingErrorCodeRegistrarFailedToCheckIn
failureReason:@"Failed to checkin. NSURLSession is invalid."];
if (completion) {
completion(nil, error);
}
return;
}
NSURL *url = [NSURL URLWithString:kDeviceCheckinURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setValue:@"application/json" forHTTPHeaderField:@"content-type"];
NSDictionary *checkinParameters = [self checkinParametersWithExistingCheckin:existingCheckin];
NSData *checkinData = [NSJSONSerialization dataWithJSONObject:checkinParameters
options:0
error:nil];
request.HTTPMethod = @"POST";
request.HTTPBody = checkinData;
void (^handler)(NSData *, NSURLResponse *, NSError *) =
^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeService000,
@"Device checkin HTTP fetch error. Error Code: %ld",
(long)error.code);
if (completion) {
completion(nil, error);
}
return;
}
NSError *serializationError = nil;
NSDictionary *dataResponse = [NSJSONSerialization JSONObjectWithData:data
options:0
error:&serializationError];
if (serializationError) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeService001,
@"Error serializing json object. Error Code: %ld",
(long)serializationError.code);
if (completion) {
completion(nil, serializationError);
}
return;
}
NSString *deviceAuthID = [dataResponse[@"android_id"] stringValue];
NSString *secretToken = [dataResponse[@"security_token"] stringValue];
if ([deviceAuthID length] == 0) {
NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidRequest
failureReason:@"Invalid device auth ID."];
if (completion) {
completion(nil, error);
}
return;
}
int64_t lastCheckinTimestampMillis = [dataResponse[@"time_msec"] longLongValue];
int64_t currentTimestampMillis = FIRMessagingCurrentTimestampInMilliseconds();
// Somehow the server clock gets out of sync with the device clock.
// Reset the last checkin timestamp in case this happens.
if (lastCheckinTimestampMillis > currentTimestampMillis) {
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeService002, @"Invalid last checkin timestamp %@ in future.",
[NSDate dateWithTimeIntervalSince1970:lastCheckinTimestampMillis / 1000.0]);
lastCheckinTimestampMillis = currentTimestampMillis;
}
NSString *deviceDataVersionInfo = dataResponse[@"device_data_version_info"] ?: @"";
NSString *digest = dataResponse[@"digest"] ?: @"";
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeService003,
@"Checkin successful with authId: %@, "
@"digest: %@, "
@"lastCheckinTimestamp: %lld",
deviceAuthID, digest, lastCheckinTimestampMillis);
NSString *versionInfo = dataResponse[@"version_info"] ?: @"";
NSMutableDictionary *gservicesData = [NSMutableDictionary dictionary];
// Read gServices data.
NSArray *flatSettings = dataResponse[@"setting"];
for (NSDictionary *dict in flatSettings) {
if (dict[@"name"] && dict[@"value"]) {
gservicesData[dict[@"name"]] = dict[@"value"];
} else {
FIRMessagingLoggerDebug(kFIRMessagingInvalidSettingResponse,
@"Invalid setting in checkin response: (%@: %@)", dict[@"name"],
dict[@"value"]);
}
}
FIRMessagingCheckinPreferences *checkinPreferences =
[[FIRMessagingCheckinPreferences alloc] initWithDeviceID:deviceAuthID
secretToken:secretToken];
NSDictionary *preferences = @{
kFIRMessagingDigestStringKey : digest,
kFIRMessagingVersionInfoStringKey : versionInfo,
kFIRMessagingLastCheckinTimeKey : @(lastCheckinTimestampMillis),
kFIRMessagingGServicesDictionaryKey : gservicesData,
kFIRMessagingDeviceDataVersionKey : deviceDataVersionInfo,
};
[checkinPreferences updateWithCheckinPlistContents:preferences];
if (completion) {
completion(checkinPreferences, nil);
}
};
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:handler];
[task resume];
}
- (void)stopFetching {
[self.session invalidateAndCancel];
// The session cannot be reused after invalidation. Dispose it to prevent accident reusing.
self.session = nil;
}
#pragma mark - Private
- (NSDictionary *)checkinParametersWithExistingCheckin:
(nullable FIRMessagingCheckinPreferences *)checkinPreferences {
NSString *deviceModel = [GULAppEnvironmentUtil deviceModel];
NSString *systemVersion = [GULAppEnvironmentUtil systemVersion];
NSString *osVersion = [NSString stringWithFormat:@"IOS_%@", systemVersion];
// Get locale from GCM if GCM exists else use system API.
NSString *locale = FIRMessagingCurrentLocale();
NSInteger userNumber = 0; // Multi Profile may change this.
NSInteger userSerialNumber = 0; // Multi Profile may change this
NSString *timeZone = [NSTimeZone localTimeZone].name;
int64_t lastCheckingTimestampMillis = checkinPreferences.lastCheckinTimestampMillis;
NSDictionary *checkinParameters = @{
@"checkin" : @{
@"iosbuild" : @{@"model" : deviceModel, @"os_version" : osVersion},
@"type" : @(kCheckinType),
@"user_number" : @(userNumber),
@"last_checkin_msec" : @(lastCheckingTimestampMillis),
},
@"fragment" : @(kFragment),
@"locale" : locale,
@"version" : @(kCheckinVersion),
@"digest" : checkinPreferences.digest ?: @"",
@"time_zone" : timeZone,
@"user_serial_number" : @(userSerialNumber),
@"id" : @([checkinPreferences.deviceID longLongValue]),
@"security_token" : @([checkinPreferences.secretToken longLongValue]),
};
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeService006, @"Checkin parameters: %@",
checkinParameters);
return checkinParameters;
}
@end

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class FIRMessagingAuthKeychain;
@class FIRMessagingBackupExcludedPlist;
@class FIRMessagingCheckinPreferences;
// These values exposed for testing
extern NSString *const kFIRMessagingCheckinKeychainService;
/**
* Checkin preferences backing store.
*/
@interface FIRMessagingCheckinStore : NSObject
/**
* Checks whether the backup excluded checkin preferences are present on the disk or not.
*
* @return YES if the backup excluded checkin plist exists on the disks else NO.
*/
- (BOOL)hasCheckinPlist;
#pragma mark - Save
/**
* Save the checkin preferences to backing store.
*
* @param preferences Checkin preferences to save.
* @param handler The callback handler which is invoked when the operation is complete,
* with an error if there is any.
*/
- (void)saveCheckinPreferences:(FIRMessagingCheckinPreferences *)preferences
handler:(void (^)(NSError *error))handler;
#pragma mark - Delete
/**
* Remove the cached checkin preferences.
*
* @param handler The callback handler which is invoked when the operation is complete,
* with an error if there is any.
*/
- (void)removeCheckinPreferencesWithHandler:(void (^)(NSError *error))handler;
#pragma mark - Get
/**
* Get the cached device secret. If we cannot access it for some reason we
* return the appropriate error object.
*
* @return The cached checkin preferences if present else nil.
*/
- (FIRMessagingCheckinPreferences *)cachedCheckinPreferences;
@end

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinStore.h"
#import "FirebaseMessaging/Sources/FIRMessagingCode.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingAuthKeychain.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingBackupExcludedPlist.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinPreferences.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinService.h"
// NOTE: These values should be in sync with what InstanceID saves in as.
static NSString *const kCheckinFileName = @"g-checkin";
static NSString *const kFIRMessagingCheckinKeychainGeneric = @"com.google.iid";
NSString *const kFIRMessagingCheckinKeychainService = @"com.google.iid.checkin";
@interface FIRMessagingCheckinStore ()
@property(nonatomic, readwrite, strong) FIRMessagingBackupExcludedPlist *plist;
@property(nonatomic, readwrite, strong) FIRMessagingAuthKeychain *keychain;
// Checkin will store items under
// Keychain account: <app bundle id>,
// Keychain service: |kFIRMessagingCheckinKeychainService|
@property(nonatomic, readonly) NSString *bundleIdentifierForKeychainAccount;
@end
@implementation FIRMessagingCheckinStore
- (instancetype)init {
self = [super init];
if (self) {
_plist = [[FIRMessagingBackupExcludedPlist alloc]
initWithFileName:kCheckinFileName
subDirectory:kFIRMessagingInstanceIDSubDirectoryName];
_keychain =
[[FIRMessagingAuthKeychain alloc] initWithIdentifier:kFIRMessagingCheckinKeychainGeneric];
}
return self;
}
- (BOOL)hasCheckinPlist {
return [self.plist doesFileExist];
}
- (NSString *)bundleIdentifierForKeychainAccount {
static NSString *bundleIdentifier;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
bundleIdentifier = FIRMessagingAppIdentifier();
});
return bundleIdentifier;
}
- (void)saveCheckinPreferences:(FIRMessagingCheckinPreferences *)preferences
handler:(void (^)(NSError *error))handler {
NSDictionary *checkinPlistContents = [preferences checkinPlistContents];
NSString *checkinKeychainContent = [preferences checkinKeychainContent];
if (![checkinKeychainContent length]) {
NSString *failureReason = @"Failed to get checkin keychain content from memory.";
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeCheckinStore000, @"%@", failureReason);
if (handler) {
handler([NSError messagingErrorWithCode:kFIRMessagingErrorCodeRegistrarFailedToCheckIn
failureReason:failureReason]);
}
return;
}
if (![checkinPlistContents count]) {
NSString *failureReason = @"Failed to get checkin plist contents from memory.";
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeCheckinStore001, @"%@", failureReason);
if (handler) {
handler([NSError messagingErrorWithCode:kFIRMessagingErrorCodeRegistrarFailedToCheckIn
failureReason:failureReason]);
}
return;
}
// Save all other checkin preferences in a plist
NSError *error;
if (![self.plist writeDictionary:checkinPlistContents error:&error]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeCheckinStore003,
@"Failed to save checkin plist contents."
@"Will delete auth credentials");
[self.keychain removeItemsMatchingService:kFIRMessagingCheckinKeychainService
account:self.bundleIdentifierForKeychainAccount
handler:nil];
if (handler) {
handler(error);
}
return;
}
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeCheckinStoreCheckinPlistSaved,
@"Checkin plist file is saved");
// Save the deviceID and secret in the Keychain
if (!preferences.hasPreCachedAuthCredentials) {
NSData *data = [checkinKeychainContent dataUsingEncoding:NSUTF8StringEncoding];
[self.keychain setData:data
forService:kFIRMessagingCheckinKeychainService
account:self.bundleIdentifierForKeychainAccount
handler:^(NSError *error) {
if (error) {
if (handler) {
handler(error);
}
return;
}
if (handler) {
handler(nil);
}
}];
} else {
handler(nil);
}
}
- (void)removeCheckinPreferencesWithHandler:(void (^)(NSError *error))handler {
// Delete the checkin preferences plist first to avoid delay.
NSError *deletePlistError;
if (![self.plist deleteFile:&deletePlistError]) {
handler(deletePlistError);
return;
}
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeCheckinStoreCheckinPlistDeleted,
@"Deleted checkin plist file.");
// Remove deviceID and secret from Keychain
[self.keychain removeItemsMatchingService:kFIRMessagingCheckinKeychainService
account:self.bundleIdentifierForKeychainAccount
handler:^(NSError *error) {
handler(error);
}];
}
- (FIRMessagingCheckinPreferences *)cachedCheckinPreferences {
// Query the keychain for deviceID and secret
NSData *item = [self.keychain dataForService:kFIRMessagingCheckinKeychainService
account:self.bundleIdentifierForKeychainAccount];
// Check info found in keychain
NSString *checkinKeychainContent = [[NSString alloc] initWithData:item
encoding:NSUTF8StringEncoding];
FIRMessagingCheckinPreferences *checkinPreferences = [FIRMessagingCheckinPreferences
preferencesFromKeychainContents:[checkinKeychainContent copy]];
NSDictionary *checkinPlistContents = [self.plist contentAsDictionary];
NSString *plistDeviceAuthID = checkinPlistContents[kFIRMessagingDeviceAuthIdKey];
NSString *plistSecretToken = checkinPlistContents[kFIRMessagingSecretTokenKey];
// If deviceID and secret not found in the keychain verify that we don't have them in the
// checkin preferences plist.
if (![checkinPreferences.deviceID length] && ![checkinPreferences.secretToken length]) {
if ([plistDeviceAuthID length] && [plistSecretToken length]) {
// Couldn't find checkin credentials in keychain but found them in the plist.
checkinPreferences =
[[FIRMessagingCheckinPreferences alloc] initWithDeviceID:plistDeviceAuthID
secretToken:plistSecretToken];
} else {
// Couldn't find checkin credentials in keychain nor plist
return nil;
}
}
[checkinPreferences updateWithCheckinPlistContents:checkinPlistContents];
return checkinPreferences;
}
@end

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
/* The Keychain error domain */
extern NSString *const kFIRMessagingKeychainErrorDomain;
/*
* Wrapping the keychain operations in a serialize queue. This is to avoid keychain operation
* blocking main queue.
*/
@interface FIRMessagingKeychain : NSObject
/**
* FIRMessagingKeychain.
*
* @return A shared instance of FIRMessagingKeychain.
*/
+ (instancetype)sharedInstance;
/**
* Get keychain items matching the given a query.
*
* @param keychainQuery The keychain query.
*
* @return An CFTypeRef result matching the provided inputs.
*/
- (CFTypeRef)itemWithQuery:(NSDictionary *)keychainQuery;
/**
* Remove the cached items from the keychain matching the query.
*
* @param keychainQuery The keychain query.
* @param handler The callback handler which is invoked when the remove operation is
* complete, with an error if there is any.
*/
- (void)removeItemWithQuery:(NSDictionary *)keychainQuery handler:(void (^)(NSError *error))handler;
/**
* Add the item with a given query.
*
* @param keychainQuery The keychain query.
* @param handler The callback handler which is invoked when the add operation is
* complete, with an error if there is any.
*/
- (void)addItemWithQuery:(NSDictionary *)keychainQuery handler:(void (^)(NSError *))handler;
@end

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingKeychain.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
NSString *const kFIRMessagingKeychainErrorDomain = @"com.google.iid";
@interface FIRMessagingKeychain () {
dispatch_queue_t _keychainOperationQueue;
}
@end
@implementation FIRMessagingKeychain
+ (instancetype)sharedInstance {
static FIRMessagingKeychain *sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[FIRMessagingKeychain alloc] init];
});
return sharedInstance;
}
- (instancetype)init {
self = [super init];
if (self) {
_keychainOperationQueue =
dispatch_queue_create("com.google.FirebaseInstanceID.Keychain", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (CFTypeRef)itemWithQuery:(NSDictionary *)keychainQuery {
__block SecKeyRef keyRef = NULL;
dispatch_sync(_keychainOperationQueue, ^{
OSStatus status =
SecItemCopyMatching((__bridge CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyRef);
if (status != noErr) {
if (keyRef) {
CFRelease(keyRef);
}
FIRMessagingLoggerDebug(kFIRMessagingKeychainReadItemError,
@"Info is not found in Keychain. OSStatus: %d. Keychain query: %@",
(int)status, keychainQuery);
}
});
return keyRef;
}
- (void)removeItemWithQuery:(NSDictionary *)keychainQuery
handler:(void (^)(NSError *error))handler {
dispatch_async(_keychainOperationQueue, ^{
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)keychainQuery);
if (status != noErr) {
FIRMessagingLoggerDebug(
kFIRMessagingKeychainDeleteItemError,
@"Couldn't delete item from Keychain OSStatus: %d with the keychain query %@",
(int)status, keychainQuery);
}
if (handler) {
NSError *error = nil;
// When item is not found, it should NOT be considered as an error. The operation should
// continue.
if (status != noErr && status != errSecItemNotFound) {
error = [NSError errorWithDomain:kFIRMessagingKeychainErrorDomain code:status userInfo:nil];
}
dispatch_async(dispatch_get_main_queue(), ^{
handler(error);
});
}
});
}
- (void)addItemWithQuery:(NSDictionary *)keychainQuery handler:(void (^)(NSError *))handler {
dispatch_async(_keychainOperationQueue, ^{
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)keychainQuery, NULL);
if (handler) {
NSError *error = nil;
if (status != noErr) {
FIRMessagingLoggerWarn(kFIRMessagingKeychainAddItemError,
@"Couldn't add item to Keychain OSStatus: %d", (int)status);
error = [NSError errorWithDomain:kFIRMessagingKeychainErrorDomain code:status userInfo:nil];
}
dispatch_async(dispatch_get_main_queue(), ^{
handler(error);
});
}
});
}
@end

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface FIRMessagingTokenDeleteOperation : FIRMessagingTokenOperation
- (instancetype)initWithAuthorizedEntity:(nullable NSString *)authorizedEntity
scope:(nullable NSString *)scope
checkinPreferences:(FIRMessagingCheckinPreferences *)checkinPreferences
instanceID:(nullable NSString *)instanceID
action:(FIRMessagingTokenAction)action
heartbeatLogger:(id<FIRHeartbeatLoggerProtocol>)heartbeatLogger;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenDeleteOperation.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinPreferences.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenOperation.h"
@implementation FIRMessagingTokenDeleteOperation
- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
checkinPreferences:(FIRMessagingCheckinPreferences *)checkinPreferences
instanceID:(NSString *)instanceID
action:(FIRMessagingTokenAction)action
heartbeatLogger:(id<FIRHeartbeatLoggerProtocol>)heartbeatLogger {
return [super initWithAction:action
forAuthorizedEntity:authorizedEntity
scope:scope
options:nil
checkinPreferences:checkinPreferences
instanceID:instanceID
heartbeatLogger:heartbeatLogger];
}
- (void)performTokenOperation {
NSMutableURLRequest *request = [self tokenRequest];
// Build form-encoded body
NSString *deviceAuthID = self.checkinPreferences.deviceID;
NSMutableArray<NSURLQueryItem *> *queryItems =
[FIRMessagingTokenOperation standardQueryItemsWithDeviceID:deviceAuthID scope:self.scope];
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"delete" value:@"true"]];
if (self.action == FIRMessagingTokenActionDeleteTokenAndIID) {
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"iid-operation" value:@"delete"]];
}
if (self.authorizedEntity) {
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"sender" value:self.authorizedEntity]];
}
// Typically we include our public key-signed url items, but in some cases (like deleting all FCM
// tokens), we don't.
if (self.instanceID.length > 0) {
[queryItems addObject:[NSURLQueryItem queryItemWithName:kFIRMessagingParamInstanceID
value:self.instanceID]];
}
NSURLComponents *components = [[NSURLComponents alloc] init];
components.queryItems = queryItems;
NSString *content = components.query;
request.HTTPBody = [content dataUsingEncoding:NSUTF8StringEncoding];
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenDeleteOperationFetchRequest,
@"Unregister request to %@ content: %@",
FIRMessagingTokenRegisterServer(), content);
FIRMessaging_WEAKIFY(self);
void (^requestHandler)(NSData *, NSURLResponse *, NSError *) =
^(NSData *data, NSURLResponse *response, NSError *error) {
FIRMessaging_STRONGIFY(self);
[self handleResponseWithData:data response:response error:error];
};
NSURLSessionConfiguration *config = NSURLSessionConfiguration.defaultSessionConfiguration;
config.timeoutIntervalForResource = 60.0f; // 1 minute
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
self.dataTask = [session dataTaskWithRequest:request completionHandler:requestHandler];
[self.dataTask resume];
}
- (void)handleResponseWithData:(NSData *)data
response:(NSURLResponse *)response
error:(NSError *)error {
if (error) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenDeleteOperationRequestError,
@"Device unregister HTTP fetch error. Error code: %ld",
(long)error.code);
[self finishWithResult:FIRMessagingTokenOperationError token:nil error:error];
return;
}
NSString *dataResponse = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (dataResponse.length == 0) {
NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeUnknown
failureReason:@"Empty response."];
[self finishWithResult:FIRMessagingTokenOperationError token:nil error:error];
return;
}
if (![dataResponse hasPrefix:@"deleted="] && ![dataResponse hasPrefix:@"token="]) {
NSString *failureReason =
[NSString stringWithFormat:@"Invalid unregister response %@", response];
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenDeleteOperationBadResponse, @"%@",
failureReason);
NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeUnknown
failureReason:failureReason];
[self finishWithResult:FIRMessagingTokenOperationError token:nil error:error];
return;
}
[self finishWithResult:FIRMessagingTokenOperationSucceeded token:nil error:nil];
}
@end

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenOperation.h"
NS_ASSUME_NONNULL_BEGIN
FOUNDATION_EXPORT NSString *const kFIRMessagingFirebaseUserAgentKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingFirebaseHeartbeatKey;
@interface FIRMessagingTokenFetchOperation : FIRMessagingTokenOperation
- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
options:(nullable NSDictionary<NSString *, NSString *> *)options
checkinPreferences:(FIRMessagingCheckinPreferences *)checkinPreferences
instanceID:(NSString *)instanceID
heartbeatLogger:(id<FIRHeartbeatLoggerProtocol>)heartbeatLogger;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,200 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenFetchOperation.h"
#import "FirebaseMessaging/Sources/FIRMessagingCode.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinPreferences.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenOperation.h"
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
// We can have a static int since this error should theoretically only
// happen once (for the first time). If it repeats there is something
// else that is wrong.
static int phoneRegistrationErrorRetryCount = 0;
static const int kMaxPhoneRegistrationErrorRetryCount = 10;
NSString *const kFIRMessagingFirebaseUserAgentKey = @"X-firebase-client";
NSString *const kFIRMessagingFirebaseHeartbeatKey = @"X-firebase-client-log-type";
@implementation FIRMessagingTokenFetchOperation
- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
options:(nullable NSDictionary<NSString *, NSString *> *)options
checkinPreferences:(FIRMessagingCheckinPreferences *)checkinPreferences
instanceID:(NSString *)instanceID
heartbeatLogger:(id<FIRHeartbeatLoggerProtocol>)heartbeatLogger {
return [super initWithAction:FIRMessagingTokenActionFetch
forAuthorizedEntity:authorizedEntity
scope:scope
options:options
checkinPreferences:checkinPreferences
instanceID:instanceID
heartbeatLogger:heartbeatLogger];
}
- (void)performTokenOperation {
NSMutableURLRequest *request = [self tokenRequest];
NSString *checkinVersionInfo = self.checkinPreferences.versionInfo;
[request setValue:checkinVersionInfo forHTTPHeaderField:@"info"];
[request setValue:[FIRApp firebaseUserAgent]
forHTTPHeaderField:kFIRMessagingFirebaseUserAgentKey];
[request setValue:@([self.heartbeatLogger heartbeatCodeForToday]).stringValue
forHTTPHeaderField:kFIRMessagingFirebaseHeartbeatKey];
// Build form-encoded body
NSString *deviceAuthID = self.checkinPreferences.deviceID;
NSMutableArray<NSURLQueryItem *> *queryItems =
[[self class] standardQueryItemsWithDeviceID:deviceAuthID scope:self.scope];
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"sender" value:self.authorizedEntity]];
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"X-subtype"
value:self.authorizedEntity]];
if (self.instanceID.length > 0) {
[queryItems addObject:[NSURLQueryItem queryItemWithName:kFIRMessagingParamInstanceID
value:self.instanceID]];
}
// Create query items from passed-in options
id apnsTokenData = self.options[kFIRMessagingTokenOptionsAPNSKey];
id apnsSandboxValue = self.options[kFIRMessagingTokenOptionsAPNSIsSandboxKey];
if ([apnsTokenData isKindOfClass:[NSData class]] &&
[apnsSandboxValue isKindOfClass:[NSNumber class]]) {
NSString *APNSString = FIRMessagingAPNSTupleStringForTokenAndServerType(
apnsTokenData, ((NSNumber *)apnsSandboxValue).boolValue);
// The name of the query item happens to be the same as the dictionary key
NSURLQueryItem *item = [NSURLQueryItem queryItemWithName:kFIRMessagingTokenOptionsAPNSKey
value:APNSString];
[queryItems addObject:item];
}
id firebaseAppID = self.options[kFIRMessagingTokenOptionsFirebaseAppIDKey];
if ([firebaseAppID isKindOfClass:[NSString class]]) {
// The name of the query item happens to be the same as the dictionary key
NSURLQueryItem *item =
[NSURLQueryItem queryItemWithName:kFIRMessagingTokenOptionsFirebaseAppIDKey
value:(NSString *)firebaseAppID];
[queryItems addObject:item];
}
NSURLComponents *components = [[NSURLComponents alloc] init];
components.queryItems = queryItems;
NSString *content = components.query;
request.HTTPBody = [content dataUsingEncoding:NSUTF8StringEncoding];
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenFetchOperationFetchRequest,
@"Register request to %@ content: %@", FIRMessagingTokenRegisterServer(),
content);
FIRMessaging_WEAKIFY(self);
void (^requestHandler)(NSData *, NSURLResponse *, NSError *) =
^(NSData *data, NSURLResponse *response, NSError *error) {
FIRMessaging_STRONGIFY(self);
[self handleResponseWithData:data response:response error:error];
};
NSURLSessionConfiguration *config = NSURLSessionConfiguration.defaultSessionConfiguration;
config.timeoutIntervalForResource = 60.0f; // 1 minute
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
self.dataTask = [session dataTaskWithRequest:request completionHandler:requestHandler];
[self.dataTask resume];
}
#pragma mark - Request Handling
- (void)handleResponseWithData:(NSData *)data
response:(NSURLResponse *)response
error:(NSError *)error {
if (error) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenFetchOperationRequestError,
@"Token fetch HTTP error. Error Code: %ld", (long)error.code);
[self finishWithResult:FIRMessagingTokenOperationError token:nil error:error];
return;
}
NSString *dataResponse = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (dataResponse.length == 0) {
NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeUnknown
failureReason:@"Empty response."];
[self finishWithResult:FIRMessagingTokenOperationError token:nil error:error];
return;
}
NSDictionary *parsedResponse = [self parseFetchTokenResponse:dataResponse];
if ([parsedResponse[@"token"] length]) {
[self finishWithResult:FIRMessagingTokenOperationSucceeded
token:parsedResponse[@"token"]
error:nil];
return;
}
NSString *errorValue = parsedResponse[@"Error"];
NSError *responseError = nil;
if (errorValue.length) {
NSArray *errorComponents = [errorValue componentsSeparatedByString:@":"];
// HACK (Kansas replication delay), PHONE_REGISTRATION_ERROR on App
// uninstall and reinstall.
if ([errorComponents containsObject:@"PHONE_REGISTRATION_ERROR"]) {
// Encountered issue http://b/27043795
// Retry register until successful or another error encountered or a
// certain number of tries are over.
if (phoneRegistrationErrorRetryCount < kMaxPhoneRegistrationErrorRetryCount) {
const int nextRetryInterval = 1 << phoneRegistrationErrorRetryCount;
FIRMessaging_WEAKIFY(self);
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(nextRetryInterval * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
FIRMessaging_STRONGIFY(self);
phoneRegistrationErrorRetryCount++;
[self performTokenOperation];
});
return;
}
} else if ([errorComponents containsObject:kFIRMessaging_CMD_RST]) {
NSString *failureReason = @"Identity is invalid. Server request identity reset.";
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeInternal001, @"%@", failureReason);
responseError = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeInvalidIdentity
failureReason:failureReason];
}
}
if (!responseError) {
NSString *failureReason = @"Invalid fetch response, expected 'token' or 'Error' key";
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenFetchOperationBadResponse, @"%@",
failureReason);
responseError = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeUnknown
failureReason:failureReason];
}
[self finishWithResult:FIRMessagingTokenOperationError token:nil error:responseError];
}
// expect a response e.g. "token=<reg id>\nGOOG.ttl=123"
- (NSDictionary *)parseFetchTokenResponse:(NSString *)response {
NSArray *lines = [response componentsSeparatedByString:@"\n"];
NSMutableDictionary *parsedResponse = [NSMutableDictionary dictionary];
for (NSString *line in lines) {
NSArray *keyAndValue = [line componentsSeparatedByString:@"="];
if ([keyAndValue count] > 1) {
parsedResponse[keyAndValue[0]] = keyAndValue[1];
}
}
return parsedResponse;
}
@end

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "FirebaseMessaging/Sources/Token/FIRMessagingAPNSInfo.h"
NS_ASSUME_NONNULL_BEGIN
/**
* Represents an Instance ID token, and all of the relevant information
* associated with it. It can read from and write to an NSDictionary object, for
* simple serialization.
*/
@interface FIRMessagingTokenInfo : NSObject <NSSecureCoding>
/// The authorized entity (also known as Sender ID), associated with the token.
@property(nonatomic, readonly, copy) NSString *authorizedEntity;
/// The scope associated with the token. This is an arbitrary string, typically "*".
@property(nonatomic, readonly, copy) NSString *scope;
/// The token value itself, with which all other properties are associated.
@property(nonatomic, readonly, copy) NSString *token;
// These properties are nullable because they might not exist for tokens fetched from
// legacy storage formats.
/// The app version that this token represents.
@property(nonatomic, readonly, copy, nullable) NSString *appVersion;
/// The Firebase app ID (also known as GMP App ID), that this token is associated with.
@property(nonatomic, readonly, copy, nullable) NSString *firebaseAppID;
/// Tokens may not always be associated with an APNs token, and may be associated after
/// being created.
@property(nonatomic, strong, nullable) FIRMessagingAPNSInfo *APNSInfo;
/// The time that this token info was updated. The cache time is writeable, since in
/// some cases the token info may be refreshed from the server. In those situations,
/// the cacheTime would be updated.
@property(nonatomic, copy, nullable) NSDate *cacheTime;
/// Indicates the info was stored on the keychain by version 10.18.0 or earlier.
@property(nonatomic, readonly) BOOL needsMigration;
/**
* Initializes a FIRMessagingTokenInfo object with the required parameters. These
* parameters represent all the relevant associated data with a token.
*
* @param authorizedEntity The authorized entity (also known as Sender ID).
* @param scope The scope of the token, typically "*" meaning
* it's a "default scope".
* @param token The token value itself.
* @param appVersion The application version that this token is associated with.
* @param firebaseAppID The Firebase app ID which this token is associated with.
* @return An instance of FIRMessagingTokenInfo.
*/
- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
token:(NSString *)token
appVersion:(nullable NSString *)appVersion
firebaseAppID:(nullable NSString *)firebaseAppID;
/**
* Check whether the token is still fresh based on:
* 1. Last fetch token is within the 7 days.
* 2. Language setting is not changed.
* 3. App version is current.
* 4. GMP App ID is current.
* 5. token is consistent with the current IID.
* 6. APNS info has changed.
* @param IID The app identifiier that is used to check if token is prefixed with.
* @return If token is fresh.
*
*/
- (BOOL)isFreshWithIID:(NSString *)IID;
/*
* Check whether the token is default token.
*/
- (BOOL)isDefaultToken;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,228 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenInfo.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
/**
* @enum Token Info Dictionary Key Constants
* @discussion The keys that are checked when a token info is
* created from a dictionary. The same keys are used
* when decoding/encoding an archive.
*/
/// Specifies a dictonary key whose value represents the authorized entity, or
/// Sender ID for the token.
static NSString *const kFIRInstanceIDAuthorizedEntityKey = @"authorized_entity";
/// Specifies a dictionary key whose value represents the scope of the token,
/// typically "*".
static NSString *const kFIRInstanceIDScopeKey = @"scope";
/// Specifies a dictionary key which represents the token value itself.
static NSString *const kFIRInstanceIDTokenKey = @"token";
/// Specifies a dictionary key which represents the app version associated
/// with the token.
static NSString *const kFIRInstanceIDAppVersionKey = @"app_version";
/// Specifies a dictionary key which represents the GMP App ID associated with
/// the token.
static NSString *const kFIRInstanceIDFirebaseAppIDKey = @"firebase_app_id";
/// Specifies a dictionary key representing an archive for a
/// `FIRInstanceIDAPNSInfo` object.
static NSString *const kFIRInstanceIDAPNSInfoKey = @"apns_info";
/// Specifies a dictionary key representing the "last cached" time for the token.
static NSString *const kFIRInstanceIDCacheTimeKey = @"cache_time";
/// Default interval that token stays fresh.
static const NSTimeInterval kDefaultFetchTokenInterval = 7 * 24 * 60 * 60; // 7 days.
@implementation FIRMessagingTokenInfo
- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
token:(NSString *)token
appVersion:(NSString *)appVersion
firebaseAppID:(NSString *)firebaseAppID {
self = [super init];
if (self) {
_authorizedEntity = [authorizedEntity copy];
_scope = [scope copy];
_token = [token copy];
_appVersion = [appVersion copy];
_firebaseAppID = [firebaseAppID copy];
}
return self;
}
- (BOOL)isFreshWithIID:(NSString *)IID {
// Last fetch token cache time could be null if token is from legacy storage format. Then token is
// considered not fresh and should be refreshed and overwrite with the latest storage format.
if (!IID) {
return NO;
}
if (!_cacheTime) {
return NO;
}
// Check if it's consistent with IID
if (![self.token hasPrefix:IID]) {
return NO;
}
if ([self hasDenylistedScope]) {
return NO;
}
// Check if app has just been updated to a new version.
NSString *currentAppVersion = FIRMessagingCurrentAppVersion();
if (!_appVersion || ![_appVersion isEqualToString:currentAppVersion]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenManager004,
@"Invalidating cached token for %@ (%@) due to app version change.",
_authorizedEntity, _scope);
return NO;
}
// Check if GMP App ID has changed
NSString *currentFirebaseAppID = FIRMessagingFirebaseAppID();
if (!_firebaseAppID || ![_firebaseAppID isEqualToString:currentFirebaseAppID]) {
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeTokenInfoFirebaseAppIDChanged,
@"Invalidating cached token due to Firebase App IID change from %@ to %@", _firebaseAppID,
currentFirebaseAppID);
return NO;
}
// Check whether locale has changed, if yes, token needs to be updated with server for locale
// information.
if (FIRMessagingHasLocaleChanged()) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenInfoLocaleChanged,
@"Invalidating cached token due to locale change");
return NO;
}
// Locale is not changed, check whether token has been fetched within 7 days.
NSTimeInterval lastFetchTokenTimestamp = [_cacheTime timeIntervalSince1970];
NSTimeInterval currentTimestamp = FIRMessagingCurrentTimestampInSeconds();
NSTimeInterval timeSinceLastFetchToken = currentTimestamp - lastFetchTokenTimestamp;
return (timeSinceLastFetchToken < kDefaultFetchTokenInterval);
}
- (BOOL)hasDenylistedScope {
/// The token with fiam scope is set by old FIAM SDK(s) which will remain in keychain for ever. So
/// we need to remove these tokens to deny its usage.
if ([self.scope isEqualToString:kFIRMessagingFIAMTokenScope]) {
return YES;
}
return NO;
}
- (BOOL)isDefaultToken {
return [self.scope isEqualToString:kFIRMessagingDefaultTokenScope];
}
#pragma mark - NSSecureCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
BOOL needsMigration = NO;
// These value cannot be nil
id authorizedEntity = [aDecoder decodeObjectForKey:kFIRInstanceIDAuthorizedEntityKey];
if (![authorizedEntity isKindOfClass:[NSString class]]) {
return nil;
}
id scope = [aDecoder decodeObjectForKey:kFIRInstanceIDScopeKey];
if (![scope isKindOfClass:[NSString class]]) {
return nil;
}
id token = [aDecoder decodeObjectForKey:kFIRInstanceIDTokenKey];
if (![token isKindOfClass:[NSString class]]) {
return nil;
}
// These values are nullable, so only fail the decode if the type does not match
id appVersion = [aDecoder decodeObjectForKey:kFIRInstanceIDAppVersionKey];
if (appVersion && ![appVersion isKindOfClass:[NSString class]]) {
return nil;
}
id firebaseAppID = [aDecoder decodeObjectForKey:kFIRInstanceIDFirebaseAppIDKey];
if (firebaseAppID && ![firebaseAppID isKindOfClass:[NSString class]]) {
return nil;
}
NSSet *classes = [[NSSet alloc] initWithArray:@[ FIRMessagingAPNSInfo.class ]];
FIRMessagingAPNSInfo *rawAPNSInfo = [aDecoder decodeObjectOfClasses:classes
forKey:kFIRInstanceIDAPNSInfoKey];
if (rawAPNSInfo && ![rawAPNSInfo isKindOfClass:[FIRMessagingAPNSInfo class]]) {
// If the decoder fails to decode a FIRMessagingAPNSInfo, check if this was archived by a
// FirebaseMessaging 10.18.0 or earlier.
// TODO(#12246) This block may be replaced with `rawAPNSInfo = nil` once we're confident all
// users have upgraded to at least 10.19.0. Perhaps, after privacy manifests have been required
// for awhile?
@try {
[NSKeyedUnarchiver setClass:[FIRMessagingAPNSInfo class]
forClassName:@"FIRInstanceIDAPNSInfo"];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
rawAPNSInfo = [NSKeyedUnarchiver unarchiveObjectWithData:(NSData *)rawAPNSInfo];
needsMigration = YES;
#pragma clang diagnostic pop
} @catch (NSException *exception) {
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeTokenInfoBadAPNSInfo,
@"Could not parse raw APNS Info while parsing archived token info.");
rawAPNSInfo = nil;
} @finally {
}
}
id cacheTime = [aDecoder decodeObjectForKey:kFIRInstanceIDCacheTimeKey];
if (cacheTime && ![cacheTime isKindOfClass:[NSDate class]]) {
return nil;
}
self = [super init];
if (self) {
_authorizedEntity = [authorizedEntity copy];
_scope = [scope copy];
_token = [token copy];
_appVersion = [appVersion copy];
_firebaseAppID = [firebaseAppID copy];
_APNSInfo = [rawAPNSInfo copy];
_cacheTime = cacheTime;
_needsMigration = needsMigration;
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.authorizedEntity forKey:kFIRInstanceIDAuthorizedEntityKey];
[aCoder encodeObject:self.scope forKey:kFIRInstanceIDScopeKey];
[aCoder encodeObject:self.token forKey:kFIRInstanceIDTokenKey];
[aCoder encodeObject:self.appVersion forKey:kFIRInstanceIDAppVersionKey];
[aCoder encodeObject:self.firebaseAppID forKey:kFIRInstanceIDFirebaseAppIDKey];
if (self.APNSInfo) {
[aCoder encodeObject:self.APNSInfo forKey:kFIRInstanceIDAPNSInfoKey];
}
[aCoder encodeObject:self.cacheTime forKey:kFIRInstanceIDCacheTimeKey];
}
@end

View File

@@ -0,0 +1,197 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h"
@class FIRMessagingAuthService;
@class FIRMessagingCheckinPreferences;
@class FIRMessagingTokenInfo;
@protocol FIRHeartbeatLoggerProtocol;
typedef NS_OPTIONS(NSUInteger, FIRMessagingInvalidTokenReason) {
FIRMessagingInvalidTokenReasonNone = 0, // 0
FIRMessagingInvalidTokenReasonAppVersion = (1 << 0), // 0...00001
FIRMessagingInvalidTokenReasonAPNSToken = (1 << 1), // 0...00010
};
/**
* Manager for the InstanceID token requests i.e `newToken` and `deleteToken`. This
* manages the overall interaction of the `FIRMessagingTokenStore`, the token register
* service and the callbacks associated with `GCMInstanceID`.
*/
@interface FIRMessagingTokenManager : NSObject
@property(nonatomic, readonly, copy) NSString *deviceAuthID;
@property(nonatomic, readonly, copy) NSString *secretToken;
@property(nonatomic, readonly, copy) NSString *versionInfo;
@property(nonatomic, readonly, copy) NSString *defaultFCMToken;
@property(nonatomic, readwrite, copy) NSString *fcmSenderID;
@property(nonatomic, readwrite, copy) NSString *firebaseAppID;
/// Expose the auth service, so it can be used by others
@property(nonatomic, readonly, strong) FIRMessagingAuthService *authService;
- (instancetype)init NS_UNAVAILABLE;
/**
* Designated initializer.
*
* @param heartbeatLogger The heartbeat logger that is injected into token operations.
*/
- (instancetype)initWithHeartbeatLogger:(id<FIRHeartbeatLoggerProtocol>)heartbeatLogger
NS_DESIGNATED_INITIALIZER;
/**
* Fetch new token for the given authorizedEntity and scope. This makes an
* asynchronous request to the InstanceID backend to create a new token for
* the service and returns it. This will replace any old token for the given
* authorizedEntity and scope that has been cached before.
*
* @param authorizedEntity The authorized entity for the token, should not be nil.
* @param scope The scope for the token, should not be nil.
* @param instanceID The unique string identifying the app instance.
* @param options The options to be added to the fetch request.
* @param handler The handler to be invoked once we have the token or the
* fetch request to InstanceID backend results in an error. Also
* since it's a public handler it should always be called
* asynchronously. This should be non-nil.
*/
- (void)fetchNewTokenWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
instanceID:(NSString *)instanceID
options:(NSDictionary *)options
handler:(FIRMessagingFCMTokenFetchCompletion)handler;
- (void)tokenWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
options:(NSDictionary *)options
handler:(FIRMessagingFCMTokenFetchCompletion)handler;
/**
* Return the cached token info, if one exists, for the given authorizedEntity and scope.
*
* @param authorizedEntity The authorized entity for the token.
* @param scope The scope for the token.
*
* @return The cached token info, if available, matching the parameters.
*/
- (FIRMessagingTokenInfo *)cachedTokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope;
/**
* Delete the token for the given authorizedEntity and scope. If the token has
* been cached, it will be deleted from the store. It will also make an
* asynchronous request to the InstanceID backend to invalidate the token.
*
* @param authorizedEntity The authorized entity for the token, should not be nil.
* @param scope The scope for the token, should not be nil.
* @param instanceID The unique string identifying the app instance.
* @param handler The handler to be invoked once the delete request to
* InstanceID backend has returned. If the request was
* successful we invoke the handler with a nil error;
* otherwise we call it with an appropriate error. Also since
* it's a public handler it should always be called
* asynchronously. This should be non-nil.
*/
- (void)deleteTokenWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
instanceID:(NSString *)instanceID
handler:(FIRMessagingDeleteFCMTokenCompletion)handler;
/**
* Deletes all cached tokens from the persistent store. This method should only be triggered
* when InstanceID is deleted
*
* @param handler The handler to be invoked once the delete request to InstanceID backend
* has returned. If the request was successful we invoke the handler with
* a nil error; else we pass in an appropriate error. This should be non-nil
* and be called asynchronously.
*/
- (void)deleteAllTokensWithHandler:(FIRMessagingDeleteFCMTokenCompletion)handler;
/**
* Deletes all cached tokens from the persistent store.
* @param handler The callback handler which is invoked when tokens deletion is complete,
* with an error if there is any.
*
*/
- (void)deleteWithHandler:(void (^)(NSError *))handler;
/**
* Stop any ongoing token operations.
*/
- (void)stopAllTokenOperations;
/**
* Invalidate any cached tokens, if the app version has changed since last launch or if the token
* is cached for more than 7 days.
* @param IID The cached instanceID, check if token is prefixed by such IID.
*
* @return Whether we should fetch default token from server.
*
* @discussion This should safely be called prior to any tokens being retrieved from
* the cache or being fetched from the network.
*/
- (BOOL)checkTokenRefreshPolicyWithIID:(NSString *)IID;
/**
* Upon being provided with different APNs or sandbox, any locally cached tokens
* should be deleted, and the new APNs token should be cached.
*
* @discussion It is possible for this method to be called while token operations are
* in-progress or queued. In this case, the in-flight token operations will have stale
* APNs information. The default token is checked for being out-of-date by Instance ID,
* and re-fetched. Custom tokens are not currently checked.
*
* @param deviceToken The APNS device token, provided by the operating system.
* @param isSandbox YES if the device token is for the sandbox environment, NO otherwise.
*
* @return The array of FIRMessagingTokenInfo objects which were invalidated.
*/
- (NSArray<FIRMessagingTokenInfo *> *)updateTokensToAPNSDeviceToken:(NSData *)deviceToken
isSandbox:(BOOL)isSandbox;
/*
* Sets APNS token
*/
- (void)setAPNSToken:(NSData *)APNSToken withUserInfo:(NSDictionary *)userInfo;
- (BOOL)hasValidCheckinInfo;
/*
* Gets the current default token, if not exist, request a new one from server.
*/
- (NSString *)tokenAndRequestIfNotExist;
/*
* Saves the default token to the keychain.
*/
- (void)saveDefaultTokenInfoInKeychain:(NSString *)defaultFcmToken;
/*
* Posts a token refresh notification when a default FCM token is generated.
*
*/
- (void)postTokenRefreshNotificationWithDefaultFCMToken:(NSString *)defaultFCMToken;
/*
* Checks if two tokens have changed.
*/
- (BOOL)hasTokenChangedFromOldToken:(NSString *)oldToken toNewToken:(NSString *)newToken;
@end

View File

@@ -0,0 +1,768 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenManager.h"
#import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingAuthKeychain.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingAuthService.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinPreferences.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinStore.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenDeleteOperation.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenFetchOperation.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenInfo.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenOperation.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenStore.h"
@interface FIRMessagingTokenManager () {
FIRMessagingTokenStore *_tokenStore;
NSString *_defaultFCMToken;
}
@property(nonatomic, readwrite, strong) FIRMessagingCheckinStore *checkinStore;
@property(nonatomic, readwrite, strong) FIRMessagingAuthService *authService;
@property(nonatomic, readonly, strong) NSOperationQueue *tokenOperations;
@property(nonatomic, readwrite, strong) FIRMessagingAPNSInfo *currentAPNSInfo;
@property(nonatomic, readwrite) FIRInstallations *installations;
@property(readonly) id<FIRHeartbeatLoggerProtocol> heartbeatLogger;
@end
@implementation FIRMessagingTokenManager
- (instancetype)initWithHeartbeatLogger:(id<FIRHeartbeatLoggerProtocol>)heartbeatLogger {
self = [super init];
if (self) {
_tokenStore = [[FIRMessagingTokenStore alloc] init];
_authService = [[FIRMessagingAuthService alloc] init];
[self resetCredentialsIfNeeded];
[self configureTokenOperations];
_installations = [FIRInstallations installations];
_heartbeatLogger = heartbeatLogger;
}
return self;
}
- (void)dealloc {
[self stopAllTokenOperations];
}
- (NSString *)tokenAndRequestIfNotExist {
if (!self.fcmSenderID.length) {
return nil;
}
if (_defaultFCMToken.length) {
return _defaultFCMToken;
}
FIRMessagingTokenInfo *cachedTokenInfo =
[self cachedTokenInfoWithAuthorizedEntity:self.fcmSenderID
scope:kFIRMessagingDefaultTokenScope];
NSString *cachedToken = cachedTokenInfo.token;
if (cachedToken) {
return cachedToken;
} else {
[self tokenWithAuthorizedEntity:self.fcmSenderID
scope:kFIRMessagingDefaultTokenScope
options:[self tokenOptions]
handler:^(NSString *_Nullable FCMToken, NSError *_Nullable error){
}];
return nil;
}
}
- (NSString *)defaultFCMToken {
return _defaultFCMToken;
}
- (void)postTokenRefreshNotificationWithDefaultFCMToken:(NSString *)defaultFCMToken {
// Should always trigger the token refresh notification when the delegate method is called
// No need to check if the token has changed, it's handled in the notification receiver.
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center postNotificationName:kFIRMessagingRegistrationTokenRefreshNotification
object:defaultFCMToken];
}
- (void)saveDefaultTokenInfoInKeychain:(NSString *)defaultFcmToken {
if ([self hasTokenChangedFromOldToken:_defaultFCMToken toNewToken:defaultFcmToken]) {
_defaultFCMToken = [defaultFcmToken copy];
FIRMessagingTokenInfo *tokenInfo =
[[FIRMessagingTokenInfo alloc] initWithAuthorizedEntity:_fcmSenderID
scope:kFIRMessagingDefaultTokenScope
token:defaultFcmToken
appVersion:FIRMessagingCurrentAppVersion()
firebaseAppID:_firebaseAppID];
tokenInfo.APNSInfo =
[[FIRMessagingAPNSInfo alloc] initWithTokenOptionsDictionary:[self tokenOptions]];
[self->_tokenStore saveTokenInfoInCache:tokenInfo];
}
}
- (BOOL)hasTokenChangedFromOldToken:(NSString *)oldToken toNewToken:(NSString *)newToken {
return oldToken.length != newToken.length ||
(oldToken.length && newToken.length && ![oldToken isEqualToString:newToken]);
}
- (NSDictionary *)tokenOptions {
NSDictionary *instanceIDOptions = @{};
NSData *apnsTokenData = self.currentAPNSInfo.deviceToken;
if (apnsTokenData) {
instanceIDOptions = @{
kFIRMessagingTokenOptionsAPNSKey : apnsTokenData,
kFIRMessagingTokenOptionsAPNSIsSandboxKey : @(self.currentAPNSInfo.isSandbox),
};
}
return instanceIDOptions;
}
- (NSString *)deviceAuthID {
return [_authService checkinPreferences].deviceID;
}
- (NSString *)secretToken {
return [_authService checkinPreferences].secretToken;
}
- (NSString *)versionInfo {
return [_authService checkinPreferences].versionInfo;
}
- (void)configureTokenOperations {
_tokenOperations = [[NSOperationQueue alloc] init];
_tokenOperations.name = @"com.google.iid-token-operations";
// For now, restrict the operations to be serial, because in some cases (like if the
// authorized entity and scope are the same), order matters.
// If we have to deal with several different token requests simultaneously, it would be a good
// idea to add some better intelligence around this (performing unrelated token operations
// simultaneously, etc.).
_tokenOperations.maxConcurrentOperationCount = 1;
if ([_tokenOperations respondsToSelector:@selector(qualityOfService)]) {
_tokenOperations.qualityOfService = NSOperationQualityOfServiceUtility;
}
}
- (void)tokenWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
options:(NSDictionary *)options
handler:(FIRMessagingFCMTokenFetchCompletion)handler {
if (!handler) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeInstanceID000, @"Invalid nil handler");
return;
}
// Add internal options
NSMutableDictionary *tokenOptions = [NSMutableDictionary dictionary];
if (options.count) {
[tokenOptions addEntriesFromDictionary:options];
}
// ensure we have an APNS Token
if (tokenOptions[kFIRMessagingTokenOptionsAPNSKey] == nil) {
// we don't have an APNS token. Don't fetch or return a FCM Token
FIRMessagingLoggerWarn(kFIRMessagingMessageCodeAPNSTokenNotAvailableDuringTokenFetch,
@"Declining request for FCM Token since no APNS Token specified");
dispatch_async(dispatch_get_main_queue(), ^{
NSError *missingAPNSTokenError =
[NSError messagingErrorWithCode:kFIRMessagingErrorCodeMissingDeviceToken
failureReason:@"No APNS token specified before fetching FCM Token"];
handler(nil, missingAPNSTokenError);
});
return;
}
#if TARGET_OS_SIMULATOR && TARGET_OS_IOS
if (tokenOptions[kFIRMessagingTokenOptionsAPNSKey] != nil) {
// If APNS token is available on iOS Simulator, we must use the sandbox profile
// https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes
tokenOptions[kFIRMessagingTokenOptionsAPNSIsSandboxKey] = @(YES);
}
#endif
if (tokenOptions[kFIRMessagingTokenOptionsAPNSKey] != nil &&
tokenOptions[kFIRMessagingTokenOptionsAPNSIsSandboxKey] == nil) {
// APNS key was given, but server type is missing. Supply the server type with automatic
// checking. This can happen when the token is requested from FCM, which does not include a
// server type during its request.
tokenOptions[kFIRMessagingTokenOptionsAPNSIsSandboxKey] = @(FIRMessagingIsSandboxApp());
}
if (self.firebaseAppID) {
tokenOptions[kFIRMessagingTokenOptionsFirebaseAppIDKey] = self.firebaseAppID;
}
// comparing enums to ints directly throws a warning
FIRMessagingErrorCode noError = INT_MAX;
FIRMessagingErrorCode errorCode = noError;
if (![authorizedEntity length]) {
errorCode = kFIRMessagingErrorCodeMissingAuthorizedEntity;
} else if (![scope length]) {
errorCode = kFIRMessagingErrorCodeMissingScope;
} else if (!self.installations) {
errorCode = kFIRMessagingErrorCodeMissingFid;
}
FIRMessagingFCMTokenFetchCompletion newHandler = ^(NSString *token, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(token, error);
});
};
if (errorCode != noError) {
newHandler(
nil,
[NSError messagingErrorWithCode:errorCode
failureReason:@"Failed to send token request, missing critical info."]);
return;
}
FIRMessaging_WEAKIFY(self);
[_authService fetchCheckinInfoWithHandler:^(FIRMessagingCheckinPreferences *preferences,
NSError *error) {
FIRMessaging_STRONGIFY(self);
if (error) {
newHandler(nil, error);
return;
}
if (!self) {
NSError *derefErr =
[NSError messagingErrorWithCode:kFIRMessagingErrorCodeInternal
failureReason:@"Unable to fetch token. Lost Reference to TokenManager"];
handler(nil, derefErr);
return;
}
FIRMessaging_WEAKIFY(self);
[self->_installations
installationIDWithCompletion:^(NSString *_Nullable identifier, NSError *_Nullable error) {
FIRMessaging_STRONGIFY(self);
if (error) {
newHandler(nil, error);
} else {
FIRMessagingTokenInfo *cachedTokenInfo =
[self cachedTokenInfoWithAuthorizedEntity:authorizedEntity scope:scope];
FIRMessagingAPNSInfo *optionsAPNSInfo =
[[FIRMessagingAPNSInfo alloc] initWithTokenOptionsDictionary:tokenOptions];
// Check if APNS Info is changed
if ((!cachedTokenInfo.APNSInfo && !optionsAPNSInfo) ||
[cachedTokenInfo.APNSInfo isEqualToAPNSInfo:optionsAPNSInfo]) {
// check if token is fresh
if ([cachedTokenInfo isFreshWithIID:identifier]) {
newHandler(cachedTokenInfo.token, nil);
return;
}
}
[self fetchNewTokenWithAuthorizedEntity:[authorizedEntity copy]
scope:[scope copy]
instanceID:identifier
options:tokenOptions
handler:newHandler];
}
}];
}];
}
- (void)fetchNewTokenWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
instanceID:(NSString *)instanceID
options:(NSDictionary *)options
handler:(FIRMessagingFCMTokenFetchCompletion)handler {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenManager000,
@"Fetch new token for authorizedEntity: %@, scope: %@", authorizedEntity,
scope);
FIRMessagingTokenFetchOperation *operation =
[self createFetchOperationWithAuthorizedEntity:authorizedEntity
scope:scope
options:options
instanceID:instanceID];
FIRMessaging_WEAKIFY(self);
FIRMessagingTokenOperationCompletion completion = ^(FIRMessagingTokenOperationResult result,
NSString *_Nullable token,
NSError *_Nullable error) {
FIRMessaging_STRONGIFY(self);
if (error) {
handler(nil, error);
return;
}
if (!self) {
NSError *lostRefError = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeInternal
failureReason:@"Lost Reference to TokenManager"];
handler(nil, lostRefError);
return;
}
if ([self isDefaultTokenWithAuthorizedEntity:authorizedEntity scope:scope]) {
[self postTokenRefreshNotificationWithDefaultFCMToken:token];
}
NSString *firebaseAppID = options[kFIRMessagingTokenOptionsFirebaseAppIDKey];
FIRMessagingTokenInfo *tokenInfo =
[[FIRMessagingTokenInfo alloc] initWithAuthorizedEntity:authorizedEntity
scope:scope
token:token
appVersion:FIRMessagingCurrentAppVersion()
firebaseAppID:firebaseAppID];
tokenInfo.APNSInfo = [[FIRMessagingAPNSInfo alloc] initWithTokenOptionsDictionary:options];
[self->_tokenStore
saveTokenInfo:tokenInfo
handler:^(NSError *error) {
if (!error) {
// Do not send the token back in case the save was unsuccessful. Since with
// the new asychronous fetch mechanism this can lead to infinite loops, for
// example, we will return a valid token even though we weren't able to store
// it in our cache. The first token will lead to a onTokenRefresh callback
// wherein the user again calls `getToken` but since we weren't able to save
// it we won't hit the cache but hit the server again leading to an infinite
// loop.
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeTokenManager001,
@"Token fetch successful, token: %@, authorizedEntity: %@, scope:%@", token,
authorizedEntity, scope);
if (handler) {
handler(token, nil);
}
} else {
if (handler) {
handler(nil, error);
}
}
}];
};
// Add completion handler, and ensure it's called on the main queue
[operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
NSString *_Nullable token, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(result, token, error);
});
}];
[self.tokenOperations addOperation:operation];
}
- (FIRMessagingTokenInfo *)cachedTokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope {
FIRMessagingTokenInfo *tokenInfo = [_tokenStore tokenInfoWithAuthorizedEntity:authorizedEntity
scope:scope];
return tokenInfo;
}
- (BOOL)isDefaultTokenWithAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope {
if (_fcmSenderID.length != authorizedEntity.length) {
return NO;
}
if (![_fcmSenderID isEqualToString:authorizedEntity]) {
return NO;
}
return [scope isEqualToString:kFIRMessagingDefaultTokenScope];
}
- (void)deleteTokenWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
instanceID:(NSString *)instanceID
handler:(FIRMessagingDeleteFCMTokenCompletion)handler {
if ([_tokenStore tokenInfoWithAuthorizedEntity:authorizedEntity scope:scope]) {
[_tokenStore removeTokenWithAuthorizedEntity:authorizedEntity scope:scope];
}
// Does not matter if we cannot find it in the cache. Still make an effort to unregister
// from the server.
FIRMessagingCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
FIRMessagingTokenDeleteOperation *operation =
[self createDeleteOperationWithAuthorizedEntity:authorizedEntity
scope:scope
checkinPreferences:checkinPreferences
instanceID:instanceID
action:FIRMessagingTokenActionDeleteToken];
if (handler) {
[operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
NSString *_Nullable token, NSError *_Nullable error) {
if ([self isDefaultTokenWithAuthorizedEntity:authorizedEntity scope:scope]) {
[self postTokenRefreshNotificationWithDefaultFCMToken:nil];
}
dispatch_async(dispatch_get_main_queue(), ^{
handler(error);
});
}];
}
[self.tokenOperations addOperation:operation];
}
- (void)deleteAllTokensWithHandler:(void (^)(NSError *))handler {
FIRMessaging_WEAKIFY(self);
[self.installations
installationIDWithCompletion:^(NSString *_Nullable identifier, NSError *_Nullable error) {
FIRMessaging_STRONGIFY(self);
if (error) {
if (handler) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(error);
});
}
return;
}
// delete all tokens
FIRMessagingCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
if (!checkinPreferences) {
// The checkin is already deleted. No need to trigger the token delete operation as client
// no longer has the checkin information for server to delete.
dispatch_async(dispatch_get_main_queue(), ^{
handler(nil);
});
return;
}
FIRMessagingTokenDeleteOperation *operation = [self
createDeleteOperationWithAuthorizedEntity:kFIRMessagingKeychainWildcardIdentifier
scope:kFIRMessagingKeychainWildcardIdentifier
checkinPreferences:checkinPreferences
instanceID:identifier
action:FIRMessagingTokenActionDeleteTokenAndIID];
if (handler) {
[operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
NSString *_Nullable token, NSError *_Nullable error) {
self->_defaultFCMToken = nil;
dispatch_async(dispatch_get_main_queue(), ^{
handler(error);
});
}];
}
[self.tokenOperations addOperation:operation];
}];
}
- (void)deleteAllTokensLocallyWithHandler:(void (^)(NSError *error))handler {
[_tokenStore removeAllTokensWithHandler:handler];
}
- (void)stopAllTokenOperations {
[self.authService stopCheckinRequest];
[self.tokenOperations cancelAllOperations];
}
- (void)deleteWithHandler:(void (^)(NSError *))handler {
FIRMessaging_WEAKIFY(self);
[self deleteAllTokensWithHandler:^(NSError *_Nullable error) {
FIRMessaging_STRONGIFY(self);
if (error) {
handler(error);
return;
}
if (!self) {
NSError *lostRefError =
[NSError messagingErrorWithCode:kFIRMessagingErrorCodeInternal
failureReason:@"Cannot delete token. Lost reference to TokenManager"];
handler(lostRefError);
return;
}
[self deleteAllTokensLocallyWithHandler:^(NSError *localError) {
[self postTokenRefreshNotificationWithDefaultFCMToken:nil];
self->_defaultFCMToken = nil;
if (localError) {
handler(localError);
return;
}
[self.authService resetCheckinWithHandler:^(NSError *_Nonnull authError) {
handler(authError);
}];
}];
}];
}
#pragma mark - CheckinStore
/**
* Reset the keychain preferences if the app had been deleted earlier and then reinstalled.
* Keychain preferences are not cleared in the above scenario so explicitly clear them.
*
* In case of an iCloud backup and restore the Keychain preferences should already be empty
* since the Keychain items are marked with `*BackupThisDeviceOnly`.
*/
- (void)resetCredentialsIfNeeded {
BOOL checkinPlistExists = [_authService hasCheckinPlist];
// Checkin info existed in backup excluded plist. Should not be a fresh install.
if (checkinPlistExists) {
return;
}
// Keychain can still exist even if app is uninstalled.
FIRMessagingCheckinPreferences *oldCheckinPreferences = _authService.checkinPreferences;
if (!oldCheckinPreferences) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeStore009,
@"App reset detected but no valid checkin auth preferences found."
@" Will not delete server token registrations.");
return;
}
[_authService resetCheckinWithHandler:^(NSError *_Nonnull error) {
if (!error) {
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeStore002,
@"Removed cached checkin preferences from Keychain because this is a fresh install.");
} else {
FIRMessagingLoggerError(
kFIRMessagingMessageCodeStore003,
@"Couldn't remove cached checkin preferences for a fresh install. Error: %@", error);
}
if (oldCheckinPreferences.deviceID.length && oldCheckinPreferences.secretToken.length) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeStore006,
@"Resetting old checkin and deleting server token registrations.");
// We don't really need to delete old FCM tokens created via IID auth tokens since
// those tokens are already hashed by APNS token as the has so creating a new
// token should automatically delete the old-token.
[self didDeleteFCMScopedTokensForCheckin:oldCheckinPreferences];
}
}];
}
- (void)didDeleteFCMScopedTokensForCheckin:(FIRMessagingCheckinPreferences *)checkin {
// Make a best effort try to delete the old client related state on the FCM server. This is
// required to delete old pubusb registrations which weren't cleared when the app was deleted.
//
// This is only a one time effort. If this call fails the client would still receive duplicate
// pubsub notifications if he is again subscribed to the same topic.
//
// The client state should be cleared on the server for the provided checkin preferences.
FIRMessagingTokenDeleteOperation *operation =
[self createDeleteOperationWithAuthorizedEntity:nil
scope:nil
checkinPreferences:checkin
instanceID:nil
action:FIRMessagingTokenActionDeleteToken];
[operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
NSString *_Nullable token, NSError *_Nullable error) {
if (error) {
FIRMessagingMessageCode code =
kFIRMessagingMessageCodeTokenManagerErrorDeletingFCMTokensOnAppReset;
FIRMessagingLoggerDebug(code, @"Failed to delete GCM server registrations on app reset.");
} else {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenManagerDeletedFCMTokensOnAppReset,
@"Successfully deleted GCM server registrations on app reset");
}
}];
[self.tokenOperations addOperation:operation];
}
#pragma mark - Unit Testing Stub Helpers
// We really have this method so that we can more easily stub it out for unit testing
- (FIRMessagingTokenFetchOperation *)
createFetchOperationWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
options:(NSDictionary<NSString *, NSString *> *)options
instanceID:(NSString *)instanceID {
FIRMessagingCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
FIRMessagingTokenFetchOperation *operation =
[[FIRMessagingTokenFetchOperation alloc] initWithAuthorizedEntity:authorizedEntity
scope:scope
options:options
checkinPreferences:checkinPreferences
instanceID:instanceID
heartbeatLogger:self.heartbeatLogger];
return operation;
}
// We really have this method so that we can more easily stub it out for unit testing
- (FIRMessagingTokenDeleteOperation *)
createDeleteOperationWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
checkinPreferences:(FIRMessagingCheckinPreferences *)checkinPreferences
instanceID:(NSString *)instanceID
action:(FIRMessagingTokenAction)action {
FIRMessagingTokenDeleteOperation *operation =
[[FIRMessagingTokenDeleteOperation alloc] initWithAuthorizedEntity:authorizedEntity
scope:scope
checkinPreferences:checkinPreferences
instanceID:instanceID
action:action
heartbeatLogger:self.heartbeatLogger];
return operation;
}
#pragma mark - Invalidating Cached Tokens
- (BOOL)checkTokenRefreshPolicyWithIID:(NSString *)IID {
// We know at least one cached token exists.
BOOL shouldFetchDefaultToken = NO;
NSArray<FIRMessagingTokenInfo *> *tokenInfos = [_tokenStore cachedTokenInfos];
NSMutableArray<FIRMessagingTokenInfo *> *tokenInfosToDelete =
[NSMutableArray arrayWithCapacity:tokenInfos.count];
for (FIRMessagingTokenInfo *tokenInfo in tokenInfos) {
if ([tokenInfo isFreshWithIID:IID]) {
// Token is fresh and in right format, do nothing
continue;
}
if ([tokenInfo isDefaultToken]) {
// Default token is expired, do not mark for deletion. Fetch directly from server to
// replace the current one.
shouldFetchDefaultToken = YES;
} else {
// Non-default token is expired, mark for deletion.
[tokenInfosToDelete addObject:tokenInfo];
}
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeTokenManagerInvalidateStaleToken,
@"Invalidating cached token for %@ (%@) due to token is no longer fresh.",
tokenInfo.authorizedEntity, tokenInfo.scope);
}
for (FIRMessagingTokenInfo *tokenInfoToDelete in tokenInfosToDelete) {
[_tokenStore removeTokenWithAuthorizedEntity:tokenInfoToDelete.authorizedEntity
scope:tokenInfoToDelete.scope];
}
return shouldFetchDefaultToken;
}
- (NSArray<FIRMessagingTokenInfo *> *)updateTokensToAPNSDeviceToken:(NSData *)deviceToken
isSandbox:(BOOL)isSandbox {
// Each cached IID token that is missing an APNSInfo, or has an APNSInfo associated should be
// checked and invalidated if needed.
FIRMessagingAPNSInfo *APNSInfo = [[FIRMessagingAPNSInfo alloc] initWithDeviceToken:deviceToken
isSandbox:isSandbox];
if ([self.currentAPNSInfo isEqualToAPNSInfo:APNSInfo]) {
return @[];
}
self.currentAPNSInfo = APNSInfo;
NSArray<FIRMessagingTokenInfo *> *tokenInfos = [_tokenStore cachedTokenInfos];
NSMutableArray<FIRMessagingTokenInfo *> *tokenInfosToDelete =
[NSMutableArray arrayWithCapacity:tokenInfos.count];
for (FIRMessagingTokenInfo *cachedTokenInfo in tokenInfos) {
// Check if the cached APNSInfo is nil, or if it is an old APNSInfo.
if (!cachedTokenInfo.APNSInfo ||
![cachedTokenInfo.APNSInfo isEqualToAPNSInfo:self.currentAPNSInfo]) {
// Mark for invalidation.
[tokenInfosToDelete addObject:cachedTokenInfo];
}
}
for (FIRMessagingTokenInfo *tokenInfoToDelete in tokenInfosToDelete) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenManagerAPNSChangedTokenInvalidated,
@"Invalidating cached token for %@ (%@) due to APNs token change.",
tokenInfoToDelete.authorizedEntity, tokenInfoToDelete.scope);
[_tokenStore removeTokenWithAuthorizedEntity:tokenInfoToDelete.authorizedEntity
scope:tokenInfoToDelete.scope];
}
return tokenInfosToDelete;
}
#pragma mark - APNS Token
- (void)setAPNSToken:(NSData *)APNSToken withUserInfo:(NSDictionary *)userInfo {
if (!APNSToken || ![APNSToken isKindOfClass:[NSData class]]) {
if ([APNSToken class]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeInternal002, @"Invalid APNS token type %@",
NSStringFromClass([APNSToken class]));
} else {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeInternal002, @"Empty APNS token type");
}
return;
}
// The APNS token is being added, or has changed (rare)
if ([self.currentAPNSInfo.deviceToken isEqualToData:APNSToken]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeInstanceID011,
@"Trying to reset APNS token to the same value. Will return");
return;
}
// Use this token type for when we have to automatically fetch tokens in the future
#if TARGET_OS_SIMULATOR && TARGET_OS_IOS
// If APNS token is available on iOS Simulator, we must use the sandbox profile
// https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes
BOOL isSandboxApp = YES;
#else
NSInteger type = [userInfo[kFIRMessagingAPNSTokenType] integerValue];
BOOL isSandboxApp = (type == FIRMessagingAPNSTokenTypeSandbox);
if (type == FIRMessagingAPNSTokenTypeUnknown) {
isSandboxApp = FIRMessagingIsSandboxApp();
}
#endif
// Pro-actively invalidate the default token, if the APNs change makes it
// invalid. Previously, we invalidated just before fetching the token.
NSArray<FIRMessagingTokenInfo *> *invalidatedTokens =
[self updateTokensToAPNSDeviceToken:APNSToken isSandbox:isSandboxApp];
self.currentAPNSInfo = [[FIRMessagingAPNSInfo alloc] initWithDeviceToken:[APNSToken copy]
isSandbox:isSandboxApp];
// Re-fetch any invalidated tokens automatically, this time with the current APNs token, so that
// they are up-to-date. Or this is a fresh install and no apns token stored yet.
if (invalidatedTokens.count > 0 || [_tokenStore cachedTokenInfos].count == 0) {
FIRMessaging_WEAKIFY(self);
[self.installations installationIDWithCompletion:^(NSString *_Nullable identifier,
NSError *_Nullable error) {
FIRMessaging_STRONGIFY(self);
if (self == nil) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeInstanceID017,
@"Instance ID shut down during token reset. Aborting");
return;
}
if (self.currentAPNSInfo == nil) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeInstanceID018,
@"apnsTokenData was set to nil during token reset. Aborting");
return;
}
NSMutableDictionary *tokenOptions = [@{
kFIRMessagingTokenOptionsAPNSKey : self.currentAPNSInfo.deviceToken,
kFIRMessagingTokenOptionsAPNSIsSandboxKey : @(isSandboxApp)
} mutableCopy];
if (self.firebaseAppID) {
tokenOptions[kFIRMessagingTokenOptionsFirebaseAppIDKey] = self.firebaseAppID;
}
for (FIRMessagingTokenInfo *tokenInfo in invalidatedTokens) {
[self fetchNewTokenWithAuthorizedEntity:tokenInfo.authorizedEntity
scope:tokenInfo.scope
instanceID:identifier
options:tokenOptions
handler:^(NSString *_Nullable token,
NSError *_Nullable error){
// Do nothing as callback is not needed and the
// sub-funciton already handle errors.
}];
}
if ([self->_tokenStore cachedTokenInfos].count == 0) {
[self tokenWithAuthorizedEntity:self.fcmSenderID
scope:kFIRMessagingDefaultTokenScope
options:tokenOptions
handler:^(NSString *_Nullable FCMToken, NSError *_Nullable error){
// Do nothing as callback is not needed and the sub-funciton
// already handle errors.
}];
}
}];
}
}
#pragma mark - checkin
- (BOOL)hasValidCheckinInfo {
return self.authService.checkinPreferences.hasValidCheckinInfo;
}
@end

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class FIRMessagingCheckinPreferences;
@protocol FIRHeartbeatLoggerProtocol;
NS_ASSUME_NONNULL_BEGIN
/**
* Represents the action taken on an FCM token.
*/
typedef NS_ENUM(NSInteger, FIRMessagingTokenAction) {
FIRMessagingTokenActionFetch,
FIRMessagingTokenActionDeleteToken,
FIRMessagingTokenActionDeleteTokenAndIID,
};
/**
* Represents the possible results of a token operation.
*/
typedef NS_ENUM(NSInteger, FIRMessagingTokenOperationResult) {
FIRMessagingTokenOperationSucceeded,
FIRMessagingTokenOperationError,
FIRMessagingTokenOperationCancelled,
};
/**
* Callback to invoke once the HTTP call to FIRMessaging backend for updating
* subscription finishes.
*
* @param result The result of the operation.
* @param token If the action for fetching a token and the request was successful, this will hold
* the value of the token. Otherwise nil.
* @param error The error which occurred while performing the token operation. This will be nil
* in case the operation was successful, or if the operation was cancelled.
*/
typedef void (^FIRMessagingTokenOperationCompletion)(FIRMessagingTokenOperationResult result,
NSString *_Nullable token,
NSError *_Nullable error);
@interface FIRMessagingTokenOperation : NSOperation
@property(nonatomic, readonly) FIRMessagingTokenAction action;
@property(nonatomic, readonly, nullable) NSString *authorizedEntity;
@property(nonatomic, readonly, nullable) NSString *scope;
@property(nonatomic, readonly, nullable) NSDictionary<NSString *, NSString *> *options;
@property(nonatomic, readonly, strong) FIRMessagingCheckinPreferences *checkinPreferences;
@property(nonatomic, readonly, strong) NSString *instanceID;
@property(nonatomic, readonly) FIRMessagingTokenOperationResult result;
@property(atomic, strong, nullable) NSURLSessionDataTask *dataTask;
@property(readonly) id<FIRHeartbeatLoggerProtocol> heartbeatLogger;
#pragma mark - Request Construction
+ (NSMutableArray<NSURLQueryItem *> *)standardQueryItemsWithDeviceID:(NSString *)deviceID
scope:(NSString *)scope;
- (NSMutableURLRequest *)tokenRequest;
- (instancetype)init NS_UNAVAILABLE;
#pragma mark - Initialization
- (instancetype)initWithAction:(FIRMessagingTokenAction)action
forAuthorizedEntity:(nullable NSString *)authorizedEntity
scope:(NSString *)scope
options:(nullable NSDictionary<NSString *, NSString *> *)options
checkinPreferences:(FIRMessagingCheckinPreferences *)checkinPreferences
instanceID:(NSString *)instanceID
heartbeatLogger:(id<FIRHeartbeatLoggerProtocol>)heartbeatLogger
NS_DESIGNATED_INITIALIZER;
- (void)addCompletionHandler:(FIRMessagingTokenOperationCompletion)handler;
#pragma mark - Result
- (void)finishWithResult:(FIRMessagingTokenOperationResult)result
token:(nullable NSString *)token
error:(nullable NSError *)error;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,230 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenOperation.h"
#import <GoogleUtilities/GULAppEnvironmentUtil.h>
#import "FirebaseCore/Extension/FIRHeartbeatLogger.h"
#import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import "FirebaseMessaging/Sources/FIRMessaging_Private.h"
#import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinPreferences.h"
static const NSInteger kFIRMessagingPlatformVersionIOS = 2;
// Scope parameter that defines the service using the token
static NSString *const kFIRMessagingParamScope = @"X-scope";
// Defines the SDK version
static NSString *const kFIRMessagingParamFCMLibVersion = @"X-cliv";
@interface FIRMessagingTokenOperation () {
BOOL _isFinished;
BOOL _isExecuting;
NSMutableArray<FIRMessagingTokenOperationCompletion> *_completionHandlers;
FIRMessagingCheckinPreferences *_checkinPreferences;
}
@property(nonatomic, readwrite, strong) NSString *instanceID;
@property(atomic, strong, nullable) NSString *FISAuthToken;
@end
@implementation FIRMessagingTokenOperation
- (instancetype)initWithAction:(FIRMessagingTokenAction)action
forAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope
options:(NSDictionary<NSString *, NSString *> *)options
checkinPreferences:(FIRMessagingCheckinPreferences *)checkinPreferences
instanceID:(NSString *)instanceID
heartbeatLogger:(id<FIRHeartbeatLoggerProtocol>)heartbeatLogger {
self = [super init];
if (self) {
_action = action;
_authorizedEntity = [authorizedEntity copy];
_scope = [scope copy];
_options = [options copy];
_checkinPreferences = checkinPreferences;
_instanceID = instanceID;
_completionHandlers = [[NSMutableArray alloc] init];
_heartbeatLogger = heartbeatLogger;
_isExecuting = NO;
_isFinished = NO;
}
return self;
}
- (void)dealloc {
[_completionHandlers removeAllObjects];
}
- (void)addCompletionHandler:(FIRMessagingTokenOperationCompletion)handler {
[_completionHandlers addObject:[handler copy]];
}
- (BOOL)isAsynchronous {
return YES;
}
- (BOOL)isExecuting {
return _isExecuting;
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_isExecuting = executing;
[self didChangeValueForKey:@"isExecuting"];
}
- (BOOL)isFinished {
return _isFinished;
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_isFinished = finished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)start {
if (self.isCancelled) {
[self finishWithResult:FIRMessagingTokenOperationCancelled token:nil error:nil];
return;
}
// Quickly validate whether or not the operation has all it needs to begin
BOOL checkinfoAvailable = [self.checkinPreferences hasCheckinInfo];
if (!checkinfoAvailable) {
FIRMessagingErrorCode errorCode = kFIRMessagingErrorCodeRegistrarFailedToCheckIn;
[self finishWithResult:FIRMessagingTokenOperationError
token:nil
error:[NSError messagingErrorWithCode:errorCode
failureReason:
@"Failed to checkin before token registration."]];
return;
}
[self setExecuting:YES];
[[FIRInstallations installations]
authTokenWithCompletion:^(FIRInstallationsAuthTokenResult *_Nullable tokenResult,
NSError *_Nullable error) {
if (tokenResult.authToken.length > 0) {
self.FISAuthToken = tokenResult.authToken;
[self performTokenOperation];
} else {
[self finishWithResult:FIRMessagingTokenOperationError token:nil error:error];
}
}];
}
- (void)finishWithResult:(FIRMessagingTokenOperationResult)result
token:(nullable NSString *)token
error:(nullable NSError *)error {
// Add a check to prevent this finish from being called more than once.
if (self.isFinished) {
return;
}
self.dataTask = nil;
_result = result;
for (FIRMessagingTokenOperationCompletion completionHandler in _completionHandlers) {
completionHandler(result, token, error);
}
[self setExecuting:NO];
[self setFinished:YES];
}
- (void)cancel {
[super cancel];
[self.dataTask cancel];
[self finishWithResult:FIRMessagingTokenOperationCancelled token:nil error:nil];
}
- (void)performTokenOperation {
}
- (NSMutableURLRequest *)tokenRequest {
NSString *authHeader =
[FIRMessagingTokenOperation HTTPAuthHeaderFromCheckin:self.checkinPreferences];
return [[self class] requestWithAuthHeader:authHeader FISAuthToken:self.FISAuthToken];
}
#pragma mark - Request Construction
+ (NSMutableURLRequest *)requestWithAuthHeader:(NSString *)authHeaderString
FISAuthToken:(NSString *)FISAuthToken {
NSURL *url = [NSURL URLWithString:FIRMessagingTokenRegisterServer()];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// Add HTTP headers
[request setValue:authHeaderString forHTTPHeaderField:@"Authorization"];
[request setValue:FIRMessagingAppIdentifier() forHTTPHeaderField:@"app"];
if (FISAuthToken) {
[request setValue:FISAuthToken forHTTPHeaderField:@"x-goog-firebase-installations-auth"];
}
request.HTTPMethod = @"POST";
return request;
}
+ (NSMutableArray<NSURLQueryItem *> *)standardQueryItemsWithDeviceID:(NSString *)deviceID
scope:(NSString *)scope {
NSMutableArray<NSURLQueryItem *> *queryItems = [NSMutableArray arrayWithCapacity:8];
// E.g. X-osv=10.2.1
NSString *systemVersion = [GULAppEnvironmentUtil systemVersion];
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"X-osv" value:systemVersion]];
// E.g. device=
if (deviceID) {
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"device" value:deviceID]];
}
// E.g. X-scope=fcm
if (scope) {
[queryItems addObject:[NSURLQueryItem queryItemWithName:kFIRMessagingParamScope value:scope]];
}
// E.g. plat=2
NSString *platform = [NSString stringWithFormat:@"%ld", (long)kFIRMessagingPlatformVersionIOS];
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"plat" value:platform]];
// E.g. app=com.myapp.foo
NSString *appIdentifier = FIRMessagingAppIdentifier();
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"app" value:appIdentifier]];
// E.g. app_ver=1.5
NSString *appVersion = FIRMessagingCurrentAppVersion();
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"app_ver" value:appVersion]];
// E.g. X-cliv=fiid-1.2.3
NSString *fcmLibraryVersion =
[NSString stringWithFormat:@"fiid-%@", [FIRMessaging FIRMessagingSDKVersion]];
if (fcmLibraryVersion.length) {
NSURLQueryItem *gcmLibVersion =
[NSURLQueryItem queryItemWithName:kFIRMessagingParamFCMLibVersion value:fcmLibraryVersion];
[queryItems addObject:gcmLibVersion];
}
return queryItems;
}
#pragma mark - Header
+ (NSString *)HTTPAuthHeaderFromCheckin:(FIRMessagingCheckinPreferences *)checkin {
NSString *deviceID = checkin.deviceID;
NSString *secret = checkin.secretToken;
return [NSString stringWithFormat:@"AidLogin %@:%@", deviceID, secret];
}
@end

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class FIRMessagingAPNSInfo;
@class FIRMessagingAuthKeychain;
@class FIRMessagingTokenInfo;
/**
* This class is responsible for retrieving and saving `FIRMessagingTokenInfo` objects from the
* keychain. The keychain keys that are used are:
* Account: <Main App Bundle ID> (e.g. com.mycompany.myapp)
* Service: <Sender ID>:<Scope> (e.g. 1234567890:*)
*/
@interface FIRMessagingTokenStore : NSObject
NS_ASSUME_NONNULL_BEGIN
- (instancetype)init;
#pragma mark - Get
/**
* Get the cached token from the Keychain.
*
* @param authorizedEntity The authorized entity for the token.
* @param scope The scope for the token.
*
* @return The cached token info if any for the given authorizedEntity and scope else
* nil.
*/
- (nullable FIRMessagingTokenInfo *)tokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope;
/**
* Return all cached token infos from the Keychain.
*
* @return The cached token infos, if any, that are stored in the Keychain.
*/
- (NSArray<FIRMessagingTokenInfo *> *)cachedTokenInfos;
#pragma mark - Save
/**
* Save the instanceID token info to the persistent store.
*
* @param tokenInfo The token info to store.
* @param handler The callback handler which is invoked when token saving is complete,
* with an error if there is any.
*/
- (void)saveTokenInfo:(FIRMessagingTokenInfo *)tokenInfo
handler:(nullable void (^)(NSError *))handler;
#pragma mark - Delete
/**
* Remove the cached token from Keychain.
*
* @param authorizedEntity The authorized entity for the token.
* @param scope The scope for the token.
*
*/
- (void)removeTokenWithAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope;
/**
* Remove all the cached tokens from the Keychain.
* @param handler The callback handler which is invoked when tokens deletion is complete,
* with an error if there is any.
*
*/
- (void)removeAllTokensWithHandler:(nullable void (^)(NSError *))handler;
/*
* Only save to local cache but not keychain. This is used when old
* InstanceID SDK updates the token in the keychain, Messaging
* should update its cache without writing to keychain again.
* @param tokenInfo The token info need to be updated in the cache.
*/
- (void)saveTokenInfoInCache:(FIRMessagingTokenInfo *)tokenInfo;
NS_ASSUME_NONNULL_END
@end

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenStore.h"
#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
#import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
#import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingAuthKeychain.h"
#import "FirebaseMessaging/Sources/Token/FIRMessagingTokenInfo.h"
static NSString *const kFIRMessagingTokenKeychainId = @"com.google.iid-tokens";
@interface FIRMessagingTokenStore ()
@property(nonatomic, readwrite, strong) FIRMessagingAuthKeychain *keychain;
@end
@implementation FIRMessagingTokenStore
- (instancetype)init {
self = [super init];
if (self) {
_keychain = [[FIRMessagingAuthKeychain alloc] initWithIdentifier:kFIRMessagingTokenKeychainId];
}
return self;
}
#pragma mark - Get
+ (NSString *)serviceKeyForAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope {
return [NSString stringWithFormat:@"%@:%@", authorizedEntity, scope];
}
- (nullable FIRMessagingTokenInfo *)tokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity
scope:(NSString *)scope {
// TODO(chliangGoogle): If we don't have the token plist we should delete all the tokens from
// the keychain. This is because not having the plist signifies a backup and restore operation.
// In case the keychain has any tokens these would now be stale and therefore should be
// deleted.
if (![authorizedEntity length] || ![scope length]) {
return nil;
}
NSString *account = FIRMessagingAppIdentifier();
NSString *service = [[self class] serviceKeyForAuthorizedEntity:authorizedEntity scope:scope];
NSData *item = [self.keychain dataForService:service account:account];
if (!item) {
return nil;
}
// Token infos created from legacy storage don't have appVersion, firebaseAppID, or APNSInfo.
FIRMessagingTokenInfo *tokenInfo = [[self class] tokenInfoFromKeychainItem:item];
if ([tokenInfo needsMigration]) {
[self
saveTokenInfo:tokenInfo
handler:^(NSError *error) {
if (error) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenManager001,
@"Failed to migrate token: %@ account: %@ service %@",
tokenInfo, account, service);
} else {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenManager001,
@"Successful token migration: %@ account: %@ service %@",
tokenInfo, account, service);
}
}];
}
return tokenInfo;
}
- (NSArray<FIRMessagingTokenInfo *> *)cachedTokenInfos {
NSString *account = FIRMessagingAppIdentifier();
NSArray<NSData *> *items =
[self.keychain itemsMatchingService:kFIRMessagingKeychainWildcardIdentifier account:account];
NSMutableArray<FIRMessagingTokenInfo *> *tokenInfos =
[NSMutableArray arrayWithCapacity:items.count];
for (NSData *item in items) {
FIRMessagingTokenInfo *tokenInfo = [[self class] tokenInfoFromKeychainItem:item];
if (tokenInfo) {
[tokenInfos addObject:tokenInfo];
}
}
return tokenInfos;
}
+ (nullable FIRMessagingTokenInfo *)tokenInfoFromKeychainItem:(NSData *)item {
// Check if it is saved as an archived FIRMessagingTokenInfo, otherwise return nil.
FIRMessagingTokenInfo *tokenInfo = nil;
// NOTE: Passing in nil to unarchiveObjectWithData will result in an iOS error logged
// in the console on iOS 10 and below. Avoid by checking item.data's existence.
if (item) {
// TODO(chliangGoogle: Use the new API and secureCoding protocol.
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[NSKeyedUnarchiver setClass:[FIRMessagingTokenInfo class]
forClassName:@"FIRInstanceIDTokenInfo"];
tokenInfo = [NSKeyedUnarchiver unarchiveObjectWithData:item];
#pragma clang diagnostic pop
} @catch (NSException *exception) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTokenStoreExceptionUnarchivingTokenInfo,
@"Unable to parse token info from Keychain item; item was in an "
@"invalid format");
tokenInfo = nil;
} @finally {
}
}
return tokenInfo;
}
#pragma mark - Save
// Token Infos will be saved under these Keychain keys:
// Account: <Main App Bundle ID> (e.g. com.mycompany.myapp)
// Service: <Sender ID>:<Scope> (e.g. 1234567890:*)
- (void)saveTokenInfo:(FIRMessagingTokenInfo *)tokenInfo
handler:(void (^)(NSError *))handler { // Keep the cachetime up-to-date.
tokenInfo.cacheTime = [NSDate date];
// Always write to the Keychain, so that the cacheTime is up-to-date.
NSData *tokenInfoData;
// TODO(chliangGoogle: Use the new API and secureCoding protocol.
[NSKeyedArchiver setClassName:@"FIRInstanceIDTokenInfo" forClass:[FIRMessagingTokenInfo class]];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
tokenInfoData = [NSKeyedArchiver archivedDataWithRootObject:tokenInfo];
#pragma clang diagnostic pop
NSString *account = FIRMessagingAppIdentifier();
NSString *service = [[self class] serviceKeyForAuthorizedEntity:tokenInfo.authorizedEntity
scope:tokenInfo.scope];
[self.keychain setData:tokenInfoData forService:service account:account handler:handler];
}
- (void)saveTokenInfoInCache:(FIRMessagingTokenInfo *)tokenInfo {
tokenInfo.cacheTime = [NSDate date];
// TODO(chliangGoogle): Use the new API and secureCoding protocol.
// Always write to the Keychain, so that the cacheTime is up-to-date.
NSData *tokenInfoData;
[NSKeyedArchiver setClassName:@"FIRInstanceIDTokenInfo" forClass:[FIRMessagingTokenInfo class]];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
tokenInfoData = [NSKeyedArchiver archivedDataWithRootObject:tokenInfo];
#pragma clang diagnostic pop
NSString *account = FIRMessagingAppIdentifier();
NSString *service = [[self class] serviceKeyForAuthorizedEntity:tokenInfo.authorizedEntity
scope:tokenInfo.scope];
[self.keychain setCacheData:tokenInfoData forService:service account:account];
}
#pragma mark - Delete
- (void)removeTokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity
scope:(nonnull NSString *)scope {
if (![authorizedEntity length] || ![scope length]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeStore012,
@"Will not delete token with invalid entity: %@, scope: %@",
authorizedEntity, scope);
return;
}
NSString *account = FIRMessagingAppIdentifier();
NSString *service = [[self class] serviceKeyForAuthorizedEntity:authorizedEntity scope:scope];
[self.keychain removeItemsMatchingService:service account:account handler:nil];
}
- (void)removeAllTokensWithHandler:(void (^)(NSError *error))handler {
NSString *account = FIRMessagingAppIdentifier();
[self.keychain removeItemsMatchingService:kFIRMessagingKeychainWildcardIdentifier
account:account
handler:handler];
}
@end

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2018 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@protocol FIRAnalyticsInteropListener;
NS_ASSUME_NONNULL_BEGIN
/// Block typedef callback parameter to `getUserProperties(with:)`.
typedef void (^FIRAInteropUserPropertiesCallback)(NSDictionary<NSString *, id> *userProperties)
NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.");
/// Connector for bridging communication between Firebase SDKs and FirebaseAnalytics APIs.
@protocol FIRAnalyticsInterop
/// Sets user property when trigger event is logged. This API is only available in the SDK.
- (void)setConditionalUserProperty:(NSDictionary<NSString *, id> *)conditionalUserProperty;
/// Clears user property if set.
- (void)clearConditionalUserProperty:(NSString *)userPropertyName
forOrigin:(NSString *)origin
clearEventName:(NSString *)clearEventName
clearEventParameters:(NSDictionary<NSString *, NSString *> *)clearEventParameters;
/// Returns currently set user properties.
- (NSArray<NSDictionary<NSString *, NSString *> *> *)conditionalUserProperties:(NSString *)origin
propertyNamePrefix:
(NSString *)propertyNamePrefix;
/// Returns the maximum number of user properties.
- (NSInteger)maxUserProperties:(NSString *)origin;
/// Returns the user properties to a callback function.
- (void)getUserPropertiesWithCallback:
(void (^)(NSDictionary<NSString *, id> *userProperties))callback;
/// Logs events.
- (void)logEventWithOrigin:(NSString *)origin
name:(NSString *)name
parameters:(nullable NSDictionary<NSString *, id> *)parameters;
/// Sets user property.
- (void)setUserPropertyWithOrigin:(NSString *)origin name:(NSString *)name value:(id)value;
/// Registers an Analytics listener for the given origin.
- (void)registerAnalyticsListener:(id<FIRAnalyticsInteropListener>)listener
withOrigin:(NSString *)origin;
/// Unregisters an Analytics listener for the given origin.
- (void)unregisterAnalyticsListenerWithOrigin:(NSString *)origin;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// Handles events and messages from Analytics.
@protocol FIRAnalyticsInteropListener <NSObject>
/// Triggers when an Analytics event happens for the registered origin with
/// FirebaseAnalyticsInterop`s `registerAnalyticsListener(_:withOrigin:)`.
- (void)messageTriggered:(NSString *)name parameters:(NSDictionary *)parameters;
@end

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2018 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// @file FIRInteropEventNames.h
#import <Foundation/Foundation.h>
/// Notification open event name.
static NSString *const kFIRIEventNotificationOpen = @"_no";
/// Notification foreground event name.
static NSString *const kFIRIEventNotificationForeground = @"_nf";
/// Campaign event name.
static NSString *const kFIRIEventFirebaseCampaign = @"_cmp";

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2018 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
/// @file FIRInteropParameterNames.h
///
/// Predefined event parameter names used by Firebase. This file is a subset of the
/// FirebaseAnalytics FIRParameterNames.h public header.
///
/// The origin of your traffic, such as an Ad network (for example, google) or partner (urban
/// airship). Identify the advertiser, site, publication, etc. that is sending traffic to your
/// property. Highly recommended (String).
/// <pre>
/// let params = [
/// kFIRParameterSource : "InMobi",
/// // ...
/// ]
/// </pre>
static NSString *const kFIRIParameterSource NS_SWIFT_NAME(AnalyticsParameterSource) = @"source";
/// The advertising or marketing medium, for example: cpc, banner, email, push. Highly recommended
/// (String).
/// <pre>
/// let params = [
/// kFIRParameterMedium : "email",
/// // ...
/// ]
/// </pre>
static NSString *const kFIRIParameterMedium NS_SWIFT_NAME(AnalyticsParameterMedium) = @"medium";
/// The individual campaign name, slogan, promo code, etc. Some networks have pre-defined macro to
/// capture campaign information, otherwise can be populated by developer. Highly Recommended
/// (String).
/// <pre>
/// let params = [
/// kFIRParameterCampaign : "winter_promotion",
/// // ...
/// ]
/// </pre>
static NSString *const kFIRIParameterCampaign NS_SWIFT_NAME(AnalyticsParameterCampaign) =
@"campaign";
/// Message identifier.
static NSString *const kFIRIParameterMessageIdentifier = @"_nmid";
/// Message name.
static NSString *const kFIRIParameterMessageName = @"_nmn";
/// Message send time.
static NSString *const kFIRIParameterMessageTime = @"_nmt";
/// Message device time.
static NSString *const kFIRIParameterMessageDeviceTime = @"_ndt";
/// Topic message.
static NSString *const kFIRIParameterTopic = @"_nt";
/// Stores the message_id of the last notification opened by the app.
static NSString *const kFIRIUserPropertyLastNotification = @"_ln";

202
Pods/FirebaseMessaging/LICENSE generated Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

297
Pods/FirebaseMessaging/README.md generated Normal file
View File

@@ -0,0 +1,297 @@
<p align="center">
<a href="https://cocoapods.org/pods/Firebase">
<img src="https://img.shields.io/github/v/release/Firebase/firebase-ios-sdk?style=flat&label=CocoaPods"/>
</a>
<a href="https://swiftpackageindex.com/firebase/firebase-ios-sdk">
<img src="https://img.shields.io/github/v/release/Firebase/firebase-ios-sdk?style=flat&label=Swift%20Package%20Index&color=red"/>
</a>
<a href="https://cocoapods.org/pods/Firebase">
<img src="https://img.shields.io/github/license/Firebase/firebase-ios-sdk?style=flat"/>
</a><br/>
<a href="https://swiftpackageindex.com/firebase/firebase-ios-sdk">
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ffirebase%2Ffirebase-ios-sdk%2Fbadge%3Ftype%3Dplatforms"/>
</a>
<a href="https://swiftpackageindex.com/firebase/firebase-ios-sdk">
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ffirebase%2Ffirebase-ios-sdk%2Fbadge%3Ftype%3Dswift-versions"/>
</a>
</p>
# Firebase Apple Open Source Development
This repository contains the source code for all Apple platform Firebase SDKs except FirebaseAnalytics.
Firebase is an app development platform with tools to help you build, grow, and
monetize your app. More information about Firebase can be found on the
[official Firebase website](https://firebase.google.com).
## Installation
See the subsections below for details about the different installation methods. Where
available, it's recommended to install any libraries with a `Swift` suffix to get the
best experience when writing your app in Swift.
1. [Standard pod install](#standard-pod-install)
2. [Swift Package Manager](#swift-package-manager)
3. [Installing from the GitHub repo](#installing-from-github)
4. [Experimental Carthage](#carthage-ios-only)
### Standard pod install
For instructions on the standard pod install, visit:
[https://firebase.google.com/docs/ios/setup](https://firebase.google.com/docs/ios/setup).
### Swift Package Manager
Instructions for [Swift Package Manager](https://swift.org/package-manager/) support can be
found in the [SwiftPackageManager.md](SwiftPackageManager.md) Markdown file.
### Installing from GitHub
These instructions can be used to access the Firebase repo at other branches,
tags, or commits.
#### Background
See [the Podfile Syntax Reference](https://guides.cocoapods.org/syntax/podfile.html#pod)
for instructions and options about overriding pod source locations.
#### Accessing Firebase Source Snapshots
All official releases are tagged in this repo and available via CocoaPods. To access a local
source snapshot or unreleased branch, use Podfile directives like the following:
To access FirebaseFirestore via a branch:
```ruby
pod 'FirebaseCore', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :branch => 'main'
pod 'FirebaseFirestore', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :branch => 'main'
```
To access FirebaseMessaging via a checked-out version of the firebase-ios-sdk repo:
```ruby
pod 'FirebaseCore', :path => '/path/to/firebase-ios-sdk'
pod 'FirebaseMessaging', :path => '/path/to/firebase-ios-sdk'
```
### Carthage (iOS only)
Instructions for the experimental Carthage distribution can be found at
[Carthage.md](Carthage.md).
### Using Firebase from a Framework or a library
For details on using Firebase from a Framework or a library, refer to [firebase_in_libraries.md](docs/firebase_in_libraries.md).
## Development
To develop Firebase software in this repository, ensure that you have at least
the following software:
* Xcode 14.1 (or later)
CocoaPods is still the canonical way to develop, but much of the repo now supports
development with Swift Package Manager.
### CocoaPods
Install the following:
* CocoaPods 1.12.0 (or later)
* [CocoaPods generate](https://github.com/square/cocoapods-generate)
For the pod that you want to develop:
```ruby
pod gen Firebase{name here}.podspec --local-sources=./ --auto-open --platforms=ios
```
Note: If the CocoaPods cache is out of date, you may need to run
`pod repo update` before the `pod gen` command.
Note: Set the `--platforms` option to `macos` or `tvos` to develop/test for
those platforms. Since 10.2, Xcode does not properly handle multi-platform
CocoaPods workspaces.
Firestore has a self-contained Xcode project. See
[Firestore/README](Firestore/README.md) Markdown file.
#### Development for Catalyst
* `pod gen {name here}.podspec --local-sources=./ --auto-open --platforms=ios`
* Check the Mac box in the App-iOS Build Settings
* Sign the App in the Settings Signing & Capabilities tab
* Click Pods in the Project Manager
* Add Signing to the iOS host app and unit test targets
* Select the Unit-unit scheme
* Run it to build and test
Alternatively, disable signing in each target:
* Go to Build Settings tab
* Click `+`
* Select `Add User-Defined Setting`
* Add `CODE_SIGNING_REQUIRED` setting with a value of `NO`
### Swift Package Manager
* To enable test schemes: `./scripts/setup_spm_tests.sh`
* `open Package.swift` or double click `Package.swift` in Finder.
* Xcode will open the project
* Choose a scheme for a library to build or test suite to run
* Choose a target platform by selecting the run destination along with the scheme
### Adding a New Firebase Pod
Refer to [AddNewPod](AddNewPod.md) Markdown file for details.
### Managing Headers and Imports
For information about managing headers and imports, see [HeadersImports](HeadersImports.md) Markdown file.
### Code Formatting
To ensure that the code is formatted consistently, run the script
[./scripts/check.sh](https://github.com/firebase/firebase-ios-sdk/blob/main/scripts/check.sh)
before creating a pull request (PR).
GitHub Actions will verify that any code changes are done in a style-compliant
way. Install `clang-format` and `mint`:
```console
brew install clang-format@18
brew install mint
```
### Running Unit Tests
Select a scheme and press Command-u to build a component and run its unit tests.
### Running Sample Apps
To run the sample apps and integration tests, you'll need a valid
`GoogleService-Info.plist
` file. The Firebase Xcode project contains dummy plist
files without real values, but they can be replaced with real plist files. To get your own
`GoogleService-Info.plist` files:
1. Go to the [Firebase Console](https://console.firebase.google.com/)
2. Create a new Firebase project, if you don't already have one
3. For each sample app you want to test, create a new Firebase app with the sample app's bundle
identifier (e.g., `com.google.Database-Example`)
4. Download the resulting `GoogleService-Info.plist` and add it to the Xcode project.
### Coverage Report Generation
For coverage report generation instructions, see [scripts/code_coverage_report/README](scripts/code_coverage_report/README.md) Markdown file.
## Specific Component Instructions
See the sections below for any special instructions for those components.
### Firebase Auth
For specific Firebase Auth development, refer to the [Auth Sample README](FirebaseAuth/Tests/Sample/README.md) for instructions about
building and running the FirebaseAuth pod along with various samples and tests.
### Firebase Database
The Firebase Database Integration tests can be run against a locally running Database Emulator
or against a production instance.
To run against a local emulator instance, invoke `./scripts/run_database_emulator.sh start` before
running the integration test.
To run against a production instance, provide a valid `GoogleServices-Info.plist` and copy it to
`FirebaseDatabase/Tests/Resources/GoogleService-Info.plist`. Your Security Rule must be set to
[public](https://firebase.google.com/docs/database/security/quickstart) while your tests are
running.
### Firebase Dynamic Links
Firebase Dynamic Links is **deprecated** and should not be used in new projects. The service will shut down on August 25, 2025.
Please see our [Dynamic Links Deprecation FAQ documentation](https://firebase.google.com/support/dynamic-links-faq) for more guidance.
### Firebase Performance Monitoring
For specific Firebase Performance Monitoring development, see
[the Performance README](FirebasePerformance/README.md) for instructions about building the SDK
and [the Performance TestApp README](FirebasePerformance/Tests/TestApp/README.md) for instructions about
integrating Performance with the dev test App.
### Firebase Storage
To run the Storage Integration tests, follow the instructions in
[StorageIntegration.swift](FirebaseStorage/Tests/Integration/StorageIntegration.swift).
#### Push Notifications
Push notifications can only be delivered to specially provisioned App IDs in the developer portal.
In order to test receiving push notifications, you will need to:
1. Change the bundle identifier of the sample app to something you own in your Apple Developer
account and enable that App ID for push notifications.
2. You'll also need to
[upload your APNs Provider Authentication Key or certificate to the
Firebase Console](https://firebase.google.com/docs/cloud-messaging/ios/certs)
at **Project Settings > Cloud Messaging > [Your Firebase App]**.
3. Ensure your iOS device is added to your Apple Developer portal as a test device.
#### iOS Simulator
The iOS Simulator cannot register for remote notifications and will not receive push notifications.
To receive push notifications, follow the steps above and run the app on a physical device.
## Building with Firebase on Apple platforms
Firebase provides official beta support for macOS, Catalyst, and tvOS. visionOS and watchOS
are community supported. Thanks to community contributions for many of the multi-platform PRs.
At this time, most of Firebase's products are available across Apple platforms. There are still
a few gaps, especially on visionOS and watchOS. For details about the current support matrix, see
[this chart](https://firebase.google.com/docs/ios/learn-more#firebase_library_support_by_platform)
in Firebase's documentation.
### visionOS
Where supported, visionOS works as expected with the exception of Firestore via Swift Package
Manager where it is required to use the source distribution.
To enable the Firestore source distribution, quit Xcode and open the desired
project from the command line with the `FIREBASE_SOURCE_FIRESTORE` environment
variable: `open --env FIREBASE_SOURCE_FIRESTORE /path/to/project.xcodeproj`.
To go back to using the binary distribution of Firestore, quit Xcode and open
Xcode like normal, without the environment variable.
### watchOS
Thanks to contributions from the community, many of Firebase SDKs now compile, run unit tests, and
work on watchOS. See the [Independent Watch App Sample](Example/watchOSSample).
Keep in mind that watchOS is not officially supported by Firebase. While we can catch basic unit
test issues with GitHub Actions, there may be some changes where the SDK no longer works as expected
on watchOS. If you encounter this, please
[file an issue](https://github.com/firebase/firebase-ios-sdk/issues).
During app setup in the console, you may get to a step that mentions something like "Checking if the
app has communicated with our servers". This relies on Analytics and will not work on watchOS.
**It's safe to ignore the message and continue**, the rest of the SDKs will work as expected.
#### Additional Crashlytics Notes
* watchOS has limited support. Due to watchOS restrictions, mach exceptions and signal crashes are
not recorded. (Crashes in SwiftUI are generated as mach exceptions, so will not be recorded)
## Combine
Thanks to contributions from the community, _FirebaseCombineSwift_ contains support for Apple's Combine
framework. This module is currently under development and not yet supported for use in production
environments. For more details, please refer to the [docs](FirebaseCombineSwift/README.md).
## Roadmap
See [Roadmap](ROADMAP.md) for more about the Firebase Apple SDK Open Source
plans and directions.
## Contributing
See [Contributing](CONTRIBUTING.md) for more information on contributing to the Firebase
Apple SDK.
## License
The contents of this repository are licensed under the
[Apache License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
Your use of Firebase is governed by the
[Terms of Service for Firebase Services](https://firebase.google.com/terms/).