@loyalytics/swan-react-native-sdk 2.3.0 → 2.3.1-beta.1
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/android/src/main/kotlin/com/loyalytics/swan/SwanNotificationModule.kt +145 -1
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselAutoRemoteViews.kt +3 -22
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselFilmstripRemoteViews.kt +7 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselRemoteViews.kt +7 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselTemplate.kt +141 -9
- package/android/src/main/res/layout/swan_carousel_auto_expanded.xml +1 -1
- package/android/src/main/res/layout/swan_carousel_expanded.xml +1 -1
- package/android/src/main/res/layout/swan_carousel_filmstrip_expanded.xml +1 -1
- package/ios/SwanAppGroup.m +55 -0
- package/ios/SwanNotificationContentExtension/NotificationViewController.swift +90 -28
- package/ios/SwanNotificationContentExtension/templates/CarouselView.swift +5 -0
- package/ios/SwanNotificationServiceExtension/NotificationService.swift +1 -0
- package/lib/commonjs/components/HeaderView.js +0 -1
- package/lib/commonjs/components/HeaderView.js.map +1 -1
- package/lib/commonjs/index.js +315 -50
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/providers/FirebasePushProvider.js +2 -2
- package/lib/commonjs/providers/FirebasePushProvider.js.map +1 -1
- package/lib/commonjs/providers/NullPushProvider.js +1 -1
- package/lib/commonjs/providers/NullPushProvider.js.map +1 -1
- package/lib/commonjs/providers/PushNotificationProvider.js.map +1 -1
- package/lib/commonjs/services/PushTokenService.js +2 -2
- package/lib/commonjs/services/PushTokenService.js.map +1 -1
- package/lib/commonjs/utils/FirebaseNotificationManager.js +9 -5
- package/lib/commonjs/utils/FirebaseNotificationManager.js.map +1 -1
- package/lib/commonjs/utils/NotificationSoundHelper.js +72 -0
- package/lib/commonjs/utils/NotificationSoundHelper.js.map +1 -0
- package/lib/commonjs/utils/SharedCredentialsManager.js +46 -8
- package/lib/commonjs/utils/SharedCredentialsManager.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/commonjs/version.js.map +1 -1
- package/lib/module/components/HeaderView.js +1 -1
- package/lib/module/components/HeaderView.js.map +1 -1
- package/lib/module/index.js +312 -48
- package/lib/module/index.js.map +1 -1
- package/lib/module/providers/FirebasePushProvider.js +2 -2
- package/lib/module/providers/FirebasePushProvider.js.map +1 -1
- package/lib/module/providers/NullPushProvider.js +1 -1
- package/lib/module/providers/NullPushProvider.js.map +1 -1
- package/lib/module/providers/PushNotificationProvider.js.map +1 -1
- package/lib/module/services/PushTokenService.js +2 -2
- package/lib/module/services/PushTokenService.js.map +1 -1
- package/lib/module/utils/FirebaseNotificationManager.js +9 -5
- package/lib/module/utils/FirebaseNotificationManager.js.map +1 -1
- package/lib/module/utils/NotificationSoundHelper.js +66 -0
- package/lib/module/utils/NotificationSoundHelper.js.map +1 -0
- package/lib/module/utils/SharedCredentialsManager.js +48 -9
- package/lib/module/utils/SharedCredentialsManager.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/module/version.js.map +1 -1
- package/lib/typescript/commonjs/src/components/HeaderView.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +20 -1
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/providers/FirebasePushProvider.d.ts +1 -1
- package/lib/typescript/commonjs/src/providers/FirebasePushProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/providers/NullPushProvider.d.ts +1 -1
- package/lib/typescript/commonjs/src/providers/NullPushProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/providers/PushNotificationProvider.d.ts +1 -1
- package/lib/typescript/commonjs/src/providers/PushNotificationProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/services/PushTokenService.d.ts +1 -1
- package/lib/typescript/commonjs/src/services/PushTokenService.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/utils/FirebaseNotificationManager.d.ts +1 -1
- package/lib/typescript/commonjs/src/utils/FirebaseNotificationManager.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/utils/NotificationSoundHelper.d.ts +34 -0
- package/lib/typescript/commonjs/src/utils/NotificationSoundHelper.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts +6 -0
- package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/version.d.ts +1 -1
- package/lib/typescript/commonjs/src/version.d.ts.map +1 -1
- package/lib/typescript/module/src/components/HeaderView.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +20 -1
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/providers/FirebasePushProvider.d.ts +1 -1
- package/lib/typescript/module/src/providers/FirebasePushProvider.d.ts.map +1 -1
- package/lib/typescript/module/src/providers/NullPushProvider.d.ts +1 -1
- package/lib/typescript/module/src/providers/NullPushProvider.d.ts.map +1 -1
- package/lib/typescript/module/src/providers/PushNotificationProvider.d.ts +1 -1
- package/lib/typescript/module/src/providers/PushNotificationProvider.d.ts.map +1 -1
- package/lib/typescript/module/src/services/PushTokenService.d.ts +1 -1
- package/lib/typescript/module/src/services/PushTokenService.d.ts.map +1 -1
- package/lib/typescript/module/src/utils/FirebaseNotificationManager.d.ts +1 -1
- package/lib/typescript/module/src/utils/FirebaseNotificationManager.d.ts.map +1 -1
- package/lib/typescript/module/src/utils/NotificationSoundHelper.d.ts +34 -0
- package/lib/typescript/module/src/utils/NotificationSoundHelper.d.ts.map +1 -0
- package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts +6 -0
- package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts.map +1 -1
- package/lib/typescript/module/src/version.d.ts +1 -1
- package/lib/typescript/module/src/version.d.ts.map +1 -1
- package/package.json +23 -10
- package/scripts/setup-ios-extension.js +61 -41
- package/swan-react-native-sdk.podspec +1 -0
|
@@ -1,20 +1,164 @@
|
|
|
1
1
|
package com.loyalytics.swan
|
|
2
2
|
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.util.Log
|
|
3
8
|
import com.facebook.react.bridge.*
|
|
9
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
4
10
|
import com.loyalytics.swan.templates.SwanTemplateRegistry
|
|
5
11
|
import com.loyalytics.swan.templates.carousel.CarouselTemplate
|
|
12
|
+
import com.loyalytics.swan.templates.common.NotificationBitmapCache
|
|
13
|
+
import com.loyalytics.swan.templates.common.NotificationStateManager
|
|
6
14
|
import kotlinx.coroutines.CoroutineScope
|
|
7
15
|
import kotlinx.coroutines.Dispatchers
|
|
8
16
|
import kotlinx.coroutines.launch
|
|
17
|
+
import org.json.JSONObject
|
|
9
18
|
|
|
10
19
|
class SwanNotificationModule(reactContext: ReactApplicationContext) :
|
|
11
|
-
ReactContextBaseJavaModule(reactContext) {
|
|
20
|
+
ReactContextBaseJavaModule(reactContext), ActivityEventListener {
|
|
12
21
|
|
|
13
22
|
override fun getName(): String = "SwanNotificationModule"
|
|
14
23
|
|
|
24
|
+
companion object {
|
|
25
|
+
private const val TAG = "SwanNotificationModule"
|
|
26
|
+
private const val EVENT_CAROUSEL_CLICK = "swanCarouselClick"
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
init {
|
|
16
30
|
// Register built-in templates
|
|
17
31
|
SwanTemplateRegistry.register(CarouselTemplate())
|
|
32
|
+
|
|
33
|
+
// Listen for new intents (e.g., when PendingIntent.getActivity() delivers
|
|
34
|
+
// carousel click data to an already-running Activity via onNewIntent)
|
|
35
|
+
reactContext.addActivityEventListener(this)
|
|
36
|
+
|
|
37
|
+
// Register click listener so carousel clicks emit a DeviceEventEmitter event
|
|
38
|
+
// to the JS layer. This covers the foreground-click case where AppState
|
|
39
|
+
// stays 'active' and the polling approach never triggers.
|
|
40
|
+
CarouselTemplate.clickListener = { clickData ->
|
|
41
|
+
try {
|
|
42
|
+
val map = Arguments.createMap().apply {
|
|
43
|
+
putString("messageId", clickData.optString("messageId"))
|
|
44
|
+
putString("route", clickData.optString("route"))
|
|
45
|
+
putInt("itemIndex", clickData.optInt("itemIndex"))
|
|
46
|
+
putString("title", clickData.optString("title"))
|
|
47
|
+
putString("body", clickData.optString("body"))
|
|
48
|
+
}
|
|
49
|
+
reactContext
|
|
50
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
51
|
+
.emit(EVENT_CAROUSEL_CLICK, map)
|
|
52
|
+
} catch (e: Exception) {
|
|
53
|
+
Log.d(TAG, "React bridge not available for click event, will use polling fallback")
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Called when the Activity receives a new intent via onNewIntent().
|
|
60
|
+
* On Android 12+, carousel clicks use PendingIntent.getActivity() which delivers
|
|
61
|
+
* click data as intent extras. We extract the data here and save it to the static
|
|
62
|
+
* variable so getPendingCarouselClick() can find it.
|
|
63
|
+
*/
|
|
64
|
+
override fun onNewIntent(intent: Intent?) {
|
|
65
|
+
if (intent == null) return
|
|
66
|
+
if (!intent.getBooleanExtra(CarouselTemplate.EXTRA_SWAN_CAROUSEL_CLICK, false)) return
|
|
67
|
+
|
|
68
|
+
val messageId = intent.getStringExtra(CarouselTemplate.EXTRA_SWAN_MESSAGE_ID) ?: ""
|
|
69
|
+
val route = intent.getStringExtra(CarouselTemplate.EXTRA_SWAN_ROUTE) ?: ""
|
|
70
|
+
val itemIndex = intent.getIntExtra(CarouselTemplate.EXTRA_SWAN_ITEM_INDEX, 0)
|
|
71
|
+
val notificationId = intent.getIntExtra(CarouselTemplate.EXTRA_SWAN_NOTIFICATION_ID, 0)
|
|
72
|
+
|
|
73
|
+
// Get title/body from notification state (saved during display)
|
|
74
|
+
val activity = currentActivity
|
|
75
|
+
val state = if (activity != null) NotificationStateManager.get(activity, notificationId) else null
|
|
76
|
+
val title = state?.optString("title", "") ?: ""
|
|
77
|
+
val body = state?.optString("body", "") ?: ""
|
|
78
|
+
|
|
79
|
+
// Save click data to static variable for JS SDK to consume
|
|
80
|
+
val clickData = JSONObject().apply {
|
|
81
|
+
put("messageId", messageId)
|
|
82
|
+
put("route", route)
|
|
83
|
+
put("itemIndex", itemIndex)
|
|
84
|
+
put("title", title)
|
|
85
|
+
put("body", body)
|
|
86
|
+
}
|
|
87
|
+
CarouselTemplate.savePendingClick(clickData)
|
|
88
|
+
|
|
89
|
+
// Clean up: cancel notification, clear bitmap cache, remove state
|
|
90
|
+
if (activity != null) {
|
|
91
|
+
NotificationBitmapCache.clear(notificationId)
|
|
92
|
+
NotificationStateManager.remove(activity, notificationId)
|
|
93
|
+
val nm = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
94
|
+
nm.cancel(notificationId)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override fun onActivityResult(activity: Activity?, requestCode: Int, resultCode: Int, data: Intent?) {
|
|
99
|
+
// Not used
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check for a pending carousel click.
|
|
104
|
+
* Called by the JS SDK on AppState 'active' to bridge native carousel
|
|
105
|
+
* clicks into the JS NOTIFICATION_OPENED event system.
|
|
106
|
+
*
|
|
107
|
+
* Checks two sources:
|
|
108
|
+
* 1. Static variable (set by BroadcastReceiver on pre-Android 12, or by onNewIntent on Android 12+)
|
|
109
|
+
* 2. Activity intent extras (covers fresh app launch from killed state on Android 12+)
|
|
110
|
+
*/
|
|
111
|
+
@ReactMethod
|
|
112
|
+
fun getPendingCarouselClick(promise: Promise) {
|
|
113
|
+
// Source 1: Static variable (primary path for both pre-12 and 12+)
|
|
114
|
+
val click = CarouselTemplate.consumePendingClick()
|
|
115
|
+
if (click != null) {
|
|
116
|
+
val map = Arguments.createMap().apply {
|
|
117
|
+
putString("messageId", click.optString("messageId"))
|
|
118
|
+
putString("route", click.optString("route"))
|
|
119
|
+
putInt("itemIndex", click.optInt("itemIndex"))
|
|
120
|
+
putString("title", click.optString("title"))
|
|
121
|
+
putString("body", click.optString("body"))
|
|
122
|
+
}
|
|
123
|
+
promise.resolve(map)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Source 2: Activity intent extras (covers fresh launch from killed state)
|
|
128
|
+
val activity = currentActivity
|
|
129
|
+
val intent = activity?.intent
|
|
130
|
+
if (intent != null && intent.getBooleanExtra(CarouselTemplate.EXTRA_SWAN_CAROUSEL_CLICK, false)) {
|
|
131
|
+
val messageId = intent.getStringExtra(CarouselTemplate.EXTRA_SWAN_MESSAGE_ID) ?: ""
|
|
132
|
+
val route = intent.getStringExtra(CarouselTemplate.EXTRA_SWAN_ROUTE) ?: ""
|
|
133
|
+
val itemIndex = intent.getIntExtra(CarouselTemplate.EXTRA_SWAN_ITEM_INDEX, 0)
|
|
134
|
+
val notificationId = intent.getIntExtra(CarouselTemplate.EXTRA_SWAN_NOTIFICATION_ID, 0)
|
|
135
|
+
|
|
136
|
+
// Get title/body from notification state (saved during display)
|
|
137
|
+
val state = NotificationStateManager.get(activity, notificationId)
|
|
138
|
+
val title = state?.optString("title", "") ?: ""
|
|
139
|
+
val body = state?.optString("body", "") ?: ""
|
|
140
|
+
|
|
141
|
+
// Clear the intent flag to prevent re-consumption
|
|
142
|
+
intent.removeExtra(CarouselTemplate.EXTRA_SWAN_CAROUSEL_CLICK)
|
|
143
|
+
|
|
144
|
+
// Clean up
|
|
145
|
+
NotificationBitmapCache.clear(notificationId)
|
|
146
|
+
NotificationStateManager.remove(activity, notificationId)
|
|
147
|
+
val nm = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
148
|
+
nm.cancel(notificationId)
|
|
149
|
+
|
|
150
|
+
val map = Arguments.createMap().apply {
|
|
151
|
+
putString("messageId", messageId)
|
|
152
|
+
putString("route", route)
|
|
153
|
+
putInt("itemIndex", itemIndex)
|
|
154
|
+
putString("title", title)
|
|
155
|
+
putString("body", body)
|
|
156
|
+
}
|
|
157
|
+
promise.resolve(map)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
promise.resolve(null)
|
|
18
162
|
}
|
|
19
163
|
|
|
20
164
|
@ReactMethod
|
package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselAutoRemoteViews.kt
CHANGED
|
@@ -2,13 +2,9 @@ package com.loyalytics.swan.templates.carousel
|
|
|
2
2
|
|
|
3
3
|
import android.app.PendingIntent
|
|
4
4
|
import android.content.Context
|
|
5
|
-
import android.content.Intent
|
|
6
5
|
import android.graphics.Bitmap
|
|
7
|
-
import android.os.Build
|
|
8
|
-
import android.os.Bundle
|
|
9
6
|
import android.widget.RemoteViews
|
|
10
7
|
import com.loyalytics.swan.R
|
|
11
|
-
import com.loyalytics.swan.templates.SwanNotificationActionReceiver
|
|
12
8
|
|
|
13
9
|
/**
|
|
14
10
|
* Builds RemoteViews for auto-carousel using ViewFlipper.
|
|
@@ -80,24 +76,9 @@ object CarouselAutoRemoteViews {
|
|
|
80
76
|
messageId: String,
|
|
81
77
|
route: String
|
|
82
78
|
): PendingIntent {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
putExtra(SwanNotificationActionReceiver.EXTRA_TEMPLATE_ACTION, CarouselTemplate.ACTION_CLICK)
|
|
87
|
-
putExtra(SwanNotificationActionReceiver.EXTRA_NOTIFICATION_ID, notificationId)
|
|
88
|
-
putExtra(CarouselTemplate.EXTRA_MESSAGE_ID, messageId)
|
|
89
|
-
putExtra(CarouselTemplate.EXTRA_ITEM_INDEX, 0)
|
|
90
|
-
putExtra(CarouselTemplate.EXTRA_ITEM_ROUTE, route)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
94
|
-
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
95
|
-
} else {
|
|
96
|
-
PendingIntent.FLAG_UPDATE_CURRENT
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return PendingIntent.getBroadcast(
|
|
100
|
-
context, "${notificationId}AUTO_CLICK".hashCode(), intent, flags
|
|
79
|
+
// Use PendingIntent.getActivity() to avoid Android 12+ trampoline restriction
|
|
80
|
+
return CarouselTemplate.createClickActivityPendingIntent(
|
|
81
|
+
context, notificationId, messageId, 0, route
|
|
101
82
|
)
|
|
102
83
|
}
|
|
103
84
|
}
|
|
@@ -109,6 +109,13 @@ object CarouselFilmstripRemoteViews {
|
|
|
109
109
|
itemIndex: Int,
|
|
110
110
|
itemRoute: String
|
|
111
111
|
): PendingIntent {
|
|
112
|
+
// For CLICK, use PendingIntent.getActivity() to avoid Android 12+ trampoline restriction
|
|
113
|
+
if (action == CarouselTemplate.ACTION_CLICK) {
|
|
114
|
+
return CarouselTemplate.createClickActivityPendingIntent(
|
|
115
|
+
context, notificationId, messageId, itemIndex, itemRoute
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
112
119
|
val intent = Intent(context, SwanNotificationActionReceiver::class.java).apply {
|
|
113
120
|
this.action = "com.loyalytics.swan.${action}_$notificationId"
|
|
114
121
|
putExtra(SwanNotificationActionReceiver.EXTRA_TEMPLATE_TYPE, CarouselTemplate.TYPE)
|
package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselRemoteViews.kt
CHANGED
|
@@ -106,6 +106,13 @@ object CarouselRemoteViews {
|
|
|
106
106
|
itemIndex: Int,
|
|
107
107
|
itemRoute: String
|
|
108
108
|
): PendingIntent {
|
|
109
|
+
// For CLICK, use PendingIntent.getActivity() to avoid Android 12+ trampoline restriction
|
|
110
|
+
if (action == CarouselTemplate.ACTION_CLICK) {
|
|
111
|
+
return CarouselTemplate.createClickActivityPendingIntent(
|
|
112
|
+
context, notificationId, messageId, itemIndex, itemRoute
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
109
116
|
val intent = Intent(context, SwanNotificationActionReceiver::class.java).apply {
|
|
110
117
|
this.action = "com.loyalytics.swan.${action}_$notificationId"
|
|
111
118
|
putExtra(SwanNotificationActionReceiver.EXTRA_TEMPLATE_TYPE, CarouselTemplate.TYPE)
|
|
@@ -36,6 +36,81 @@ class CarouselTemplate : SwanNotificationTemplate {
|
|
|
36
36
|
const val EXTRA_ITEM_INDEX = "itemIndex"
|
|
37
37
|
const val EXTRA_ITEM_ROUTE = "itemRoute"
|
|
38
38
|
const val EXTRA_MESSAGE_ID = "messageId"
|
|
39
|
+
|
|
40
|
+
// Activity intent extras (prefixed to avoid conflicts)
|
|
41
|
+
const val EXTRA_SWAN_CAROUSEL_CLICK = "swanCarouselClick"
|
|
42
|
+
const val EXTRA_SWAN_MESSAGE_ID = "swanMessageId"
|
|
43
|
+
const val EXTRA_SWAN_ROUTE = "swanRoute"
|
|
44
|
+
const val EXTRA_SWAN_ITEM_INDEX = "swanItemIndex"
|
|
45
|
+
const val EXTRA_SWAN_NOTIFICATION_ID = "swanNotificationId"
|
|
46
|
+
const val EXTRA_SWAN_TITLE = "swanTitle"
|
|
47
|
+
const val EXTRA_SWAN_BODY = "swanBody"
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Pending click data for the JS bridge.
|
|
51
|
+
* When a carousel item is clicked, the click data is stored here
|
|
52
|
+
* so the JS SDK can read it via SwanNotificationModule.getPendingCarouselClick()
|
|
53
|
+
* and emit the appropriate NOTIFICATION_OPENED event.
|
|
54
|
+
*/
|
|
55
|
+
@Volatile
|
|
56
|
+
private var pendingClick: JSONObject? = null
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Optional listener invoked immediately when a carousel item is clicked.
|
|
60
|
+
* SwanNotificationModule registers this to emit a DeviceEventEmitter event
|
|
61
|
+
* to the JS layer, covering the foreground-click case where AppState
|
|
62
|
+
* doesn't change (so the polling approach never triggers).
|
|
63
|
+
*/
|
|
64
|
+
var clickListener: ((JSONObject) -> Unit)? = null
|
|
65
|
+
|
|
66
|
+
fun savePendingClick(data: JSONObject) {
|
|
67
|
+
pendingClick = data
|
|
68
|
+
clickListener?.invoke(data)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fun consumePendingClick(): JSONObject? {
|
|
72
|
+
val click = pendingClick
|
|
73
|
+
pendingClick = null
|
|
74
|
+
return click
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a PendingIntent that directly launches the app's main Activity.
|
|
79
|
+
* Android 12+ blocks BroadcastReceiver → startActivity() ("notification trampolines"),
|
|
80
|
+
* so CLICK actions must use PendingIntent.getActivity() instead of getBroadcast().
|
|
81
|
+
*/
|
|
82
|
+
fun createClickActivityPendingIntent(
|
|
83
|
+
context: Context,
|
|
84
|
+
notificationId: Int,
|
|
85
|
+
messageId: String,
|
|
86
|
+
itemIndex: Int,
|
|
87
|
+
itemRoute: String
|
|
88
|
+
): PendingIntent {
|
|
89
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
90
|
+
?: Intent(Intent.ACTION_MAIN).apply {
|
|
91
|
+
addCategory(Intent.CATEGORY_LAUNCHER)
|
|
92
|
+
setPackage(context.packageName)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
launchIntent.apply {
|
|
96
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
97
|
+
putExtra(EXTRA_SWAN_CAROUSEL_CLICK, true)
|
|
98
|
+
putExtra(EXTRA_SWAN_MESSAGE_ID, messageId)
|
|
99
|
+
putExtra(EXTRA_SWAN_ROUTE, itemRoute)
|
|
100
|
+
putExtra(EXTRA_SWAN_ITEM_INDEX, itemIndex)
|
|
101
|
+
putExtra(EXTRA_SWAN_NOTIFICATION_ID, notificationId)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
105
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
106
|
+
} else {
|
|
107
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return PendingIntent.getActivity(
|
|
111
|
+
context, "$notificationId${ACTION_CLICK}$itemIndex".hashCode(), launchIntent, flags
|
|
112
|
+
)
|
|
113
|
+
}
|
|
39
114
|
}
|
|
40
115
|
|
|
41
116
|
override fun canHandle(notificationType: String): Boolean = notificationType == TYPE
|
|
@@ -142,6 +217,11 @@ class CarouselTemplate : SwanNotificationTemplate {
|
|
|
142
217
|
)
|
|
143
218
|
}
|
|
144
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Handle carousel item click from BroadcastReceiver.
|
|
222
|
+
* On Android 12+ (API 31+), CLICK actions use PendingIntent.getActivity() directly,
|
|
223
|
+
* so this method is only called on older Android versions.
|
|
224
|
+
*/
|
|
145
225
|
private fun handleItemClick(context: Context, extras: Bundle, notificationId: Int) {
|
|
146
226
|
NotificationBitmapCache.clear(notificationId)
|
|
147
227
|
|
|
@@ -149,16 +229,35 @@ class CarouselTemplate : SwanNotificationTemplate {
|
|
|
149
229
|
val itemRoute = extras.getString(EXTRA_ITEM_ROUTE, "")
|
|
150
230
|
val itemIndex = extras.getInt(EXTRA_ITEM_INDEX, 0)
|
|
151
231
|
|
|
232
|
+
// Get notification metadata from state for the JS event
|
|
233
|
+
val state = NotificationStateManager.get(context, notificationId)
|
|
234
|
+
val title = state?.optString("title", "") ?: ""
|
|
235
|
+
val body = state?.optString("body", "") ?: ""
|
|
236
|
+
val defaultRoute = state?.optString("defaultRoute", "") ?: ""
|
|
237
|
+
|
|
152
238
|
Log.d(TAG, "Carousel item clicked: messageId=$messageId, index=$itemIndex, route=$itemRoute")
|
|
153
239
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
240
|
+
// Save click data for JS SDK to consume via getPendingCarouselClick()
|
|
241
|
+
val clickData = JSONObject().apply {
|
|
242
|
+
put("messageId", messageId)
|
|
243
|
+
put("route", itemRoute.ifEmpty { defaultRoute })
|
|
244
|
+
put("itemIndex", itemIndex)
|
|
245
|
+
put("title", title)
|
|
246
|
+
put("body", body)
|
|
247
|
+
}
|
|
248
|
+
savePendingClick(clickData)
|
|
249
|
+
|
|
250
|
+
// Launch the app — on Android 12+, this is handled by PendingIntent.getActivity() instead
|
|
251
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
252
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
253
|
+
if (launchIntent != null) {
|
|
254
|
+
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
255
|
+
launchIntent.putExtra(EXTRA_SWAN_CAROUSEL_CLICK, true)
|
|
256
|
+
launchIntent.putExtra(EXTRA_SWAN_MESSAGE_ID, messageId)
|
|
257
|
+
launchIntent.putExtra(EXTRA_SWAN_ROUTE, itemRoute)
|
|
258
|
+
launchIntent.putExtra(EXTRA_SWAN_ITEM_INDEX, itemIndex)
|
|
259
|
+
context.startActivity(launchIntent)
|
|
260
|
+
}
|
|
162
261
|
}
|
|
163
262
|
|
|
164
263
|
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
@@ -249,6 +348,18 @@ class CarouselTemplate : SwanNotificationTemplate {
|
|
|
249
348
|
context, notificationId, ACTION_DISMISS, Bundle()
|
|
250
349
|
))
|
|
251
350
|
|
|
351
|
+
// Set contentIntent so tapping the notification (collapsed or expanded text area) opens the app
|
|
352
|
+
val defaultRoute = state?.optString("defaultRoute", "") ?: ""
|
|
353
|
+
val contentRoute = itemRoute.ifEmpty { defaultRoute }
|
|
354
|
+
val contentClickExtras = Bundle().apply {
|
|
355
|
+
putString(EXTRA_MESSAGE_ID, messageId)
|
|
356
|
+
putString(EXTRA_ITEM_ROUTE, contentRoute)
|
|
357
|
+
putInt(EXTRA_ITEM_INDEX, currentIndex)
|
|
358
|
+
}
|
|
359
|
+
builder.setContentIntent(createActionPendingIntent(
|
|
360
|
+
context, notificationId, ACTION_CLICK, contentClickExtras
|
|
361
|
+
))
|
|
362
|
+
|
|
252
363
|
// DecoratedCustomViewStyle on Android 12+ (same as CleverTap)
|
|
253
364
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
254
365
|
builder.setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
|
@@ -301,12 +412,22 @@ class CarouselTemplate : SwanNotificationTemplate {
|
|
|
301
412
|
.setCustomBigContentView(views.expanded)
|
|
302
413
|
.setCustomContentView(views.collapsed)
|
|
303
414
|
.setOngoing(false)
|
|
304
|
-
.setAutoCancel(
|
|
415
|
+
.setAutoCancel(true)
|
|
305
416
|
.setOnlyAlertOnce(true)
|
|
306
417
|
.setDeleteIntent(createActionPendingIntent(
|
|
307
418
|
context, notificationId, ACTION_DISMISS, Bundle()
|
|
308
419
|
))
|
|
309
420
|
|
|
421
|
+
// Set contentIntent so tapping the notification opens the app with the default route
|
|
422
|
+
val contentClickExtras = Bundle().apply {
|
|
423
|
+
putString(EXTRA_MESSAGE_ID, messageId)
|
|
424
|
+
putString(EXTRA_ITEM_ROUTE, route)
|
|
425
|
+
putInt(EXTRA_ITEM_INDEX, 0)
|
|
426
|
+
}
|
|
427
|
+
builder.setContentIntent(createActionPendingIntent(
|
|
428
|
+
context, notificationId, ACTION_CLICK, contentClickExtras
|
|
429
|
+
))
|
|
430
|
+
|
|
310
431
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
311
432
|
builder.setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
|
312
433
|
}
|
|
@@ -321,6 +442,17 @@ class CarouselTemplate : SwanNotificationTemplate {
|
|
|
321
442
|
action: String,
|
|
322
443
|
extras: Bundle
|
|
323
444
|
): PendingIntent {
|
|
445
|
+
// For CLICK, use PendingIntent.getActivity() to avoid Android 12+ trampoline restriction
|
|
446
|
+
if (action == ACTION_CLICK) {
|
|
447
|
+
return createClickActivityPendingIntent(
|
|
448
|
+
context,
|
|
449
|
+
notificationId,
|
|
450
|
+
extras.getString(EXTRA_MESSAGE_ID, ""),
|
|
451
|
+
extras.getInt(EXTRA_ITEM_INDEX, 0),
|
|
452
|
+
extras.getString(EXTRA_ITEM_ROUTE, "")
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
|
|
324
456
|
val intent = Intent(context, SwanNotificationActionReceiver::class.java).apply {
|
|
325
457
|
this.action = "com.loyalytics.swan.${action}_$notificationId"
|
|
326
458
|
putExtra(SwanNotificationActionReceiver.EXTRA_TEMPLATE_TYPE, TYPE)
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
<ViewFlipper
|
|
42
42
|
android:id="@+id/swan_carousel_flipper"
|
|
43
43
|
android:layout_width="match_parent"
|
|
44
|
-
android:layout_height="
|
|
44
|
+
android:layout_height="144dp"
|
|
45
45
|
android:layout_below="@id/swan_carousel_auto_header"
|
|
46
46
|
android:inAnimation="@anim/swan_slide_in_right"
|
|
47
47
|
android:outAnimation="@anim/swan_slide_out_left"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<!-- 3-image filmstrip: left preview | center | right preview -->
|
|
24
24
|
<LinearLayout
|
|
25
25
|
android:layout_width="match_parent"
|
|
26
|
-
android:layout_height="
|
|
26
|
+
android:layout_height="144dp"
|
|
27
27
|
android:orientation="horizontal"
|
|
28
28
|
android:gravity="center_vertical">
|
|
29
29
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//
|
|
2
|
+
// SwanAppGroup.m
|
|
3
|
+
// swan-react-native-sdk
|
|
4
|
+
//
|
|
5
|
+
// Native bridge for iOS App Group UserDefaults.
|
|
6
|
+
// Replaces react-native-shared-group-preferences to fix cross-process
|
|
7
|
+
// caching bug: the third-party library caches the NSUserDefaults instance
|
|
8
|
+
// and misses writes from the Notification Content Extension process.
|
|
9
|
+
//
|
|
10
|
+
// This module creates a fresh NSUserDefaults instance on every call,
|
|
11
|
+
// ensuring reads always reflect the latest data from any process.
|
|
12
|
+
//
|
|
13
|
+
|
|
14
|
+
#import <React/RCTBridgeModule.h>
|
|
15
|
+
|
|
16
|
+
@interface SwanAppGroup : NSObject <RCTBridgeModule>
|
|
17
|
+
@end
|
|
18
|
+
|
|
19
|
+
@implementation SwanAppGroup
|
|
20
|
+
|
|
21
|
+
RCT_EXPORT_MODULE();
|
|
22
|
+
|
|
23
|
+
RCT_EXPORT_METHOD(getItem:(NSString *)key
|
|
24
|
+
suiteName:(NSString *)suiteName
|
|
25
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
26
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
27
|
+
{
|
|
28
|
+
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName];
|
|
29
|
+
if (!defaults) {
|
|
30
|
+
reject(@"ERR_APP_GROUP", @"Failed to access App Group", nil);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
NSString *value = [defaults stringForKey:key];
|
|
35
|
+
resolve(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
RCT_EXPORT_METHOD(setItem:(NSString *)key
|
|
39
|
+
value:(NSString *)value
|
|
40
|
+
suiteName:(NSString *)suiteName
|
|
41
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
42
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
43
|
+
{
|
|
44
|
+
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName];
|
|
45
|
+
if (!defaults) {
|
|
46
|
+
reject(@"ERR_APP_GROUP", @"Failed to access App Group", nil);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[defaults setObject:value forKey:key];
|
|
51
|
+
[defaults synchronize];
|
|
52
|
+
resolve(nil);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@end
|