diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8d9b6b8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,45 @@ +# Changelog +All notable changes to this package will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [1.0.0-pre.3] - 2021-08-19 + +### Fixed + +* Updated 3rd party code notices + +## [1.0.0-pre.2] - 2021-08-17 + +### New Features + +* The package now uses the Core initialisation flow. + +### Fixed + +* Subsequent updates of the Android token will no longer cause a null pointer exception + +## [1.0.0-pre.1] - 2021-08-05 + +### New Features + +* Support for Android devices, including rich push notifications +* A simplified, unified external interface that allows one registration for both platforms + +### Fixed + +* Notifications now show when app is in the foreground on iOS + +## [0.1.0-preview] - 2021-07-27 + +### New Features + +* Support for iOS platform, including rich push notifications +* Push notification analytics - notificationOpened and notificationServices events + +### Known Issues + +* Android is not currently supported +* Analytics platform is hard coded to "IOS" as this integration is changing in the next release and currently this is the only platform we support +* Analytics country is hard coded to "GB" as this integration is changing in the next release and the currently released Analytics package doesn't provide this API diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta new file mode 100644 index 0000000..ef1d369 --- /dev/null +++ b/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6191b06c9d0954509824711f36813692 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Documentation~/index.md b/Documentation~/index.md new file mode 100644 index 0000000..f831bd3 --- /dev/null +++ b/Documentation~/index.md @@ -0,0 +1,78 @@ +# Push Notifications SDK Documentation + +The Push Notifications SDK allows you to send rich push notifications to mobile devices. + +## Platform Support + +This SDK supports both iOS and Android. iOS 10+ and Android SDK > 26 (Oreo) are supported. + +## Quick Start + +The SDK comes with a sample script that will register for push notifications. Simply add this to your project to get going. + +## Registering for Push Notifications + +To register for push notifications, two steps are required. Firstly, you need to initialise Unity Services, so that the required analytics events can be sent to Unity Analytics 2.0. + +Once that is complete, you can then register for notifications. To ensure no notifications are missed, this should be done in the startup code for your game. Note that on first registration on iOS, a user will be shown a permission request, so ensure this call is made at a convenient place in your game. The SDK will handle the showing of notification content, including images, titles and message body. + +A full code sample is shown below. + +```cs +// In a monobehaviour in a convenient place in your game. +async void Start() +{ + await UnityServices.InitializeAsync(); + PushNotificationSettings settings = new PushNotificationSettings() + { + AndroidApiKey = "API_KEY", + AndroidSenderId = "SENDER_ID", + AndroidApplicationId = "APPLICATION_ID", + AndroidProjectId = "PROJECT_ID" + }; + + try + { + + string pushToken = await PushNotifications.RegisterForPushNotificationsAsync(settings); + + PushNotifications.OnNotificationReceived += notificationData => + { + Debug.Log("Received a notification!"); + }; + } + catch (Exception e) + { + Debug.Log("Failed to retrieve a push notification token."); + } +} +``` + +### Notification Received Callbacks + +You can register a delegate to receive a C# event callback when a notification is received, if you wish to perform custom behaviour at that point. To do this, add a delegate / method callback to `PushNotifications.OnNotificationReceived` as indicated in the sample above. + +### Push Notification Settings + +The SDK requires a number of settings in order to function correctly. Some settings are only used on a certain platform, this is indicated in the setting name. The following settings can be set: + +* AndroidApiKey: The API key for a Firebase project to be used for Android's Firebase Cloud Messaging API. This can be found in your Firebase dashboard. +* AndroidSenderId: The sender ID to be used for Android's Firebase Cloud Messaging. This can be found in your Firebase dashboard. +* AndroidApplicationId: The application ID for a Firebase application to be used for Android's Firebase Cloud Messaging API. This can be found in your Firebase dashboard. +* AndroidProjectId: The project ID for a Firebase project to be used for Android's Firebase Cloud Messaging API. This can be found in your Firebase dashboard. + +### Analytics + +The SDK will records two analytics events: + +* `notificationServices`: This event is recorded whenever a new token is registered on the client. It contains the push token and is used to register this token with the backend service. +* `notificationOpened`: This event is recorded whenever a notification is opened by a user. It contains data regarding which campaign and cohort the user was in, and whether the app was launched from the notification. + +## Analytics Only Mode + +> **Note:** This section is only required if you have an existing push notification solution you wish to use alongside the Unity Push Notifications package. +> This will result in reduced functionality of this package - e.g. images in notifications will not be displayed. + +It is possible to integrate the SDK with an existing push notification service if required. To do so, use the two methods in the `PushNotifications.Analytics` class. + +`RecordPushTokenUpdated` should be called when you receive \ No newline at end of file diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..07a509f --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9ec5f4dd555924fcba7fefacf0065ebd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Android.meta b/Editor/Android.meta new file mode 100644 index 0000000..f444606 --- /dev/null +++ b/Editor/Android.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8c43c5553e6646dba2ce2f141425301d +timeCreated: 1627632749 \ No newline at end of file diff --git a/Editor/Android/InsertPushNotificationDependenciesIntoGradleScript.cs b/Editor/Android/InsertPushNotificationDependenciesIntoGradleScript.cs new file mode 100644 index 0000000..b844357 --- /dev/null +++ b/Editor/Android/InsertPushNotificationDependenciesIntoGradleScript.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.IO; +using UnityEditor; +using UnityEditor.Android; +using UnityEngine; + +namespace Editor +{ + public class InsertPushNotificationDependenciesIntoGradleScript: IPostGenerateGradleAndroidProject + { + public int callbackOrder => 0; + + const string k_GradleDependencyOpeningTag = "dependencies {"; + + readonly Dictionary m_Dependencies = new Dictionary + { + {"com.google.firebase:firebase-messaging-ktx", "22.0.0"} + }; + + public void OnPostGenerateGradleAndroidProject(string path) + { + string libraryBuildGradlePath = Path.Combine(path, "build.gradle"); + string buildGradleFileContent = File.ReadAllText(libraryBuildGradlePath); + + string dependencyString = ""; + + foreach (var keyValuePair in m_Dependencies) + { + string library = keyValuePair.Key; + string version = keyValuePair.Value; + + if (!buildGradleFileContent.Contains(library)) + { + dependencyString = $"{dependencyString} implementation '{library}:{version}'\n"; + } + } + + string updatedBuildGradleFileContent = buildGradleFileContent.Replace(k_GradleDependencyOpeningTag, $"{k_GradleDependencyOpeningTag}\n{dependencyString}"); + File.WriteAllText(libraryBuildGradlePath, updatedBuildGradleFileContent); + +#if UNITY_2020_1_OR_NEWER + string projectRoot = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar)); + string gradlePropertiesFilePath = Path.Combine(projectRoot, "gradle.properties"); + + string gradlePropertiesFileContent = File.Exists(gradlePropertiesFilePath) ? File.ReadAllText(gradlePropertiesFilePath) : ""; + + string updatedPropertiesFileContent = gradlePropertiesFileContent; + if (!gradlePropertiesFileContent.Contains("android.useAndroidX=")) + { + updatedPropertiesFileContent = $"{gradlePropertiesFileContent}\nandroid.useAndroidX=true"; + } + else if (gradlePropertiesFileContent.Contains("android.useAndroidX=false")) + { + Debug.LogWarning("The Unity Push Notifications SDK requires androidx support. We've updated your gradle.properties file to enable androidX, check this is appropriate for your use case."); + updatedPropertiesFileContent = gradlePropertiesFileContent.Replace("android.useAndroidX=false", "android.useAndroidX=true"); + } + File.WriteAllText(gradlePropertiesFilePath, updatedPropertiesFileContent); +#endif + } + } +} diff --git a/Editor/Android/InsertPushNotificationDependenciesIntoGradleScript.cs.meta b/Editor/Android/InsertPushNotificationDependenciesIntoGradleScript.cs.meta new file mode 100644 index 0000000..3831525 --- /dev/null +++ b/Editor/Android/InsertPushNotificationDependenciesIntoGradleScript.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 279633b7a997e40638d3ded8c5b5da50 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Unity.Services.PushNotifications.Editor.asmdef b/Editor/Unity.Services.PushNotifications.Editor.asmdef new file mode 100644 index 0000000..0925343 --- /dev/null +++ b/Editor/Unity.Services.PushNotifications.Editor.asmdef @@ -0,0 +1,10 @@ +{ + "name": "Unity.Services.PushNotifications.Editor", + "references": [ + "Unity.Services.PushNotifications" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [] +} diff --git a/Editor/Unity.Services.PushNotifications.Editor.asmdef.meta b/Editor/Unity.Services.PushNotifications.Editor.asmdef.meta new file mode 100644 index 0000000..88289cb --- /dev/null +++ b/Editor/Unity.Services.PushNotifications.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d56c93ce3337747b88483398f7b6e124 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/iOS.meta b/Editor/iOS.meta new file mode 100644 index 0000000..98c2760 --- /dev/null +++ b/Editor/iOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cad6594013c6845bea0f68fbf20852fc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/iOS/IOSBasicPushNotificationPostProcess.cs b/Editor/iOS/IOSBasicPushNotificationPostProcess.cs new file mode 100644 index 0000000..0fde919 --- /dev/null +++ b/Editor/iOS/IOSBasicPushNotificationPostProcess.cs @@ -0,0 +1,89 @@ +#if UNITY_IOS +using System; +using System.IO; +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEditor.iOS.Xcode; +using UnityEngine; + +namespace Unity.Services.PushNotifications.Editor +{ + public class IOSBasicPushNotificationPostProcess : MonoBehaviour + { + [PostProcessBuild(0)] + public static void OnPostProcessBuild(BuildTarget buildTarget, String path) + { + if (buildTarget != BuildTarget.iOS) + { + return; + } + + string projectPath = PBXProject.GetPBXProjectPath(path); + PBXProject project = new PBXProject(); + project.ReadFromFile(projectPath); + + string mainTargetGuid = project.GetUnityMainTargetGuid(); + string unityFrameworkGuid = project.GetUnityFrameworkTargetGuid(); + project.AddFrameworkToProject(unityFrameworkGuid, "UserNotifications.framework", true); + + AddCapabilities(project, projectPath, mainTargetGuid, path); + UpdateInfoPlist(path); + UpdatePreprocessorFile(path); + + project.WriteToFile(projectPath); + } + + static void AddCapabilities(PBXProject project, string projectPath, string mainTargetGuid, string buildPath) + { + var entitlementsFileName = project.GetBuildPropertyForConfig(mainTargetGuid, "CODE_SIGN_ENTITLEMENTS"); + if (entitlementsFileName == null) + { + string bundleIdentifier = PlayerSettings.GetApplicationIdentifier(BuildTargetGroup.iOS); + entitlementsFileName = $"{bundleIdentifier.Substring(bundleIdentifier.LastIndexOf(".") + 1)}.entitlements"; + + // Add the entitlements file to the build + project.AddFile(Path.Combine(buildPath, entitlementsFileName), entitlementsFileName); + project.AddBuildProperty(mainTargetGuid, "CODE_SIGN_ENTITLEMENTS", entitlementsFileName); + } + + var capManager = new ProjectCapabilityManager(projectPath, entitlementsFileName, "Unity-iPhone"); + + // TODO: Do we want to allow the user to specify which push environment they want to use, or should we always assume + // live? If so, the below will need updating with a new setting. + const bool useDevEnvironment = false; + capManager.AddPushNotifications(useDevEnvironment); + + capManager.WriteToFile(); + } + + /* + * Add the remote notification background mode to the Xcode Info.plist file. Without this, we won't + * get access to the correct background modes and iOS won't successfully register for remote notifications. + */ + static void UpdateInfoPlist(string projectPath) + { + string plistPath = projectPath + "/Info.plist"; + PlistDocument plist = new PlistDocument(); + plist.ReadFromFile(plistPath); + + PlistElementArray existingBackgroundModes = (PlistElementArray) plist.root["UIBackgroundModes"] ?? plist.root.CreateArray("UIBackgroundModes"); + existingBackgroundModes.AddString("remote-notification"); + + plist.WriteToFile(plistPath); + } + + /* + * Update the preprocessor file to ensure Unity sends us registration callbacks when we register for remote notifications. + * If we don't do this, we are able to register, but we can't get access to the token in the native plugin to pass back + * to Unity. + */ + static void UpdatePreprocessorFile(string projectPath) + { + string preprocessorPath = projectPath + "/Classes/Preprocessor.h"; + var preprocessor = File.ReadAllText(preprocessorPath); + preprocessor = preprocessor.Replace("UNITY_USES_REMOTE_NOTIFICATIONS 0", "UNITY_USES_REMOTE_NOTIFICATIONS 1"); + File.WriteAllText(preprocessorPath, preprocessor); + } + } +} +#endif diff --git a/Editor/iOS/IOSBasicPushNotificationPostProcess.cs.meta b/Editor/iOS/IOSBasicPushNotificationPostProcess.cs.meta new file mode 100644 index 0000000..f7b25ad --- /dev/null +++ b/Editor/iOS/IOSBasicPushNotificationPostProcess.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 89eec7982b5884270974987553813942 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/iOS/IOSRichPushNotificationPostProcess.cs b/Editor/iOS/IOSRichPushNotificationPostProcess.cs new file mode 100644 index 0000000..08dec0e --- /dev/null +++ b/Editor/iOS/IOSRichPushNotificationPostProcess.cs @@ -0,0 +1,130 @@ +#if UNITY_IOS +using System.IO; +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEditor.iOS.Xcode; +using UnityEditor.iOS.Xcode.Extensions; +using UnityEngine; + +namespace Unity.Services.PushNotifications.Editor +{ + public class IOSRichPushNotificationPostProcess : MonoBehaviour + { + const string k_PathToInfoPlistInsideTarget = "Info.plist"; + + [PostProcessBuild(1)] + public static void CreateRichPushNotificationTarget(BuildTarget buildTarget, string buildOutputPath) + { + if (buildTarget != BuildTarget.iOS) + { + return; + } + + string xcodeProjectPath = PBXProject.GetPBXProjectPath(buildOutputPath); + PBXProject project = new PBXProject(); + project.ReadFromFile(xcodeProjectPath); + + string guidOfInitialTarget = project.GetUnityMainTargetGuid(); + + string bundleIdentifierForNotificationService = Application.identifier + ".notificationservice"; + int indexOfLastIdentifierSection = bundleIdentifierForNotificationService.LastIndexOf('.') + 1; + string displayName = bundleIdentifierForNotificationService.Substring(indexOfLastIdentifierSection); + + string pathToNotificationServiceImplementation = Path.Combine(buildOutputPath, displayName); + + if (!Directory.Exists(pathToNotificationServiceImplementation)) + { + Directory.CreateDirectory(pathToNotificationServiceImplementation); + } + + PlistDocument notificationServicePlist = new PlistDocument(); + string plistTemplatePath = Path.Combine(GetPathToSourceDirectory(), "Info.plist"); + notificationServicePlist.ReadFromFile(plistTemplatePath); + notificationServicePlist.root.SetString("CFBundleDisplayName", displayName); + notificationServicePlist.root.SetString("CFBundleIdentifier", bundleIdentifierForNotificationService); + notificationServicePlist.root.SetString("CFBundleShortVersionString", PlayerSettings.bundleVersion); + notificationServicePlist.root.SetString("CFBundleVersion", PlayerSettings.iOS.buildNumber); + string pathToNotificationServicePlist = Path.Combine(pathToNotificationServiceImplementation, k_PathToInfoPlistInsideTarget); + notificationServicePlist.WriteToFile(pathToNotificationServicePlist); + + string guidOfExtension = project.AddAppExtension(guidOfInitialTarget, + displayName, + bundleIdentifierForNotificationService, + pathToNotificationServicePlist + ); + string buildPhaseId = project.AddSourcesBuildPhase(guidOfExtension); + + AddSourceFileToProject( + project, + "NotificationService.h", + displayName, + guidOfExtension, + buildPhaseId, + pathToNotificationServiceImplementation + ); + AddSourceFileToProject( + project, + "NotificationService.m", + displayName, + guidOfExtension, + buildPhaseId, + pathToNotificationServiceImplementation + ); + AddFileToProject( + project, + pathToNotificationServicePlist, + "Info.plist", + displayName + ); + + project.AddFrameworkToProject(guidOfExtension, "NotificationCenter.framework", true); + project.AddFrameworkToProject(guidOfExtension, "UserNotifications.framework", true); + project.SetBuildProperty(guidOfExtension, "ARCHS", "$(ARCHS_STANDARD"); + project.SetBuildProperty(guidOfExtension, "DEVELOPMENT_TEAM", PlayerSettings.iOS.appleDeveloperTeamID); + + string[] copyableProperties = + { + "IPHONEOS_DEPLOYMENT_TARGET", + "TARGETED_DEVICE_FAMILY" + }; + foreach (string copyableProperty in copyableProperties) + { + string originalBuildProperty = project.GetBuildPropertyForAnyConfig(guidOfInitialTarget, copyableProperty); + project.SetBuildProperty(guidOfExtension, copyableProperty, originalBuildProperty); + } + + project.WriteToFile(xcodeProjectPath); + } + + static void AddSourceFileToProject( + PBXProject project, + string filename, + string extensionDisplayName, + string extensionGuid, + string buildPhase, + string pathToImplementation + ) + { + string sourceFilepath = Path.Combine(GetPathToSourceDirectory(), filename); + string destinationFilepath = Path.Combine(pathToImplementation, filename); + if (File.Exists(destinationFilepath)) + { + File.Delete(destinationFilepath); + } + File.Copy(sourceFilepath, destinationFilepath); + string fileGUID = AddFileToProject(project, destinationFilepath, filename, extensionDisplayName); + project.AddFileToBuildSection(extensionGuid, buildPhase, fileGUID); + } + + static string GetPathToSourceDirectory() + { + return Path.Combine("Packages", "com.unity.services.push-notifications", "Editor", "iOS", "NotificationServiceFiles"); + } + + static string AddFileToProject(PBXProject project, string filepath, string filename, string extensionDisplayName) + { + return project.AddFile(filepath, extensionDisplayName + "/" + filename); + } + } +} +#endif \ No newline at end of file diff --git a/Editor/iOS/IOSRichPushNotificationPostProcess.cs.meta b/Editor/iOS/IOSRichPushNotificationPostProcess.cs.meta new file mode 100644 index 0000000..be25f28 --- /dev/null +++ b/Editor/iOS/IOSRichPushNotificationPostProcess.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 27296c2eb3a254108ac21ff377673ca5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/iOS/NotificationServiceFiles.meta b/Editor/iOS/NotificationServiceFiles.meta new file mode 100644 index 0000000..0bc3bac --- /dev/null +++ b/Editor/iOS/NotificationServiceFiles.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a38694436ca96441bbfa7e9f53384e4a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/iOS/NotificationServiceFiles/Info.plist b/Editor/iOS/NotificationServiceFiles/Info.plist new file mode 100644 index 0000000..51d5f43 --- /dev/null +++ b/Editor/iOS/NotificationServiceFiles/Info.plist @@ -0,0 +1,31 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + NotificationService + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + NotificationService + + + diff --git a/Editor/iOS/NotificationServiceFiles/Info.plist.meta b/Editor/iOS/NotificationServiceFiles/Info.plist.meta new file mode 100644 index 0000000..246425b --- /dev/null +++ b/Editor/iOS/NotificationServiceFiles/Info.plist.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: bed33b130269546068035422e6a0b7ed +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/iOS/NotificationServiceFiles/NotificationService.h b/Editor/iOS/NotificationServiceFiles/NotificationService.h new file mode 100644 index 0000000..32d6298 --- /dev/null +++ b/Editor/iOS/NotificationServiceFiles/NotificationService.h @@ -0,0 +1,5 @@ +#import + +@interface NotificationService : UNNotificationServiceExtension + +@end diff --git a/Editor/iOS/NotificationServiceFiles/NotificationService.h.meta b/Editor/iOS/NotificationServiceFiles/NotificationService.h.meta new file mode 100644 index 0000000..1d04ccb --- /dev/null +++ b/Editor/iOS/NotificationServiceFiles/NotificationService.h.meta @@ -0,0 +1,27 @@ +fileFormatVersion: 2 +guid: acba2ee97d19c49beba3a5de77e977fd +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/iOS/NotificationServiceFiles/NotificationService.m b/Editor/iOS/NotificationServiceFiles/NotificationService.m new file mode 100644 index 0000000..7b2b80e --- /dev/null +++ b/Editor/iOS/NotificationServiceFiles/NotificationService.m @@ -0,0 +1,67 @@ +#import "NotificationService.h" + +@interface NotificationService () + +@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver); +@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent; + +@end + +@implementation NotificationService + +- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { + /* + Store the iOS notification content handler, and the best attempt we have at notification content, + so we can then return the content for the notification to iOS either when we have finished processing + it, or when the system is about to expire our notification extension's alloted time to handle the + notification (e.g. if the network times out while fetching an attachment, for example). + */ + self.contentHandler = contentHandler; + self.bestAttemptContent = [request.content mutableCopy]; + + NSDictionary *userInfo = [request.content.userInfo objectForKey:@"aps"]; + NSString *imageUrl = [userInfo objectForKey:@"imageUrl"]; + + if (imageUrl == nil) { + // If we have no image, then send a plain text notification with a header and text content + // as provided by the system by default. + [self contentComplete]; + return; + } + + NSString *fileExt = [@"." stringByAppendingString:[imageUrl pathExtension]]; + NSURL *attachmentUrl = [NSURL URLWithString:imageUrl]; + + NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; + [[session downloadTaskWithURL:attachmentUrl + completionHandler:^(NSURL *temporaryFileLocation, NSURLResponse *response, NSError *error) { + UNNotificationAttachment *attachment = nil; + if (error != nil) { + NSLog(@"Failed to retrieve attachment contents for notification: %@", error.localizedDescription); + } else { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *localUrl = [NSURL fileURLWithPath:[temporaryFileLocation.path stringByAppendingString:fileExt]]; + [fileManager moveItemAtURL:temporaryFileLocation toURL:localUrl error:&error]; + + NSError *attachmentError = nil; + attachment = [UNNotificationAttachment attachmentWithIdentifier:@"" URL:localUrl options:nil error:&attachmentError]; + if (attachmentError) { + NSLog(@"Failed to create attachment from attachment contents for notification: %@", attachmentError.localizedDescription); + } + } + if (attachment) { + self.bestAttemptContent.attachments = [NSArray arrayWithObject:attachment]; + } + [self contentComplete]; + }] resume]; +} + +- (void)contentComplete { + self.contentHandler(self.bestAttemptContent); +} + +- (void)serviceExtensionTimeWillExpire { + [self contentComplete]; +} + +@end diff --git a/Editor/iOS/NotificationServiceFiles/NotificationService.m.meta b/Editor/iOS/NotificationServiceFiles/NotificationService.m.meta new file mode 100644 index 0000000..f04dcf1 --- /dev/null +++ b/Editor/iOS/NotificationServiceFiles/NotificationService.m.meta @@ -0,0 +1,27 @@ +fileFormatVersion: 2 +guid: 8c94e45475e9641759e072a72cd7cc43 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e03c26b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,5 @@ +Push Notifications copyright © 2021 Unity Technologies SF + +This software is subject to, and made available under, the terms of services for Push Notifications (see https://unity3d.com/legal/one-operate-services-terms-of-service ). + +Unless expressly provided otherwise, the software under this license is made available strictly on an "AS IS" BASIS WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Please review the terms of service for details on these and other terms and conditions." diff --git a/LICENSE.md.meta b/LICENSE.md.meta new file mode 100644 index 0000000..c11f115 --- /dev/null +++ b/LICENSE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 10639e3c0c9f945b2a3068bae4333410 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Plugins.meta b/Plugins.meta new file mode 100644 index 0000000..7bcacbe --- /dev/null +++ b/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ec594a51f338d4bfeb034f417a852ee3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Plugins/Android.meta b/Plugins/Android.meta new file mode 100644 index 0000000..2906889 --- /dev/null +++ b/Plugins/Android.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7a0ad973bdff248e08c61b101e65931a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Plugins/Android/PushNotificationsAndroidLib-release.aar b/Plugins/Android/PushNotificationsAndroidLib-release.aar new file mode 100644 index 0000000..9b620cf Binary files /dev/null and b/Plugins/Android/PushNotificationsAndroidLib-release.aar differ diff --git a/Plugins/Android/PushNotificationsAndroidLib-release.aar.meta b/Plugins/Android/PushNotificationsAndroidLib-release.aar.meta new file mode 100644 index 0000000..d4c74c2 --- /dev/null +++ b/Plugins/Android/PushNotificationsAndroidLib-release.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 23ee097da219644c5975f9a13e3fbec9 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Plugins/iOS.meta b/Plugins/iOS.meta new file mode 100644 index 0000000..693352e --- /dev/null +++ b/Plugins/iOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cf662c0d28635416ea5a220d519b8de0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Plugins/iOS/PushNotificationManager.h b/Plugins/iOS/PushNotificationManager.h new file mode 100644 index 0000000..49108dd --- /dev/null +++ b/Plugins/iOS/PushNotificationManager.h @@ -0,0 +1,25 @@ +#ifndef PushNotificationManager_h +#define PushNotificationManager_h + +#import "UnityAppController.h" +#import + +// Required for internal messaging constants. +#include "Classes/PluginBase/LifeCycleListener.h" +#include "Classes/PluginBase/AppDelegateListener.h" + +typedef void (REGISTRATION_CALLBACK)(char *token); +typedef void (NOTIFICATION_CALLBACK)(char *serialisedUserInfo); + +@interface PushNotificationManager : NSObject + +@property NSString *deviceToken; +@property (nonatomic) REGISTRATION_CALLBACK *registrationCallback; +@property (nonatomic) NOTIFICATION_CALLBACK *notificationCallback; + ++ (instancetype)sharedInstance; +- (void) registerForRemoteNotifications : (REGISTRATION_CALLBACK) callback; + +@end + +#endif diff --git a/Plugins/iOS/PushNotificationManager.h.meta b/Plugins/iOS/PushNotificationManager.h.meta new file mode 100644 index 0000000..28eb743 --- /dev/null +++ b/Plugins/iOS/PushNotificationManager.h.meta @@ -0,0 +1,80 @@ +fileFormatVersion: 2 +guid: 46294242127bd47e493a651900fee6cb +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 0 + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: UserNotifications; + userData: + assetBundleName: + assetBundleVariant: diff --git a/Plugins/iOS/PushNotificationManager.mm b/Plugins/iOS/PushNotificationManager.mm new file mode 100644 index 0000000..6561f5e --- /dev/null +++ b/Plugins/iOS/PushNotificationManager.mm @@ -0,0 +1,136 @@ +#import +#import +#import + +#import "PushNotificationManager.h" + +@implementation PushNotificationManager : NSObject + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [PushNotificationManager sharedInstance]; + + UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; + center.delegate = [PushNotificationManager sharedInstance]; + }); +} + +// MARK: Interface implementations + ++ (instancetype)sharedInstance +{ + static PushNotificationManager *sharedInstance = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[PushNotificationManager alloc] init]; + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + + [nc addObserver:sharedInstance + selector:@selector(didRegisterForRemoteNotifications:) + name:kUnityDidRegisterForRemoteNotificationsWithDeviceToken + object:nil]; + + [nc addObserver:sharedInstance + selector:@selector(didFailToRegisterForRemoteNotifications:) + name:kUnityDidFailToRegisterForRemoteNotificationsWithError + object:nil]; + + [nc addObserver:sharedInstance + selector:@selector(didReceiveNotification:) + name:kUnityDidReceiveRemoteNotification + object:nil]; + }); + + return sharedInstance; +} + +/// Called externally to register for push notifications. Initally requests notification permissions, +/// then registers to get a device token, which will be handled by one of the notification handlers. +- (void) registerForRemoteNotifications : (REGISTRATION_CALLBACK) callback +{ + // If we've already registered for a token previously, and have one ready, then just return it without + // requesting authorisation again. + if (_deviceToken != NULL && _deviceToken.length > 0) { + callback((char *)_deviceToken.UTF8String); + return; + } + + _registrationCallback = callback; + + // Register for the permissions to display notifications from the iOS system. Without this, we'd receive + // the pushes but wouldn't be able to display them to a user. + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + NSInteger authOptions = (UNAuthorizationOptionSound + UNAuthorizationOptionAlert + UNAuthorizationOptionBadge); + [center requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError *error) { + if (granted) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[UIApplication sharedApplication] registerForRemoteNotifications]; + }); + } + }]; +} + +// MARK: Delegate methods + +- (void) userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler +{ + NSDictionary *userInfo = notification.request.content.userInfo; + [self handleReceivedNotificationUserInfo:userInfo]; + completionHandler((UNAuthorizationOptionSound + UNAuthorizationOptionAlert + UNAuthorizationOptionBadge)); +} + +// MARK: Internal methods + +- (void) didRegisterForRemoteNotifications: (NSNotification *) notification +{ + if ([notification.userInfo isKindOfClass:[NSData class]]) + { + NSString *token = [[PushNotificationManager sharedInstance] getDeviceTokenFromNSData: (NSData*)notification.userInfo]; + [PushNotificationManager sharedInstance].deviceToken = token; + [PushNotificationManager sharedInstance].registrationCallback((char *)token.UTF8String); + } +} + +- (void) didFailToRegisterForRemoteNotifications: (NSNotification *) notification +{ + NSLog(@"Failed to register for push notifications"); +} + +- (void) didReceiveNotification: (NSNotification *) notification +{ + NSDictionary *userInfoDictionary = notification.userInfo; + [self handleReceivedNotificationUserInfo:userInfoDictionary]; +} + +- (void) handleReceivedNotificationUserInfo: (NSDictionary *)userInfoDictionary +{ + NSError *error = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:userInfoDictionary options:kNilOptions error:&error]; + + if (error != nil) + { + NSLog(@"Failed to serialise notification user info to string. User info was: %@", userInfoDictionary); + return; + } + + char *jsonString = (char *) [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] UTF8String]; + + [PushNotificationManager sharedInstance].notificationCallback(jsonString); +} + +// MARK: Helper methods + +- (NSString *)getDeviceTokenFromNSData:(NSData *)deviceTokenData { + const char *data = (const char *)[deviceTokenData bytes]; + NSMutableString *token = [NSMutableString string]; + + for (NSUInteger i = 0; i < [deviceTokenData length]; i++) { + [token appendFormat:@"%02.2hhX", data[i]]; + } + + return [token copy]; +} + +@end diff --git a/Plugins/iOS/PushNotificationManager.mm.meta b/Plugins/iOS/PushNotificationManager.mm.meta new file mode 100644 index 0000000..a9c5d72 --- /dev/null +++ b/Plugins/iOS/PushNotificationManager.mm.meta @@ -0,0 +1,85 @@ +fileFormatVersion: 2 +guid: da538eb90ba2d4ee5b13504644879380 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 0 + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: x86 + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: x86_64 + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: UserNotifications; + - first: + tvOS: tvOS + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Plugins/iOS/PushNotificationManagerWrapper.h b/Plugins/iOS/PushNotificationManagerWrapper.h new file mode 100644 index 0000000..006d19d --- /dev/null +++ b/Plugins/iOS/PushNotificationManagerWrapper.h @@ -0,0 +1,9 @@ +#ifndef PushNotificationManagerWrapper_h +#define PushNotificationManagerWrapper_h + +#import "PushNotificationManager.h" + +extern "C" void NativeRegisterForPushNotifications(REGISTRATION_CALLBACK callback); +extern "C" void RegisterUnityCallbackForNotificationReceived(NOTIFICATION_CALLBACK callback); + +#endif diff --git a/Plugins/iOS/PushNotificationManagerWrapper.h.meta b/Plugins/iOS/PushNotificationManagerWrapper.h.meta new file mode 100644 index 0000000..a389527 --- /dev/null +++ b/Plugins/iOS/PushNotificationManagerWrapper.h.meta @@ -0,0 +1,80 @@ +fileFormatVersion: 2 +guid: 8ff38dbd0933b433abe19d794a877c63 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 0 + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: UserNotifications; + userData: + assetBundleName: + assetBundleVariant: diff --git a/Plugins/iOS/PushNotificationManagerWrapper.mm b/Plugins/iOS/PushNotificationManagerWrapper.mm new file mode 100644 index 0000000..62fe42b --- /dev/null +++ b/Plugins/iOS/PushNotificationManagerWrapper.mm @@ -0,0 +1,11 @@ +#import "PushNotificationManagerWrapper.h" + +extern "C" { + void NativeRegisterForPushNotifications(REGISTRATION_CALLBACK callback) { + [[PushNotificationManager sharedInstance] registerForRemoteNotifications:callback]; + } + + void RegisterUnityCallbackForNotificationReceived(NOTIFICATION_CALLBACK callback) { + [[PushNotificationManager sharedInstance] setNotificationCallback:callback]; + } +} diff --git a/Plugins/iOS/PushNotificationManagerWrapper.mm.meta b/Plugins/iOS/PushNotificationManagerWrapper.mm.meta new file mode 100644 index 0000000..64838ba --- /dev/null +++ b/Plugins/iOS/PushNotificationManagerWrapper.mm.meta @@ -0,0 +1,85 @@ +fileFormatVersion: 2 +guid: ed9aa836168124bf1920a23f63d6935d +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 0 + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: x86 + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: x86_64 + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: UserNotifications; + - first: + tvOS: tvOS + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md new file mode 100644 index 0000000..89dd652 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Push Notifications SDK + +> Note: This product is in closed beta, for access to the service please contact us. + +This package adds support for push notifications to your project, allowing you to send push notification campaigns to your users. \ No newline at end of file diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..4cf83ae --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8118b506cc77b41b2979cf31b597add7 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..206ac56 --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a9fc421f96a3643f4be2bf91268af9c6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Analytics.meta b/Runtime/Analytics.meta new file mode 100644 index 0000000..a499fdf --- /dev/null +++ b/Runtime/Analytics.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 975149072fc040b0bc9f3b1d126f024f +timeCreated: 1627290248 \ No newline at end of file diff --git a/Runtime/Analytics/EventsWrapper.cs b/Runtime/Analytics/EventsWrapper.cs new file mode 100644 index 0000000..9ae47ec --- /dev/null +++ b/Runtime/Analytics/EventsWrapper.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Unity.Services.Analytics; +using Unity.Services.Analytics.Internal; + +namespace Unity.Services.PushNotifications +{ + interface IPushNotificationEventsWrapper + { + void RecordCustomEvent(string eventName, Dictionary parameters, int version); + } + + class EventsWrapper: IPushNotificationEventsWrapper + { + public void RecordCustomEvent(string eventName, Dictionary parameters, int version) + { + Event evt = new Event(eventName, version); + + foreach (KeyValuePair parameter in parameters) + { + evt.Parameters.Set(parameter.Key, parameter.Value); + } + + Events.Buffer.PushEvent(evt); + } + } +} diff --git a/Runtime/Analytics/EventsWrapper.cs.meta b/Runtime/Analytics/EventsWrapper.cs.meta new file mode 100644 index 0000000..103c2f6 --- /dev/null +++ b/Runtime/Analytics/EventsWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0673bcedb71414fd388cdf2c8440b377 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Analytics/PushNotificationAnalytics.cs b/Runtime/Analytics/PushNotificationAnalytics.cs new file mode 100644 index 0000000..5201faf --- /dev/null +++ b/Runtime/Analytics/PushNotificationAnalytics.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Unity.Services.PushNotifications +{ + static class SdkVersion + { + // This value should always match what is in the package.json for this package + public static readonly string SDK_VERSION = "1.0.0-pre.2"; + } + + interface IPushNotificationsAnalytics + { + void RecordPushTokenUpdated(string pushToken); + void RecordNotificationOpened(Dictionary payload, bool didLaunch); + } + + public class PushNotificationAnalytics: IPushNotificationsAnalytics + { + IPushNotificationEventsWrapper m_EventsWrapper; + IPushNotificationAnalyticsPlatformWrapper m_AnalyticsPlatformWrapper; + + internal PushNotificationAnalytics(IPushNotificationEventsWrapper eventsWrapper, IPushNotificationAnalyticsPlatformWrapper analyticsPlatformWrapper) + { + m_EventsWrapper = eventsWrapper; + m_AnalyticsPlatformWrapper = analyticsPlatformWrapper; + } + + /// + /// This method should be called when the user's push notification token is updated. Most commonly this is + /// when first registering for notifications, but can also occur at other points during the app lifecycle. + /// If using one of this SDK's platform specific implementations this method will be called for you. + /// + /// The push token relating to this user's device. + /// + public void RecordPushTokenUpdated(string pushToken) + { + Dictionary eventData = new Dictionary + { + { "clientVersion", m_AnalyticsPlatformWrapper.ApplicationVersion() }, + { "sdkVersion", SdkVersion.SDK_VERSION }, + { "sdkMethod", "com.unity.services.pushNotifications.PushNotificationsAnalytics.RecordPushTokenUpdated" }, + { "platform", m_AnalyticsPlatformWrapper.AnalyticsPlatform() }, + { "userCountry", m_AnalyticsPlatformWrapper.UserCountry() } + }; + + RuntimePlatform runtimePlatform = m_AnalyticsPlatformWrapper.RuntimePlatform(); + if (runtimePlatform == RuntimePlatform.Android) + { + eventData.Add("androidRegistrationID", pushToken); + } + else if (runtimePlatform == RuntimePlatform.IPhonePlayer || runtimePlatform == RuntimePlatform.tvOS) + { + eventData.Add("pushNotificationToken", pushToken); + } + + m_EventsWrapper.RecordCustomEvent("notificationServices", eventData, 1); + } + + /// + /// This method should be called when the user opens the app from a push notification. + /// If using one of this SDK's platform specific implementations this method will be called for you. + /// + /// The dictionary containing required the event data. + /// Was the app launched from opening the notification? + /// + public void RecordNotificationOpened(Dictionary payload, bool didLaunch) + { + Dictionary eventParams = new Dictionary + { + { "clientVersion", m_AnalyticsPlatformWrapper.ApplicationVersion() }, + { "sdkVersion", SdkVersion.SDK_VERSION }, + { "sdkMethod", "com.unity.services.pushNotifications.PushNotificationsAnalytics.RecordNotificationOpened" }, + { "platform", m_AnalyticsPlatformWrapper.AnalyticsPlatform() }, + { "userCountry", m_AnalyticsPlatformWrapper.UserCountry() } + }; + + bool insertCommunicationAttrs = false; + + if (payload.ContainsKey("_ddCampaign")) { + eventParams["campaignId"] = Convert.ToInt64(payload["_ddCampaign"]); + insertCommunicationAttrs = true; + } + if (payload.ContainsKey("_ddCohort")) { + eventParams["cohortId"] = Convert.ToInt64(payload["_ddCohort"]); + insertCommunicationAttrs = true; + } + if (insertCommunicationAttrs && payload.ContainsKey("_ddCommunicationSender")) { + eventParams["communicationSender"] = payload["_ddCommunicationSender"]; + eventParams["communicationState"] = "OPEN"; + } + + if (didLaunch) { + eventParams["notificationLaunch"] = true; + } + if (payload.ContainsKey("_ddId")) + { + eventParams["notificationId"] = Convert.ToInt64(payload["_ddId"]); + } + if (payload.ContainsKey("_ddName")) { + eventParams["notificationName"] = payload["_ddName"]; + } + eventParams["communicationState"] = "OPEN"; + + m_EventsWrapper.RecordCustomEvent("notificationOpened", eventParams, 1); + } + } +} \ No newline at end of file diff --git a/Runtime/Analytics/PushNotificationAnalytics.cs.meta b/Runtime/Analytics/PushNotificationAnalytics.cs.meta new file mode 100644 index 0000000..5298f88 --- /dev/null +++ b/Runtime/Analytics/PushNotificationAnalytics.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9c09e709b9ead47f3a04e1a40cffb0cb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Analytics/PushNotificationsAnalyticsPlatformWrapper.cs b/Runtime/Analytics/PushNotificationsAnalyticsPlatformWrapper.cs new file mode 100644 index 0000000..8542494 --- /dev/null +++ b/Runtime/Analytics/PushNotificationsAnalyticsPlatformWrapper.cs @@ -0,0 +1,47 @@ +using UnityEngine; + +namespace Unity.Services.PushNotifications +{ + interface IPushNotificationAnalyticsPlatformWrapper + { + string ApplicationVersion(); + RuntimePlatform RuntimePlatform(); + bool IsApplicationFocused(); + string AnalyticsPlatform(); + string UserCountry(); + } + + class PushNotificationsAnalyticsPlatformWrapper: IPushNotificationAnalyticsPlatformWrapper + { + public string ApplicationVersion() + { + return Application.version; + } + + public RuntimePlatform RuntimePlatform() + { + return Application.platform; + } + + public bool IsApplicationFocused() + { + return Application.isFocused; + } + + public string AnalyticsPlatform() + { +#if UNITY_IOS + return "IOS"; +#elif UNITY_ANDROID + return "ANDROID"; +#else + return "UNKNOWN"; +#endif + } + + public string UserCountry() + { + return "GB"; + } + } +} diff --git a/Runtime/Analytics/PushNotificationsAnalyticsPlatformWrapper.cs.meta b/Runtime/Analytics/PushNotificationsAnalyticsPlatformWrapper.cs.meta new file mode 100644 index 0000000..4815bbf --- /dev/null +++ b/Runtime/Analytics/PushNotificationsAnalyticsPlatformWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e98be168a391e497b99bc795be68b8fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Android.meta b/Runtime/Android.meta new file mode 100644 index 0000000..afe33c9 --- /dev/null +++ b/Runtime/Android.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e6b00f86b74f34c6788b04515f4eb4b7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Android/AndroidPushNotifications.cs b/Runtime/Android/AndroidPushNotifications.cs new file mode 100644 index 0000000..d6bb615 --- /dev/null +++ b/Runtime/Android/AndroidPushNotifications.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using UnityEngine; + +namespace Unity.Services.PushNotifications +{ + class AndroidPushNotifications: AndroidJavaProxy + { + static object s_RegistrationLock = new object(); + static TaskCompletionSource s_DeviceRegistrationTcs; + static string s_DeviceToken; + + PushNotificationReceivedHandler m_NotificationReceivedHandler; + PushNotificationAnalytics m_NotificationAnalytics; + + public AndroidPushNotifications(PushNotificationReceivedHandler notificationReceivedHandler, PushNotificationAnalytics analytics) + : base("com.unity.services.pushnotifications.android.UnityCallbackClass") + { + m_NotificationReceivedHandler = notificationReceivedHandler; + m_NotificationAnalytics = analytics; + } + + internal event Action> InternalNotificationWasReceived; + + public Task RegisterForPushNotificationsAsync(string firebaseApiKey, string firebaseSenderId, string firebaseApplicationId, string firebaseProjectId) + { + lock (s_RegistrationLock) + { + if (!string.IsNullOrEmpty(s_DeviceToken)) + { + return Task.FromResult(s_DeviceToken); + } + + if (s_DeviceRegistrationTcs != null) + { + return s_DeviceRegistrationTcs.Task; + } + + s_DeviceRegistrationTcs = new TaskCompletionSource(); + + AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); + AndroidJavaObject activity = unityPlayer.GetStatic("currentActivity"); + AndroidJavaObject applicationContext = activity.Call("getApplicationContext"); + + AndroidJavaObject instance = GetPluginInstance(); + instance.Call("setCallbackClass", this); + instance.Call("initialize", applicationContext, + firebaseApiKey, firebaseApplicationId, firebaseSenderId, firebaseProjectId); + + return s_DeviceRegistrationTcs.Task; + } + } + + AndroidJavaObject GetPluginInstance() + { + AndroidJavaClass notificationPluginObject = new AndroidJavaClass("com.unity.services.pushnotifications.android.UnityNotifications"); + AndroidJavaObject instance = notificationPluginObject.GetStatic("INSTANCE"); + if (instance == null) + { + Debug.LogError("Unity Push Notification Android plugin is missing, android push notifications will not work as expected."); + return null; + } + + return instance; + } + + // Called from the Kotlin code + internal void OnTokenReceived(string token) + { + lock (s_RegistrationLock) + { + if (s_DeviceRegistrationTcs != null) + { + if (String.IsNullOrEmpty(token)) + { + s_DeviceRegistrationTcs.TrySetException(new Exception("Failed to register the device for remote notifications.")); + } + else + { + s_DeviceRegistrationTcs.TrySetResult(token); + } + // Reset registration flow ready for next time. + s_DeviceRegistrationTcs = null; + } + + if (!String.IsNullOrEmpty(token)) + { + s_DeviceToken = token; + m_NotificationAnalytics.RecordPushTokenUpdated(token); + Debug.Log($"Successfully registered for remote push notifications with token: {token}"); + } + } + } + + // Called from the Kotlin code + internal void OnNotificationReceived(string notificationDataAsJson) + { + if (string.IsNullOrEmpty(notificationDataAsJson)) + { + Debug.Log("Notification received with no data, ignoring"); + return; + } + + Dictionary notificationData = m_NotificationReceivedHandler.HandleReceivedNotification(notificationDataAsJson); + InternalNotificationWasReceived?.Invoke(notificationData); + } + } +} + diff --git a/Runtime/Android/AndroidPushNotifications.cs.meta b/Runtime/Android/AndroidPushNotifications.cs.meta new file mode 100644 index 0000000..72cb5ad --- /dev/null +++ b/Runtime/Android/AndroidPushNotifications.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c23a476d77870499ab0bf3f10328a2dc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/PushNotificationCoreInitialization.cs b/Runtime/PushNotificationCoreInitialization.cs new file mode 100644 index 0000000..479a829 --- /dev/null +++ b/Runtime/PushNotificationCoreInitialization.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using Unity.Services.Core.Internal; +using UnityEngine; + +namespace Unity.Services.PushNotifications +{ + public class PushNotificationCoreInitialization: IInitializablePackage + { + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + static void Register() + { + CoreRegistry.Instance.RegisterPackage(new PushNotificationCoreInitialization()); + } + + public Task Initialize(CoreRegistry registry) + { + PushNotifications.isInitialised = true; + + return Task.CompletedTask; + } + } +} diff --git a/Runtime/PushNotificationCoreInitialization.cs.meta b/Runtime/PushNotificationCoreInitialization.cs.meta new file mode 100644 index 0000000..aaab1fc --- /dev/null +++ b/Runtime/PushNotificationCoreInitialization.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 31f7f59f7d07437e93de38fca669e016 +timeCreated: 1629109337 \ No newline at end of file diff --git a/Runtime/PushNotificationReceivedHandler.cs b/Runtime/PushNotificationReceivedHandler.cs new file mode 100644 index 0000000..3cf89eb --- /dev/null +++ b/Runtime/PushNotificationReceivedHandler.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using UnityEngine; + +namespace Unity.Services.PushNotifications +{ + class PushNotificationReceivedHandler + { + readonly IPushNotificationAnalyticsPlatformWrapper m_PlatformWrapper; + readonly IPushNotificationsAnalytics m_NotificationAnalytics; + + bool m_IsCleanStart = true; + + internal PushNotificationReceivedHandler(IPushNotificationsAnalytics analytics, IPushNotificationAnalyticsPlatformWrapper platformWrapper) + { + m_NotificationAnalytics = analytics; + m_PlatformWrapper = platformWrapper; + } + + internal Dictionary HandleReceivedNotification(string jsonNotificationData) + { + try + { + Dictionary notificationData = JsonConvert.DeserializeObject>(jsonNotificationData); + if (notificationData != null) + { + // If the application is in the background, or we're opening for the first time, then we want to send the relevant analytics. + // (Note: This doesn't 100% guarantee that we're launching from an event, but it matches the previous deltaDNA behavior). + if (!m_PlatformWrapper.IsApplicationFocused() || m_IsCleanStart) + { + m_NotificationAnalytics.RecordNotificationOpened(notificationData, true); + m_IsCleanStart = false; + } + else + { + m_NotificationAnalytics.RecordNotificationOpened(notificationData, false); + } + } + + return notificationData; + } + catch (Exception e) + { + Debug.Log($"Failed to parse notification user info dictionary data: {e.Message}"); + return null; + } + } + } +} diff --git a/Runtime/PushNotificationReceivedHandler.cs.meta b/Runtime/PushNotificationReceivedHandler.cs.meta new file mode 100644 index 0000000..aa08ca1 --- /dev/null +++ b/Runtime/PushNotificationReceivedHandler.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6c5362295dee40ca9b18c3eb8afef691 +timeCreated: 1627985420 \ No newline at end of file diff --git a/Runtime/PushNotificationSettings.cs b/Runtime/PushNotificationSettings.cs new file mode 100644 index 0000000..9465cad --- /dev/null +++ b/Runtime/PushNotificationSettings.cs @@ -0,0 +1,35 @@ +namespace Unity.Services.PushNotifications +{ + /// + /// A class for configuring the Push Notifications SDK. + /// + /// Some of the fields in this class are platform specific, and are prefixed with the platform they relate to. However, it is not required to + /// use a separate settings object on each platform, as the SDK will automatically only retrieve settings for the platform it is running on. + /// + public class PushNotificationSettings + { + /// + /// The Firebase API Key for the project to use for Firebase Cloud Messaging. This should match the project you have configured + /// in the Push Notification dashboard. + /// + public string AndroidApiKey; + + /// + /// The Firebase sender ID for the project to use for Firebase Cloud Messaging. This should match the project you have configured + /// in the Push Notification dashboard. + /// + public string AndroidSenderId; + + /// + /// The Firebase application ID for the project to use for Firebase Cloud Messaging. This should match the project you have configured + /// in the Push Notification dashboard. + /// + public string AndroidApplicationId; + + /// + /// The Firebase project ID for the project to use for Firebase Cloud Messaging. This should match the project you have configured + /// in the Push Notification dashboard. + /// + public string AndroidProjectId; + } +} diff --git a/Runtime/PushNotificationSettings.cs.meta b/Runtime/PushNotificationSettings.cs.meta new file mode 100644 index 0000000..d738ad0 --- /dev/null +++ b/Runtime/PushNotificationSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 63f1f23061eb94677b4c099331c06f0b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/PushNotifications.cs b/Runtime/PushNotifications.cs new file mode 100644 index 0000000..d3be6a6 --- /dev/null +++ b/Runtime/PushNotifications.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using UnityEngine; + +[assembly: InternalsVisibleTo("Unity.Services.PushNotifications.Tests")] + +namespace Unity.Services.PushNotifications +{ + public class PushNotifications + { + static PushNotificationsAnalyticsPlatformWrapper s_AnalyticsPlatformWrapper = new PushNotificationsAnalyticsPlatformWrapper(); + static PushNotificationAnalytics s_PushNotificationAnalyticsImpl = new PushNotificationAnalytics(new EventsWrapper(), s_AnalyticsPlatformWrapper); + internal static PushNotificationReceivedHandler notificationReceivedHandler = new PushNotificationReceivedHandler(s_PushNotificationAnalyticsImpl, s_AnalyticsPlatformWrapper); + + internal static bool isInitialised = false; + +#if UNITY_IOS + static IOSPushNotifications s_IOSPushNotifications = new IOSPushNotifications(); +#elif UNITY_ANDROID + static AndroidPushNotifications s_AndroidPushNotifications = new AndroidPushNotifications(notificationReceivedHandler, s_PushNotificationAnalyticsImpl); +#endif + + public static event Action> OnNotificationReceived + { +#if UNITY_IOS + add => IOSPushNotifications.InternalNotificationWasReceived += value; + remove => IOSPushNotifications.InternalNotificationWasReceived -= value; +#elif UNITY_ANDROID + add => s_AndroidPushNotifications.InternalNotificationWasReceived += value; + remove => s_AndroidPushNotifications.InternalNotificationWasReceived -= value; +#else + add + { /* No action on unsupported platforms */ } + remove + { /* No action on unsupported platforms */ } +#endif + } + + public static PushNotificationAnalytics Analytics + { + get + { + if (!isInitialised) + { + throw new Exception("Unity services core hasn't been initialised - please make sure you've called `await UnityServices.InitializeAsync()` before using the Push Notifications SDK."); + } + return s_PushNotificationAnalyticsImpl; + } + } + + /// + /// Registers for push notifications with the appropriate mechanism for the current platform. + /// + /// This method will automatically handle platform specific intricacies of getting a push notification token, and will + /// send the appropriate analytics events to Unity Analytics 2. + /// + /// A PushNotificationSettings object with the settings for the SDK. See the documentation on that class for more information. + /// (Asynchronously via a Task) The device token as a string. + public static Task RegisterForPushNotificationsAsync(PushNotificationSettings settings) + { + if (!isInitialised) + { + throw new Exception("Unity services core hasn't been initialised - please make sure you've called `await UnityServices.InitializeAsync()` before using the Push Notifications SDK."); + } +#if UNITY_IOS + return s_IOSPushNotifications.RegisterForPushNotificationsAsync(); +#elif UNITY_ANDROID + return s_AndroidPushNotifications.RegisterForPushNotificationsAsync(settings.AndroidApiKey, settings.AndroidSenderId, settings.AndroidApplicationId, settings.AndroidProjectId); +#else + Debug.Log("Push notifications are not supported on this platform at this time, returning an empty push token"); + return Task.FromResult(""); +#endif + } + } +} diff --git a/Runtime/PushNotifications.cs.meta b/Runtime/PushNotifications.cs.meta new file mode 100644 index 0000000..481841d --- /dev/null +++ b/Runtime/PushNotifications.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 27271d43466fa482089a2e976c231504 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Unity.Services.PushNotifications.asmdef b/Runtime/Unity.Services.PushNotifications.asmdef new file mode 100644 index 0000000..b4ed3ee --- /dev/null +++ b/Runtime/Unity.Services.PushNotifications.asmdef @@ -0,0 +1,18 @@ +{ + "name": "Unity.Services.PushNotifications", + "references": [ + "Unity.Services.Analytics", + "Unity.Services.AnalyticsRuntime", + "Unity.Services.Core", + "Unity.Services.Core.Internal" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Unity.Services.PushNotifications.asmdef.meta b/Runtime/Unity.Services.PushNotifications.asmdef.meta new file mode 100644 index 0000000..74589b4 --- /dev/null +++ b/Runtime/Unity.Services.PushNotifications.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 75dec5bced1f541bcb6d18b71e4d2039 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/iOS.meta b/Runtime/iOS.meta new file mode 100644 index 0000000..4e55f7f --- /dev/null +++ b/Runtime/iOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 85c5045330d5a4e7c99b4fbc6e73ca06 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/iOS/iOSPushNotifications.cs b/Runtime/iOS/iOSPushNotifications.cs new file mode 100644 index 0000000..8b3475c --- /dev/null +++ b/Runtime/iOS/iOSPushNotifications.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using UnityEngine; + +namespace Unity.Services.PushNotifications +{ + class IOSPushNotifications + { + static object s_RegistrationLock = new object(); + static TaskCompletionSource s_DeviceRegistrationTcs; + static string s_DeviceToken; + + internal static event Action> InternalNotificationWasReceived; + + delegate void NotificationRegistrationCallback(string deviceToken); + + delegate void NotificationReceivedCallback(string serialisedNotificationData); + +#if UNITY_IOS && !UNITY_EDITOR + [DllImport("__Internal")] + static extern void NativeRegisterForPushNotifications(NotificationRegistrationCallback callback); + + [DllImport("__Internal")] + static extern void RegisterUnityCallbackForNotificationReceived(NotificationReceivedCallback callback); + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + internal static void PerformLaunchActions() + { + RegisterUnityCallbackForNotificationReceived(NotificationReceived); + } +#endif + + /// + /// Registers for push notifications on iOS. Returns the device token for the registered device. + /// + /// The push notification token for this device + public Task RegisterForPushNotificationsAsync() + { + #if UNITY_IOS && !UNITY_EDITOR + lock (s_RegistrationLock) + { + if (!String.IsNullOrEmpty(s_DeviceToken)) + { + return Task.FromResult(s_DeviceToken); + } + + if (s_DeviceRegistrationTcs != null) + { + return s_DeviceRegistrationTcs.Task; + } + + s_DeviceRegistrationTcs = new TaskCompletionSource(); + + NativeRegisterForPushNotifications(NotificationRegistrationTokenReceived); + + return s_DeviceRegistrationTcs.Task; + } + #else + Debug.Log("iOS notification support is only available in iOS builds"); + return Task.FromResult(null); + #endif + } + + [AOT.MonoPInvokeCallback(typeof(NotificationRegistrationCallback))] + internal static void NotificationRegistrationTokenReceived(string token) + { + lock (s_RegistrationLock) + { + if (String.IsNullOrEmpty(token)) + { + s_DeviceRegistrationTcs.TrySetException(new Exception("Failed to register the device for remote notifications.")); + } + else + { + s_DeviceToken = token; + s_DeviceRegistrationTcs.TrySetResult(token); + PushNotifications.Analytics.RecordPushTokenUpdated(token); + Debug.Log($"Successfully registered for remote push notifications with token: {token}"); + } + + // Reset registration flow ready for next time. + s_DeviceRegistrationTcs = null; + } + } + + [AOT.MonoPInvokeCallback(typeof(NotificationReceivedCallback))] + internal static void NotificationReceived(string serialisedNotificationData) + { + if (string.IsNullOrEmpty(serialisedNotificationData)) + { + return; + } + + Dictionary userInfo = PushNotifications.notificationReceivedHandler.HandleReceivedNotification(serialisedNotificationData); + InternalNotificationWasReceived?.Invoke(userInfo); + } + } +} diff --git a/Runtime/iOS/iOSPushNotifications.cs.meta b/Runtime/iOS/iOSPushNotifications.cs.meta new file mode 100644 index 0000000..a5eabc7 --- /dev/null +++ b/Runtime/iOS/iOSPushNotifications.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 693caa443e83f4692beb184ebda551e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/Example.meta b/Samples~/Example.meta new file mode 100644 index 0000000..104c957 --- /dev/null +++ b/Samples~/Example.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 639933add0c4742d2b44d6b25e501447 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/Example/.sample.json b/Samples~/Example/.sample.json new file mode 100644 index 0000000..78af700 --- /dev/null +++ b/Samples~/Example/.sample.json @@ -0,0 +1,5 @@ +{ + "displayName": "Push Notifications Sample", + "description": "This basic sample registers for push notifications on each supported platform, and logs when notifications are received.", + "createSeparatePackage": false +} diff --git a/Samples~/Example/PushNotificationExample.cs b/Samples~/Example/PushNotificationExample.cs new file mode 100644 index 0000000..b86697a --- /dev/null +++ b/Samples~/Example/PushNotificationExample.cs @@ -0,0 +1,30 @@ +using System; +using Unity.Services.Core; +using Unity.Services.PushNotifications; +using UnityEngine; + +public class PushNotificationExample : MonoBehaviour +{ + // Start is called before the first frame update + async void Start() + { + await UnityServices.InitializeAsync(); + + // Update the below settings to match a project you use. + PushNotificationSettings settings = new PushNotificationSettings() + { + AndroidApiKey = "API_KEY", + AndroidSenderId = "SENDER_ID", + AndroidApplicationId = "APPLICATION_ID", + AndroidProjectId = "PROJECT_ID" + }; + + string token = await PushNotifications.RegisterForPushNotificationsAsync(settings); + Debug.Log($"The push notification token is {token}"); + + PushNotifications.OnNotificationReceived += notificationData => + { + Debug.Log("Data retrieved!"); + }; + } +} diff --git a/Samples~/Example/PushNotificationExample.cs.meta b/Samples~/Example/PushNotificationExample.cs.meta new file mode 100644 index 0000000..21cb611 --- /dev/null +++ b/Samples~/Example/PushNotificationExample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 41136186e350545ada94ee9cf08c7b72 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests.meta b/Tests.meta new file mode 100644 index 0000000..9464976 --- /dev/null +++ b/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6b493f18ff8804437b09c4c8e754590a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/.tests.json b/Tests/.tests.json new file mode 100644 index 0000000..605133e --- /dev/null +++ b/Tests/.tests.json @@ -0,0 +1,3 @@ +{ + "createSeparatePackage": false +} diff --git a/Tests/Runtime.meta b/Tests/Runtime.meta new file mode 100644 index 0000000..6b6fa69 --- /dev/null +++ b/Tests/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 839aa2a89461b4c4ea90d0dfe3ecf785 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/MockPlatformWrapper.cs b/Tests/Runtime/MockPlatformWrapper.cs new file mode 100644 index 0000000..9ae1dc6 --- /dev/null +++ b/Tests/Runtime/MockPlatformWrapper.cs @@ -0,0 +1,38 @@ +using UnityEngine; + +namespace Unity.Services.PushNotifications.Tests +{ + class MockPlatformWrapper: IPushNotificationAnalyticsPlatformWrapper + { + public const string mockApplicationVersion = "1.0.0"; + public RuntimePlatform mockRuntimePlatform = UnityEngine.RuntimePlatform.Android; + public const string mockAnalyticsPlatform = "IPHONE"; + public const string mockUserCountry = "UK"; + public bool mockIsApplicationFocused = true; + + public string ApplicationVersion() + { + return mockApplicationVersion; + } + + public RuntimePlatform RuntimePlatform() + { + return mockRuntimePlatform; + } + + public string AnalyticsPlatform() + { + return mockAnalyticsPlatform; + } + + public string UserCountry() + { + return mockUserCountry; + } + + public bool IsApplicationFocused() + { + return mockIsApplicationFocused; + } + } +} diff --git a/Tests/Runtime/MockPlatformWrapper.cs.meta b/Tests/Runtime/MockPlatformWrapper.cs.meta new file mode 100644 index 0000000..59f6068 --- /dev/null +++ b/Tests/Runtime/MockPlatformWrapper.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2165f1bd76f04df48df0065bb3bf2194 +timeCreated: 1628074455 \ No newline at end of file diff --git a/Tests/Runtime/PushNotificationAnalyticsTest.cs b/Tests/Runtime/PushNotificationAnalyticsTest.cs new file mode 100644 index 0000000..182a050 --- /dev/null +++ b/Tests/Runtime/PushNotificationAnalyticsTest.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using Unity.Services.PushNotifications; +using Unity.Services.PushNotifications.Tests; +using UnityEngine; + +[TestFixture] +class PushNotificationAnalyticsTests +{ + PushNotificationAnalytics m_Analytics; + MockPlatformWrapper m_MockPlatformWrapper; + EventMock m_MockEventWrapper; + + Dictionary m_NotificationUserInfoDataFull = new Dictionary + { + {"_ddCampaign", "23456"}, + {"_ddCohort", "34567"}, + {"_ddCommunicationSender", "IOS"}, + {"_ddId", "12345"}, + {"_ddName", "ddName"}, + }; + + + [SetUp] + public void SetUp() + { + m_MockEventWrapper = new EventMock(); + m_MockPlatformWrapper = new MockPlatformWrapper(); + m_Analytics = new PushNotificationAnalytics(m_MockEventWrapper, m_MockPlatformWrapper); + } + + [Test] + public void WhenPushTokenIsUpdatedAndThePlatformIsIOSRecordTheRightData() + { + string token = "myPushNotificationToken"; + m_MockPlatformWrapper.mockRuntimePlatform = RuntimePlatform.IPhonePlayer; + + m_Analytics.RecordPushTokenUpdated(token); + + Assert.AreEqual("notificationServices", m_MockEventWrapper.LastCalledEventName); + Assert.AreEqual(1, m_MockEventWrapper.LastCalledVersion); + + Dictionary calledParams = m_MockEventWrapper.LastCalledParams; + Assert.AreEqual(MockPlatformWrapper.mockApplicationVersion, calledParams["clientVersion"]); + Assert.AreEqual(MockPlatformWrapper.mockAnalyticsPlatform, calledParams["platform"]); + Assert.AreEqual(MockPlatformWrapper.mockUserCountry, calledParams["userCountry"]); + Assert.AreEqual(SdkVersion.SDK_VERSION, calledParams["sdkVersion"]); + Assert.AreEqual("com.unity.services.pushNotifications.PushNotificationsAnalytics.RecordPushTokenUpdated", calledParams["sdkMethod"]); + Assert.AreEqual(token, calledParams["pushNotificationToken"]); + } + + [Test] + public void WhenPushTokenIsUpdatedAndThePlatformIsAppleTVRecordTheRightData() + { + string token = "myPushNotificationToken"; + m_MockPlatformWrapper.mockRuntimePlatform = RuntimePlatform.tvOS; + + m_Analytics.RecordPushTokenUpdated(token); + + Assert.AreEqual("notificationServices", m_MockEventWrapper.LastCalledEventName); + Assert.AreEqual(1, m_MockEventWrapper.LastCalledVersion); + + Dictionary calledParams = m_MockEventWrapper.LastCalledParams; + Assert.AreEqual(MockPlatformWrapper.mockApplicationVersion, calledParams["clientVersion"]); + Assert.AreEqual(MockPlatformWrapper.mockAnalyticsPlatform, calledParams["platform"]); + Assert.AreEqual(MockPlatformWrapper.mockUserCountry, calledParams["userCountry"]); + Assert.AreEqual(SdkVersion.SDK_VERSION, calledParams["sdkVersion"]); + Assert.AreEqual("com.unity.services.pushNotifications.PushNotificationsAnalytics.RecordPushTokenUpdated", calledParams["sdkMethod"]); + Assert.AreEqual(token, calledParams["pushNotificationToken"]); + } + + [Test] + public void WhenPushTokenIsUpdatedAndThePlatformIsAndroidRecordTheRightData() + { + string token = "myPushNotificationToken"; + m_MockPlatformWrapper.mockRuntimePlatform = RuntimePlatform.Android; + + m_Analytics.RecordPushTokenUpdated(token); + + Assert.AreEqual("notificationServices", m_MockEventWrapper.LastCalledEventName); + Assert.AreEqual(1, m_MockEventWrapper.LastCalledVersion); + + Dictionary calledParams = m_MockEventWrapper.LastCalledParams; + Assert.AreEqual(MockPlatformWrapper.mockApplicationVersion, calledParams["clientVersion"]); + Assert.AreEqual(MockPlatformWrapper.mockAnalyticsPlatform, calledParams["platform"]); + Assert.AreEqual(MockPlatformWrapper.mockUserCountry, calledParams["userCountry"]); + Assert.AreEqual(SdkVersion.SDK_VERSION, calledParams["sdkVersion"]); + Assert.AreEqual("com.unity.services.pushNotifications.PushNotificationsAnalytics.RecordPushTokenUpdated", calledParams["sdkMethod"]); + Assert.AreEqual(token, calledParams["androidRegistrationID"]); + } + + [Test] + public void WhenNotificationOpenedIsCalledWithFullDataAndDidLaunchTheRightFieldsAreSet() + { + m_Analytics.RecordNotificationOpened(m_NotificationUserInfoDataFull, true); + + Assert.AreEqual("notificationOpened", m_MockEventWrapper.LastCalledEventName); + Assert.AreEqual(1, m_MockEventWrapper.LastCalledVersion); + + Dictionary calledParams = m_MockEventWrapper.LastCalledParams; + Assert.AreEqual(MockPlatformWrapper.mockApplicationVersion, calledParams["clientVersion"]); + Assert.AreEqual(MockPlatformWrapper.mockAnalyticsPlatform, calledParams["platform"]); + Assert.AreEqual(MockPlatformWrapper.mockUserCountry, calledParams["userCountry"]); + Assert.AreEqual(SdkVersion.SDK_VERSION, calledParams["sdkVersion"]); + Assert.AreEqual("com.unity.services.pushNotifications.PushNotificationsAnalytics.RecordNotificationOpened", calledParams["sdkMethod"]); + Assert.AreEqual(true, calledParams["notificationLaunch"]); + Assert.AreEqual("OPEN", calledParams["communicationState"]); + + Assert.AreEqual(23456, calledParams["campaignId"]); + Assert.AreEqual(34567, calledParams["cohortId"]); + Assert.AreEqual("IOS", calledParams["communicationSender"]); + Assert.AreEqual(12345, calledParams["notificationId"]); + Assert.AreEqual("ddName", calledParams["notificationName"]); + } + + [Test] + public void WhenNotificationOpenedIsCalledWithMinimalDataAndDidLaunchTheRightFieldsAreSet() + { + m_Analytics.RecordNotificationOpened(new Dictionary(), true); + + Assert.AreEqual("notificationOpened", m_MockEventWrapper.LastCalledEventName); + Assert.AreEqual(1, m_MockEventWrapper.LastCalledVersion); + + Dictionary calledParams = m_MockEventWrapper.LastCalledParams; + Assert.AreEqual(MockPlatformWrapper.mockApplicationVersion, calledParams["clientVersion"]); + Assert.AreEqual(MockPlatformWrapper.mockAnalyticsPlatform, calledParams["platform"]); + Assert.AreEqual(MockPlatformWrapper.mockUserCountry, calledParams["userCountry"]); + Assert.AreEqual(SdkVersion.SDK_VERSION, calledParams["sdkVersion"]); + Assert.AreEqual("com.unity.services.pushNotifications.PushNotificationsAnalytics.RecordNotificationOpened", calledParams["sdkMethod"]); + Assert.AreEqual(true, calledParams["notificationLaunch"]); + Assert.AreEqual("OPEN", calledParams["communicationState"]); + } + + [Test] + public void WhenNotificationOpenedIsCalledWithMinimalDataAndDidNotLaunchTheRightFieldsAreSet() + { + m_Analytics.RecordNotificationOpened(m_NotificationUserInfoDataFull, false); + + Assert.AreEqual("notificationOpened", m_MockEventWrapper.LastCalledEventName); + Assert.AreEqual(1, m_MockEventWrapper.LastCalledVersion); + + Dictionary calledParams = m_MockEventWrapper.LastCalledParams; + Assert.AreEqual(MockPlatformWrapper.mockApplicationVersion, calledParams["clientVersion"]); + Assert.AreEqual(MockPlatformWrapper.mockAnalyticsPlatform, calledParams["platform"]); + Assert.AreEqual(MockPlatformWrapper.mockUserCountry, calledParams["userCountry"]); + Assert.AreEqual(SdkVersion.SDK_VERSION, calledParams["sdkVersion"]); + Assert.AreEqual("com.unity.services.pushNotifications.PushNotificationsAnalytics.RecordNotificationOpened", calledParams["sdkMethod"]); + Assert.AreEqual(false, calledParams.ContainsValue("notificationLaunch")); + Assert.AreEqual("OPEN", calledParams["communicationState"]); + } + + class EventMock : IPushNotificationEventsWrapper + { + public string LastCalledEventName; + public Dictionary LastCalledParams; + public int LastCalledVersion; + + public void RecordCustomEvent(string eventName, Dictionary parameters, int version) + { + LastCalledParams = parameters; + LastCalledEventName = eventName; + LastCalledVersion = version; + } + } +} diff --git a/Tests/Runtime/PushNotificationAnalyticsTest.cs.meta b/Tests/Runtime/PushNotificationAnalyticsTest.cs.meta new file mode 100644 index 0000000..16aa287 --- /dev/null +++ b/Tests/Runtime/PushNotificationAnalyticsTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d5edd0e4d3ad41269fa270a0afdbc6b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/PushNotificationReceivedHandlerTest.cs b/Tests/Runtime/PushNotificationReceivedHandlerTest.cs new file mode 100644 index 0000000..e88f481 --- /dev/null +++ b/Tests/Runtime/PushNotificationReceivedHandlerTest.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using NUnit.Framework; +using Unity.Services.PushNotifications; +using Unity.Services.PushNotifications.Tests; + +[TestFixture] +public class PushNotificationReceivedHandlerTest +{ + PushNotificationReceivedHandler m_NotificationReceivedHandler; + PushNotificationsAnalyticsMock m_PushNotificationsAnalyticsMock; + MockPlatformWrapper m_AnalyticsPlatformWrapper; + + static string s_SampleJson = "{\"test\":true}"; + + [SetUp] + public void SetUp() + { + m_AnalyticsPlatformWrapper = new MockPlatformWrapper(); + m_PushNotificationsAnalyticsMock = new PushNotificationsAnalyticsMock(); + m_NotificationReceivedHandler = new PushNotificationReceivedHandler(m_PushNotificationsAnalyticsMock, m_AnalyticsPlatformWrapper); + } + + [Test] + public void WhenHandlingAReceivedNotification_AndTheDataIsMissing_NoAnalyticsDataShouldBeSent_AndEventShouldNotBeProcessed() + { + Dictionary data = m_NotificationReceivedHandler.HandleReceivedNotification(null); + + Assert.IsNull(data); + Assert.IsFalse(m_PushNotificationsAnalyticsMock.NotificationOpenedWasCalled); + } + + [Test] + public void WhenHandlingAReceivedNotification_AndTheDataIsNotValidJson_NoAnalyticsDataShouldBeSent_AndEventShouldNotBeProcessed() + { + Dictionary data = m_NotificationReceivedHandler.HandleReceivedNotification("notValidJson"); + + Assert.IsNull(data); + Assert.IsFalse(m_PushNotificationsAnalyticsMock.NotificationOpenedWasCalled); + } + + [Test] + public void WhenHandlingAReceivedNotification_AndTheDataIsValid_AndItIsACleanStart_TheCorrectAnalyticsDataShouldBeSent() + { + Dictionary data = m_NotificationReceivedHandler.HandleReceivedNotification(s_SampleJson); + + Assert.AreEqual(true, data["test"]); + Assert.AreEqual(data, m_PushNotificationsAnalyticsMock.LastUpdatedPayload); + Assert.AreEqual(true, m_PushNotificationsAnalyticsMock.LastUpdatedAppDidLaunch); + } + + [Test] + public void WhenHandlingAReceivedNotification_AndTheDataIsValid_AndItIsNotACleanStart_AndTheApplicationIsNotFocused_AsANotificationHasBeenPreviouslyProcessed_TheCorrectAnalyticsShouldBeSent() + { + m_AnalyticsPlatformWrapper.mockIsApplicationFocused = false; + m_NotificationReceivedHandler.HandleReceivedNotification(s_SampleJson); + Dictionary secondData = m_NotificationReceivedHandler.HandleReceivedNotification(s_SampleJson); + + Assert.AreEqual(true, secondData["test"]); + Assert.AreEqual(secondData, m_PushNotificationsAnalyticsMock.LastUpdatedPayload); + Assert.AreEqual(true, m_PushNotificationsAnalyticsMock.LastUpdatedAppDidLaunch); + } + + [Test] + public void WhenHandlingAReceivedNotification_AndTheDataIsValid_AndItIsNotACleanStart_AndTheApplicationIsFocused_AsANotificationHasBeenPreviouslyProcessed_TheCorrectAnalyticsShouldBeSent() + { + m_AnalyticsPlatformWrapper.mockIsApplicationFocused = true; + m_NotificationReceivedHandler.HandleReceivedNotification(s_SampleJson); + Dictionary secondData = m_NotificationReceivedHandler.HandleReceivedNotification(s_SampleJson); + + Assert.AreEqual(true, secondData["test"]); + Assert.AreEqual(secondData, m_PushNotificationsAnalyticsMock.LastUpdatedPayload); + Assert.AreEqual(false, m_PushNotificationsAnalyticsMock.LastUpdatedAppDidLaunch); + } + + class PushNotificationsAnalyticsMock : IPushNotificationsAnalytics + { + public Dictionary LastUpdatedPayload; + public bool LastUpdatedAppDidLaunch; + + public bool NotificationOpenedWasCalled; + + public void RecordPushTokenUpdated(string pushToken) + {} + + public void RecordNotificationOpened(Dictionary payload, bool didLaunch) + { + LastUpdatedPayload = payload; + LastUpdatedAppDidLaunch = didLaunch; + NotificationOpenedWasCalled = true; + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/PushNotificationReceivedHandlerTest.cs.meta b/Tests/Runtime/PushNotificationReceivedHandlerTest.cs.meta new file mode 100644 index 0000000..fa5cb99 --- /dev/null +++ b/Tests/Runtime/PushNotificationReceivedHandlerTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e528acd713d447ceb8204d0cfe72ccd3 +timeCreated: 1627986354 \ No newline at end of file diff --git a/Tests/Runtime/Unity.Services.PushNotifications.Tests.asmdef b/Tests/Runtime/Unity.Services.PushNotifications.Tests.asmdef new file mode 100644 index 0000000..ee11a84 --- /dev/null +++ b/Tests/Runtime/Unity.Services.PushNotifications.Tests.asmdef @@ -0,0 +1,11 @@ +{ + "name": "Unity.Services.PushNotifications.Tests", + "references": [ + "Unity.Services.PushNotifications" + ], + "optionalUnityReferences": [ + "TestAssemblies" + ], + "includePlatforms": [], + "excludePlatforms": [] +} diff --git a/Tests/Runtime/Unity.Services.PushNotifications.Tests.asmdef.meta b/Tests/Runtime/Unity.Services.PushNotifications.Tests.asmdef.meta new file mode 100644 index 0000000..e9514d9 --- /dev/null +++ b/Tests/Runtime/Unity.Services.PushNotifications.Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 55e5e0155ef67402c9e41de22b942f50 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Third Party Notices.md b/Third Party Notices.md new file mode 100644 index 0000000..2b24123 --- /dev/null +++ b/Third Party Notices.md @@ -0,0 +1,16 @@ +This package contains third-party software components governed by the license(s) indicated below: +--------- + +Component Name: com.unity.nuget.newtonsoft-json + +License Type: MIT + +The MIT License (MIT) + +Copyright (c) 2020 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Third Party Notices.md.meta b/Third Party Notices.md.meta new file mode 100644 index 0000000..6d12e48 --- /dev/null +++ b/Third Party Notices.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0d527c561edeb4ce1aaa65e746fa1b5e +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..f4b335c --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "com.unity.services.push-notifications", + "displayName": "Push Notifications", + "version": "1.0.0-pre.3", + "unity": "2020.3", + "description": "This package adds support for Push Notifications to your game. It allows sending rich push notifications with images, and provides analytics on the number of received push notifications.", + "dependencies": { + "com.unity.services.analytics": "2.0.7-pre.2", + "com.unity.nuget.newtonsoft-json": "2.0.0", + "com.unity.services.core": "1.1.0-pre.8" + }, + "samples": [ + { + "displayName": "Push Notifications Sample", + "description": "This basic sample registers for push notifications on each supported platform, and logs when notifications are received.", + "path": "Samples~/Example" + } + ], + "upmCi": { + "footprint": "9d6c795c8bd032072185cb9105b9e2c5af550356" + }, + "repository": { + "url": "https://github.cds.internal.unity3d.com/unity/Catapult.mono.git", + "type": "git", + "revision": "24e0850287ee78608686d6fe92bf0526a965afd4" + } +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..545b39a --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6897f4613657a4e53bf7c6a9c2aec017 +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: