@aws-amplify/rtn-push-notification 1.0.1-push-notification-dryrun.7810
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AmplifyRTNPushNotification.podspec +36 -0
- package/LICENSE +201 -0
- package/android/build.gradle +76 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/android/gradle.properties +23 -0
- package/android/gradlew +234 -0
- package/android/gradlew.bat +89 -0
- package/android/src/main/AndroidManifest.xml +22 -0
- package/android/src/main/kotlin/com/amazonaws/amplify/rtnpushnotification/PushNotificationAWSPinpointUtils.kt +159 -0
- package/android/src/main/kotlin/com/amazonaws/amplify/rtnpushnotification/PushNotificationEventManager.kt +49 -0
- package/android/src/main/kotlin/com/amazonaws/amplify/rtnpushnotification/PushNotificationFirebaseMessagingService.kt +74 -0
- package/android/src/main/kotlin/com/amazonaws/amplify/rtnpushnotification/PushNotificationHeadlessTaskService.kt +28 -0
- package/android/src/main/kotlin/com/amazonaws/amplify/rtnpushnotification/PushNotificationLaunchActivity.kt +27 -0
- package/android/src/main/kotlin/com/amazonaws/amplify/rtnpushnotification/PushNotificationModule.kt +188 -0
- package/android/src/main/kotlin/com/amazonaws/amplify/rtnpushnotification/PushNotificationPackage.kt +22 -0
- package/android/src/test/kotlin/PushNotificationAWSPinpointUtilsTest.kt +326 -0
- package/android/src/test/kotlin/PushNotificationEventManagerTest.kt +45 -0
- package/android/src/test/kotlin/PushNotificationFirebaseMessagingServiceTest.kt +92 -0
- package/android/src/test/kotlin/PushNotificationLaunchActivityTest.kt +45 -0
- package/android/src/test/kotlin/PushNotificationModuleTest.kt +201 -0
- package/ios/AmplifyPushNotification.h +17 -0
- package/ios/AmplifyPushNotification.m +21 -0
- package/ios/AmplifyPushNotificationAppDelegateHelper.swift +31 -0
- package/ios/AmplifyRTNEventManager.swift +73 -0
- package/ios/AmplifyRTNPushNotification-Bridging-Header.h +7 -0
- package/ios/AmplifyRTNPushNotification.m +26 -0
- package/ios/AmplifyRTNPushNotification.swift +121 -0
- package/ios/AmplifyRTNPushNotification.xcworkspace/contents.xcworkspacedata +28 -0
- package/ios/AmplifyRTNPushNotification.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/ios/AmplifyRTNPushNotificationManager.swift +258 -0
- package/lib/.tsbuildinfo +3 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -0
- package/lib/types.d.ts +18 -0
- package/lib/types.js +5 -0
- package/lib/types.js.map +1 -0
- package/lib-esm/.tsbuildinfo +3 -0
- package/lib-esm/index.d.ts +3 -0
- package/lib-esm/index.js +5 -0
- package/lib-esm/index.js.map +1 -0
- package/lib-esm/types.d.ts +18 -0
- package/lib-esm/types.js +3 -0
- package/lib-esm/types.js.map +1 -0
- package/package.json +48 -0
- package/src/index.ts +10 -0
- package/src/types.ts +23 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
package="com.amazonaws.amplify.rtnpushnotification">
|
|
4
|
+
|
|
5
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
6
|
+
<application>
|
|
7
|
+
<activity
|
|
8
|
+
android:name=".PushNotificationLaunchActivity"
|
|
9
|
+
android:launchMode="singleInstance"
|
|
10
|
+
android:exported="false" />
|
|
11
|
+
|
|
12
|
+
<service android:name=".PushNotificationHeadlessTaskService" />
|
|
13
|
+
|
|
14
|
+
<service
|
|
15
|
+
android:name=".PushNotificationFirebaseMessagingService"
|
|
16
|
+
android:exported="false">
|
|
17
|
+
<intent-filter>
|
|
18
|
+
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
19
|
+
</intent-filter>
|
|
20
|
+
</service>
|
|
21
|
+
</application>
|
|
22
|
+
</manifest>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
package com.amazonaws.amplify.rtnpushnotification
|
|
5
|
+
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.content.Intent
|
|
8
|
+
import android.net.Uri
|
|
9
|
+
import android.os.Bundle
|
|
10
|
+
import android.util.Log
|
|
11
|
+
import com.amplifyframework.annotations.InternalAmplifyApi
|
|
12
|
+
import com.amplifyframework.notifications.pushnotifications.NotificationContentProvider
|
|
13
|
+
import com.amplifyframework.notifications.pushnotifications.NotificationPayload
|
|
14
|
+
import com.amplifyframework.pushnotifications.pinpoint.PinpointNotificationPayload
|
|
15
|
+
import com.amplifyframework.pushnotifications.pinpoint.PushNotificationsConstants
|
|
16
|
+
import com.amplifyframework.pushnotifications.pinpoint.PushNotificationsUtils
|
|
17
|
+
import com.amplifyframework.pushnotifications.pinpoint.permissions.PermissionRequestResult
|
|
18
|
+
import com.amplifyframework.pushnotifications.pinpoint.permissions.PushNotificationPermission
|
|
19
|
+
import com.facebook.react.bridge.Arguments
|
|
20
|
+
import com.facebook.react.bridge.WritableMap
|
|
21
|
+
import com.google.firebase.messaging.RemoteMessage
|
|
22
|
+
import kotlinx.serialization.decodeFromString
|
|
23
|
+
import kotlinx.serialization.json.Json
|
|
24
|
+
import kotlin.random.Random
|
|
25
|
+
|
|
26
|
+
private const val TAG = "PushNotificationAWSPinpointUtils"
|
|
27
|
+
|
|
28
|
+
class PushNotificationPermission(context: Context) {
|
|
29
|
+
private val permission = PushNotificationPermission(context)
|
|
30
|
+
|
|
31
|
+
val hasRequiredPermission = permission.hasRequiredPermission
|
|
32
|
+
|
|
33
|
+
suspend fun requestPermission(): Boolean {
|
|
34
|
+
return permission.requestPermission() is PermissionRequestResult.Granted
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@InternalAmplifyApi
|
|
39
|
+
class PushNotificationUtils(context: Context) {
|
|
40
|
+
private val utils = PushNotificationsUtils(context)
|
|
41
|
+
|
|
42
|
+
fun showNotification(
|
|
43
|
+
payload: NotificationPayload
|
|
44
|
+
) {
|
|
45
|
+
PinpointNotificationPayload.fromNotificationPayload(payload)?.let {
|
|
46
|
+
if (utils.areNotificationsEnabled() && !it.silentPush) {
|
|
47
|
+
utils.showNotification(
|
|
48
|
+
payload.notificationId, it, PushNotificationLaunchActivity::class.java
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fun isAppInForeground(): Boolean {
|
|
55
|
+
return utils.isAppInForeground()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@InternalAmplifyApi
|
|
60
|
+
fun Bundle.getNotificationPayload(): NotificationPayload? {
|
|
61
|
+
val payload = NotificationPayload(NotificationContentProvider.FCM(RemoteMessage(this).data))
|
|
62
|
+
return PinpointNotificationPayload.fromNotificationPayload(payload)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@InternalAmplifyApi
|
|
66
|
+
fun NotificationPayload?.getProcessedIntent(
|
|
67
|
+
context: Context,
|
|
68
|
+
): Intent? {
|
|
69
|
+
// Always launch app
|
|
70
|
+
val notificationIntent: Intent? =
|
|
71
|
+
context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
72
|
+
this?.let {
|
|
73
|
+
PinpointNotificationPayload.fromNotificationPayload(it)?.action?.let { action ->
|
|
74
|
+
when {
|
|
75
|
+
// Attach action to open url
|
|
76
|
+
action[PushNotificationsConstants.URL] != null -> {
|
|
77
|
+
notificationIntent?.action = Intent.ACTION_VIEW
|
|
78
|
+
notificationIntent?.data = Uri.parse(action[PushNotificationsConstants.URL])
|
|
79
|
+
}
|
|
80
|
+
// Attach action to open deep link
|
|
81
|
+
action[PushNotificationsConstants.DEEPLINK] != null -> {
|
|
82
|
+
notificationIntent?.action = Intent.ACTION_VIEW
|
|
83
|
+
notificationIntent?.data =
|
|
84
|
+
Uri.parse(action[PushNotificationsConstants.DEEPLINK])
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return notificationIntent
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@InternalAmplifyApi
|
|
93
|
+
fun NotificationPayload.toWritableMap(): WritableMap {
|
|
94
|
+
val payload = PinpointNotificationPayload.fromNotificationPayload(this)
|
|
95
|
+
|
|
96
|
+
// Build actions map
|
|
97
|
+
val action = payload?.action?.let { action ->
|
|
98
|
+
Arguments.createMap().apply {
|
|
99
|
+
action[PushNotificationsConstants.OPENAPP]?.let {
|
|
100
|
+
putString(PushNotificationsConstants.OPENAPP, it)
|
|
101
|
+
}
|
|
102
|
+
action[PushNotificationsConstants.URL]?.let {
|
|
103
|
+
putString(PushNotificationsConstants.URL, it)
|
|
104
|
+
}
|
|
105
|
+
action[PushNotificationsConstants.DEEPLINK]?.let {
|
|
106
|
+
putString(PushNotificationsConstants.DEEPLINK, it)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Build rawData map
|
|
112
|
+
val rawData = Arguments.createMap()
|
|
113
|
+
this.rawData.entries.forEach {
|
|
114
|
+
rawData.putString(it.key, it.value)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Build and return final map
|
|
118
|
+
return Arguments.createMap().apply {
|
|
119
|
+
payload?.title?.let { putString("title", it) }
|
|
120
|
+
payload?.body?.let { putString("body", it) }
|
|
121
|
+
payload?.imageUrl?.let { putString("imageUrl", it) }
|
|
122
|
+
payload?.channelId?.let { putString("channelId", it) }
|
|
123
|
+
action?.let { putMap("action", action) }
|
|
124
|
+
putMap("rawData", rawData)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@InternalAmplifyApi
|
|
129
|
+
private val NotificationPayload.notificationId: Int
|
|
130
|
+
get() {
|
|
131
|
+
var sourceId: String?
|
|
132
|
+
var activityId: String?
|
|
133
|
+
|
|
134
|
+
// Assign campaign attributes
|
|
135
|
+
sourceId = this.rawData[PushNotificationsConstants.PINPOINT_CAMPAIGN_CAMPAIGN_ID]
|
|
136
|
+
activityId = this.rawData[PushNotificationsConstants.PINPOINT_CAMPAIGN_CAMPAIGN_ACTIVITY_ID]
|
|
137
|
+
|
|
138
|
+
// If no campaign id, try to assign journey attributes
|
|
139
|
+
if (sourceId.isNullOrEmpty()) {
|
|
140
|
+
val journeyAttributes = this.rawData[PushNotificationsConstants.PINPOINT_PREFIX]?.let {
|
|
141
|
+
try {
|
|
142
|
+
Json.decodeFromString<Map<String, Map<String, String>>>(it)[PushNotificationsConstants.JOURNEY]
|
|
143
|
+
} catch (e: Exception) {
|
|
144
|
+
Log.e(TAG, "Error parsing journey attribute", e)
|
|
145
|
+
null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
journeyAttributes?.let {
|
|
149
|
+
sourceId = it[PushNotificationsConstants.JOURNEY_ID]
|
|
150
|
+
activityId = it[PushNotificationsConstants.JOURNEY_ACTIVITY_ID]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// If no activity id (even if campaign was direct send), use a random id, otherwise hash
|
|
155
|
+
// attributes to prevent displaying duplicate notifications from an activity
|
|
156
|
+
return activityId?.let {
|
|
157
|
+
"$sourceId:$activityId".hashCode()
|
|
158
|
+
} ?: Random.nextInt()
|
|
159
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
package com.amazonaws.amplify.rtnpushnotification
|
|
5
|
+
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
7
|
+
import com.facebook.react.bridge.WritableMap
|
|
8
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
9
|
+
|
|
10
|
+
enum class PushNotificationEventType(val value: String) {
|
|
11
|
+
FOREGROUND_MESSAGE_RECEIVED("ForegroundMessageReceived"),
|
|
12
|
+
LAUNCH_NOTIFICATION_OPENED("LaunchNotificationOpened"),
|
|
13
|
+
NOTIFICATION_OPENED("NotificationOpened"),
|
|
14
|
+
TOKEN_RECEIVED("TokenReceived")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class PushNotificationEvent(val type: PushNotificationEventType, val params: WritableMap?)
|
|
18
|
+
|
|
19
|
+
object PushNotificationEventManager {
|
|
20
|
+
private lateinit var reactContext: ReactApplicationContext
|
|
21
|
+
private var isInitialized: Boolean = false
|
|
22
|
+
private val eventQueue: MutableList<PushNotificationEvent> = mutableListOf()
|
|
23
|
+
|
|
24
|
+
fun init(reactContext: ReactApplicationContext) {
|
|
25
|
+
this.reactContext = reactContext
|
|
26
|
+
isInitialized = true
|
|
27
|
+
flushEventQueue()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fun sendEvent(type: PushNotificationEventType, params: WritableMap?) {
|
|
31
|
+
if (!isInitialized) {
|
|
32
|
+
eventQueue.add(PushNotificationEvent(type, params))
|
|
33
|
+
} else {
|
|
34
|
+
sendJSEvent(type, params)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private fun sendJSEvent(type: PushNotificationEventType, params: WritableMap?) {
|
|
39
|
+
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
40
|
+
?.emit(type.value, params)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private fun flushEventQueue() {
|
|
44
|
+
eventQueue.forEach {
|
|
45
|
+
sendJSEvent(it.type, it.params)
|
|
46
|
+
}
|
|
47
|
+
eventQueue.clear()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
package com.amazonaws.amplify.rtnpushnotification
|
|
5
|
+
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.os.Bundle
|
|
8
|
+
import android.util.Log
|
|
9
|
+
import com.amplifyframework.annotations.InternalAmplifyApi
|
|
10
|
+
import com.amplifyframework.notifications.pushnotifications.NotificationPayload
|
|
11
|
+
import com.facebook.react.HeadlessJsTaskService
|
|
12
|
+
import com.facebook.react.bridge.Arguments
|
|
13
|
+
import com.google.firebase.messaging.FirebaseMessagingService
|
|
14
|
+
|
|
15
|
+
@InternalAmplifyApi
|
|
16
|
+
private val TAG = PushNotificationFirebaseMessagingService::class.java.simpleName
|
|
17
|
+
|
|
18
|
+
@InternalAmplifyApi
|
|
19
|
+
class PushNotificationFirebaseMessagingService : FirebaseMessagingService() {
|
|
20
|
+
|
|
21
|
+
private lateinit var utils: PushNotificationUtils
|
|
22
|
+
|
|
23
|
+
override fun onCreate() {
|
|
24
|
+
super.onCreate()
|
|
25
|
+
utils = PushNotificationUtils(baseContext)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun onNewToken(token: String) {
|
|
29
|
+
super.onNewToken(token)
|
|
30
|
+
val params = Arguments.createMap()
|
|
31
|
+
params.putString("token", token)
|
|
32
|
+
Log.d(TAG, "Send device token event")
|
|
33
|
+
PushNotificationEventManager.sendEvent(PushNotificationEventType.TOKEN_RECEIVED, params)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override fun handleIntent(intent: Intent) {
|
|
37
|
+
val extras = intent.extras ?: Bundle()
|
|
38
|
+
extras.getNotificationPayload()?.let {
|
|
39
|
+
// message contains push notification payload, show notification
|
|
40
|
+
onMessageReceived(it)
|
|
41
|
+
} ?: run {
|
|
42
|
+
Log.d(TAG, "Ignore intents that don't contain push notification payload")
|
|
43
|
+
super.handleIntent(intent)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private fun onMessageReceived(payload: NotificationPayload) {
|
|
48
|
+
if (utils.isAppInForeground()) {
|
|
49
|
+
Log.d(TAG, "Send foreground message received event")
|
|
50
|
+
PushNotificationEventManager.sendEvent(
|
|
51
|
+
PushNotificationEventType.FOREGROUND_MESSAGE_RECEIVED, payload.toWritableMap()
|
|
52
|
+
)
|
|
53
|
+
} else {
|
|
54
|
+
Log.d(
|
|
55
|
+
TAG, "App is in background, try to create notification and start headless service"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
utils.showNotification(payload)
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
val serviceIntent =
|
|
62
|
+
Intent(baseContext, PushNotificationHeadlessTaskService::class.java)
|
|
63
|
+
serviceIntent.putExtra("amplifyNotificationPayload", payload)
|
|
64
|
+
if (baseContext.startService(serviceIntent) != null) {
|
|
65
|
+
HeadlessJsTaskService.acquireWakeLockNow(baseContext)
|
|
66
|
+
}
|
|
67
|
+
} catch (exception: Exception) {
|
|
68
|
+
Log.e(
|
|
69
|
+
TAG, "Something went wrong while starting headless task: ${exception.message}"
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
package com.amazonaws.amplify.rtnpushnotification
|
|
5
|
+
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import com.amplifyframework.annotations.InternalAmplifyApi
|
|
8
|
+
import com.amplifyframework.notifications.pushnotifications.NotificationPayload
|
|
9
|
+
import com.facebook.react.HeadlessJsTaskService
|
|
10
|
+
import com.facebook.react.jstasks.HeadlessJsTaskConfig
|
|
11
|
+
|
|
12
|
+
class PushNotificationHeadlessTaskService : HeadlessJsTaskService() {
|
|
13
|
+
|
|
14
|
+
private val defaultTimeout: Long = 10000 // 10 seconds
|
|
15
|
+
|
|
16
|
+
@InternalAmplifyApi
|
|
17
|
+
override fun getTaskConfig(intent: Intent): HeadlessJsTaskConfig? {
|
|
18
|
+
return NotificationPayload.fromIntent(intent)?.let {
|
|
19
|
+
HeadlessJsTaskConfig(
|
|
20
|
+
HEADLESS_TASK_KEY, it.toWritableMap(), defaultTimeout, true
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
companion object {
|
|
26
|
+
const val HEADLESS_TASK_KEY = "PushNotificationHeadlessTaskKey"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package com.amazonaws.amplify.rtnpushnotification
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.ActivityNotFoundException
|
|
5
|
+
import android.os.Bundle
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import com.amplifyframework.annotations.InternalAmplifyApi
|
|
8
|
+
import com.amplifyframework.notifications.pushnotifications.NotificationPayload
|
|
9
|
+
|
|
10
|
+
private val TAG = PushNotificationLaunchActivity::class.java.simpleName
|
|
11
|
+
|
|
12
|
+
class PushNotificationLaunchActivity : Activity() {
|
|
13
|
+
@Override
|
|
14
|
+
@InternalAmplifyApi
|
|
15
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
16
|
+
super.onCreate(savedInstanceState)
|
|
17
|
+
val payload = NotificationPayload.fromIntent(intent)
|
|
18
|
+
val notificationIntent = payload.getProcessedIntent(applicationContext)
|
|
19
|
+
notificationIntent?.putExtras(intent)
|
|
20
|
+
try {
|
|
21
|
+
startActivity(notificationIntent)
|
|
22
|
+
} catch (e: ActivityNotFoundException) {
|
|
23
|
+
Log.e(TAG, "Unable to launch intent.", e)
|
|
24
|
+
}
|
|
25
|
+
finish()
|
|
26
|
+
}
|
|
27
|
+
}
|
package/android/src/main/kotlin/com/amazonaws/amplify/rtnpushnotification/PushNotificationModule.kt
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
package com.amazonaws.amplify.rtnpushnotification
|
|
5
|
+
|
|
6
|
+
import android.app.Activity
|
|
7
|
+
import android.content.Context.MODE_PRIVATE
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.util.Log
|
|
10
|
+
import androidx.core.app.ActivityCompat
|
|
11
|
+
import com.amplifyframework.annotations.InternalAmplifyApi
|
|
12
|
+
import com.amplifyframework.notifications.pushnotifications.NotificationPayload
|
|
13
|
+
import com.facebook.react.bridge.*
|
|
14
|
+
import com.google.android.gms.tasks.OnCompleteListener
|
|
15
|
+
import com.google.firebase.messaging.FirebaseMessaging
|
|
16
|
+
import kotlinx.coroutines.*
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
private val TAG = PushNotificationModule::class.java.simpleName
|
|
20
|
+
private const val PERMISSION = "android.permission.POST_NOTIFICATIONS"
|
|
21
|
+
private const val PREF_FILE_KEY = "com.amazonaws.amplify.rtnpushnotification"
|
|
22
|
+
private const val PREF_PREVIOUSLY_DENIED = "wasPermissionPreviouslyDenied"
|
|
23
|
+
|
|
24
|
+
enum class PushNotificationPermissionStatus {
|
|
25
|
+
ShouldRequest,
|
|
26
|
+
ShouldExplainThenRequest,
|
|
27
|
+
Granted,
|
|
28
|
+
Denied,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class PushNotificationModule(
|
|
32
|
+
reactContext: ReactApplicationContext,
|
|
33
|
+
dispatcher: CoroutineDispatcher = Dispatchers.Main
|
|
34
|
+
) : ReactContextBaseJavaModule(reactContext), ActivityEventListener, LifecycleEventListener {
|
|
35
|
+
|
|
36
|
+
private var isAppLaunch: Boolean = true
|
|
37
|
+
private var launchNotification: WritableMap? = null
|
|
38
|
+
private val sharedPreferences = reactContext.getSharedPreferences(PREF_FILE_KEY, MODE_PRIVATE)
|
|
39
|
+
private val scope = CoroutineScope(dispatcher)
|
|
40
|
+
|
|
41
|
+
init {
|
|
42
|
+
reactContext.addActivityEventListener(this)
|
|
43
|
+
reactContext.addLifecycleEventListener(this)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@ReactMethod
|
|
47
|
+
fun getLaunchNotification(promise: Promise) {
|
|
48
|
+
launchNotification?.let {
|
|
49
|
+
promise.resolve(launchNotification)
|
|
50
|
+
launchNotification = null
|
|
51
|
+
} ?: promise.resolve(null)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@ReactMethod
|
|
55
|
+
fun getPermissionStatus(promise: Promise) {
|
|
56
|
+
val permission = PushNotificationPermission(reactApplicationContext)
|
|
57
|
+
// If permission has already been granted
|
|
58
|
+
if (permission.hasRequiredPermission) {
|
|
59
|
+
return promise.resolve(PushNotificationPermissionStatus.Granted.name)
|
|
60
|
+
}
|
|
61
|
+
// If the shouldShowRequestPermissionRationale flag is true, permission must have been
|
|
62
|
+
// denied once (and only once) previously
|
|
63
|
+
if (shouldShowRequestPermissionRationale()) {
|
|
64
|
+
return promise.resolve(PushNotificationPermissionStatus.ShouldExplainThenRequest.name)
|
|
65
|
+
}
|
|
66
|
+
// If the shouldShowRequestPermissionRationale flag is false and the permission was
|
|
67
|
+
// already previously denied then user has denied permissions twice
|
|
68
|
+
if (sharedPreferences.getBoolean(PREF_PREVIOUSLY_DENIED, false)) {
|
|
69
|
+
return promise.resolve(PushNotificationPermissionStatus.Denied.name)
|
|
70
|
+
}
|
|
71
|
+
// Otherwise it's never been requested (or user could have dismissed the request without
|
|
72
|
+
// explicitly denying)
|
|
73
|
+
promise.resolve(PushNotificationPermissionStatus.ShouldRequest.name)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@ReactMethod
|
|
77
|
+
fun requestPermissions(
|
|
78
|
+
@Suppress("UNUSED_PARAMETER") permissions: ReadableMap,
|
|
79
|
+
promise: Promise
|
|
80
|
+
) {
|
|
81
|
+
scope.launch {
|
|
82
|
+
val permission = PushNotificationPermission(reactApplicationContext)
|
|
83
|
+
if (permission.requestPermission()) {
|
|
84
|
+
promise.resolve(true)
|
|
85
|
+
} else {
|
|
86
|
+
// If permission was not granted and the shouldShowRequestPermissionRationale flag
|
|
87
|
+
// is true then user must have denied for the first time. We will set the
|
|
88
|
+
// wasPermissionPreviouslyDenied value to true only in this scenario since it's
|
|
89
|
+
// possible to dismiss the permission request without explicitly denying as well.
|
|
90
|
+
if (shouldShowRequestPermissionRationale()) {
|
|
91
|
+
with(sharedPreferences.edit()) {
|
|
92
|
+
putBoolean(PREF_PREVIOUSLY_DENIED, true)
|
|
93
|
+
apply()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
promise.resolve(false)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@ReactMethod
|
|
102
|
+
fun addListener(@Suppress("UNUSED_PARAMETER") eventName: String) {
|
|
103
|
+
// noop - only required for RN NativeEventEmitter
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@ReactMethod
|
|
107
|
+
fun removeListeners(@Suppress("UNUSED_PARAMETER") count: Int) {
|
|
108
|
+
// noop - only required for RN NativeEventEmitter
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
override fun getName() = "AmplifyRTNPushNotification"
|
|
112
|
+
|
|
113
|
+
override fun getConstants(): MutableMap<String, Any> = hashMapOf(
|
|
114
|
+
"NativeEvent" to PushNotificationEventType.values()
|
|
115
|
+
.associateBy({ it.name }, { it.value }),
|
|
116
|
+
"NativeHeadlessTaskKey" to PushNotificationHeadlessTaskService.HEADLESS_TASK_KEY
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
override fun onActivityResult(p0: Activity?, p1: Int, p2: Int, p3: Intent?) {
|
|
120
|
+
// noop - only overridden as this class implements ActivityEventListener
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Send notification opened app event to JS layer if the app is in a background state
|
|
125
|
+
*/
|
|
126
|
+
@InternalAmplifyApi
|
|
127
|
+
override fun onNewIntent(intent: Intent) {
|
|
128
|
+
val payload = NotificationPayload.fromIntent(intent)
|
|
129
|
+
if (payload != null) {
|
|
130
|
+
PushNotificationEventManager.sendEvent(
|
|
131
|
+
PushNotificationEventType.NOTIFICATION_OPENED, payload.toWritableMap()
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* On every app resume (including launch), send the current device token to JS layer. Also
|
|
138
|
+
* store the app launching notification if app is in a quit state
|
|
139
|
+
*/
|
|
140
|
+
@InternalAmplifyApi
|
|
141
|
+
override fun onHostResume() {
|
|
142
|
+
if (isAppLaunch) {
|
|
143
|
+
isAppLaunch = false
|
|
144
|
+
PushNotificationEventManager.init(reactApplicationContext)
|
|
145
|
+
val firebaseInstance = FirebaseMessaging.getInstance()
|
|
146
|
+
firebaseInstance.token.addOnCompleteListener(OnCompleteListener { task ->
|
|
147
|
+
if (!task.isSuccessful) {
|
|
148
|
+
Log.w(TAG, "Fetching FCM registration token failed")
|
|
149
|
+
return@OnCompleteListener
|
|
150
|
+
}
|
|
151
|
+
val params = Arguments.createMap().apply {
|
|
152
|
+
putString("token", task.result)
|
|
153
|
+
}
|
|
154
|
+
Log.d(TAG, "Send device token event")
|
|
155
|
+
PushNotificationEventManager.sendEvent(
|
|
156
|
+
PushNotificationEventType.TOKEN_RECEIVED,
|
|
157
|
+
params
|
|
158
|
+
)
|
|
159
|
+
})
|
|
160
|
+
currentActivity?.intent?.let {
|
|
161
|
+
val payload = NotificationPayload.fromIntent(it)
|
|
162
|
+
if (payload != null) {
|
|
163
|
+
launchNotification = payload.toWritableMap()
|
|
164
|
+
// Launch notification opened event is emitted for internal use only
|
|
165
|
+
PushNotificationEventManager.sendEvent(
|
|
166
|
+
PushNotificationEventType.LAUNCH_NOTIFICATION_OPENED,
|
|
167
|
+
payload.toWritableMap()
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
// Wipe the launching notification as app was re-opened by some other means
|
|
173
|
+
launchNotification = null
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
override fun onHostPause() {
|
|
178
|
+
// noop - only overridden as this class implements LifecycleEventListener
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
override fun onHostDestroy() {
|
|
182
|
+
scope.cancel()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private fun shouldShowRequestPermissionRationale(): Boolean {
|
|
186
|
+
return ActivityCompat.shouldShowRequestPermissionRationale(currentActivity!!, PERMISSION)
|
|
187
|
+
}
|
|
188
|
+
}
|
package/android/src/main/kotlin/com/amazonaws/amplify/rtnpushnotification/PushNotificationPackage.kt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
package com.amazonaws.amplify.rtnpushnotification
|
|
5
|
+
|
|
6
|
+
import android.view.View
|
|
7
|
+
import com.facebook.react.ReactPackage
|
|
8
|
+
import com.facebook.react.bridge.NativeModule
|
|
9
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
10
|
+
import com.facebook.react.uimanager.ReactShadowNode
|
|
11
|
+
import com.facebook.react.uimanager.ViewManager
|
|
12
|
+
|
|
13
|
+
class PushNotificationPackage : ReactPackage {
|
|
14
|
+
|
|
15
|
+
override fun createViewManagers(
|
|
16
|
+
reactContext: ReactApplicationContext
|
|
17
|
+
): MutableList<ViewManager<View, ReactShadowNode<*>>> = mutableListOf()
|
|
18
|
+
|
|
19
|
+
override fun createNativeModules(
|
|
20
|
+
reactContext: ReactApplicationContext
|
|
21
|
+
): MutableList<NativeModule> = listOf(PushNotificationModule(reactContext)).toMutableList()
|
|
22
|
+
}
|