@loyalytics/swan-react-native-sdk 2.1.3-beta.0 → 2.1.3-beta.2
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/build.gradle +66 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/kotlin/com/loyalytics/swan/SwanNotificationModule.kt +43 -0
- package/android/src/main/kotlin/com/loyalytics/swan/SwanNotificationPackage.kt +16 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/SwanNotificationActionReceiver.kt +49 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/SwanNotificationTemplate.kt +20 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/SwanTemplateRegistry.kt +47 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselAutoRemoteViews.kt +103 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselFilmstripRemoteViews.kt +132 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselRemoteViews.kt +129 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselTemplate.kt +412 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/common/NotificationBitmapCache.kt +70 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/common/NotificationImageLoader.kt +97 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/common/NotificationStateManager.kt +85 -0
- package/android/src/main/res/anim/swan_fade_in.xml +6 -0
- package/android/src/main/res/anim/swan_fade_out.xml +6 -0
- package/android/src/main/res/anim/swan_slide_in_right.xml +8 -0
- package/android/src/main/res/anim/swan_slide_out_left.xml +8 -0
- package/android/src/main/res/drawable/swan_ic_chevron_left.xml +11 -0
- package/android/src/main/res/drawable/swan_ic_chevron_right.xml +11 -0
- package/android/src/main/res/layout/swan_carousel_auto_expanded.xml +51 -0
- package/android/src/main/res/layout/swan_carousel_collapsed.xml +31 -0
- package/android/src/main/res/layout/swan_carousel_expanded.xml +96 -0
- package/android/src/main/res/layout/swan_carousel_filmstrip_expanded.xml +115 -0
- package/android/src/main/res/layout/swan_carousel_flipper_item.xml +7 -0
- package/android/src/test/kotlin/com/loyalytics/swan/templates/carousel/CarouselTemplateTest.kt +125 -0
- package/docs/SDK_INDUSTRY_REVIEW_REPORT.md +347 -0
- package/docs/Swan_Push_Notifications.postman_collection.json +330 -0
- package/docs/deep-link-attribution.md +281 -0
- package/ios/SwanNotificationContentExtension/Info.plist +40 -0
- package/ios/SwanNotificationContentExtension/MainInterface.storyboard +19 -0
- package/ios/SwanNotificationContentExtension/NotificationViewController.swift +190 -0
- package/ios/SwanNotificationContentExtension/SwanNotificationContentExtension.entitlements +10 -0
- package/ios/SwanNotificationContentExtension/common/ImageDownloader.swift +32 -0
- package/ios/SwanNotificationContentExtension/templates/CarouselView.swift +336 -0
- package/lib/commonjs/constants/ApiUrls.js.map +1 -1
- package/lib/commonjs/index.js +150 -35
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/providers/NullPushProvider.js.map +1 -1
- package/lib/commonjs/services/DeviceRegistrationService.js.map +1 -1
- package/lib/commonjs/state/AuthStateMachine.js.map +1 -1
- package/lib/commonjs/state/DeviceStateMachine.js.map +1 -1
- package/lib/commonjs/state/PushStateMachine.js.map +1 -1
- package/lib/commonjs/utils/FirebaseNotificationManager.js.map +1 -1
- package/lib/commonjs/utils/Logger.js.map +1 -1
- package/lib/commonjs/utils/SharedCredentialsManager.js +28 -0
- package/lib/commonjs/utils/SharedCredentialsManager.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/index.js +150 -35
- package/lib/module/index.js.map +1 -1
- package/lib/module/providers/NullPushProvider.js.map +1 -1
- package/lib/module/services/DeviceRegistrationService.js.map +1 -1
- package/lib/module/state/AuthStateMachine.js.map +1 -1
- package/lib/module/state/DeviceStateMachine.js.map +1 -1
- package/lib/module/state/PushStateMachine.js.map +1 -1
- package/lib/module/utils/FirebaseNotificationManager.js.map +1 -1
- package/lib/module/utils/Logger.js.map +1 -1
- package/lib/module/utils/SharedCredentialsManager.js +28 -0
- package/lib/module/utils/SharedCredentialsManager.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/commonjs/src/constants/ApiUrls.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +24 -7
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/providers/NullPushProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/services/DeviceRegistrationService.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/state/AuthStateMachine.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/state/DeviceStateMachine.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/state/PushStateMachine.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/utils/FirebaseNotificationManager.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/utils/Logger.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts +13 -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/module/src/constants/ApiUrls.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +24 -7
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/providers/NullPushProvider.d.ts.map +1 -1
- package/lib/typescript/module/src/services/DeviceRegistrationService.d.ts.map +1 -1
- package/lib/typescript/module/src/state/AuthStateMachine.d.ts.map +1 -1
- package/lib/typescript/module/src/state/DeviceStateMachine.d.ts.map +1 -1
- package/lib/typescript/module/src/state/PushStateMachine.d.ts.map +1 -1
- package/lib/typescript/module/src/utils/FirebaseNotificationManager.d.ts.map +1 -1
- package/lib/typescript/module/src/utils/Logger.d.ts.map +1 -1
- package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts +13 -0
- package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts.map +1 -1
- package/lib/typescript/module/src/version.d.ts +1 -1
- package/package.json +7 -10
- package/react-native.config.json +12 -0
- package/scripts/setup-ios-extension.js +100 -20
- package/scripts/test-carousel-push.js +266 -0
- package/swan-react-native-sdk.podspec +18 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = {prop, fallback ->
|
|
3
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
repositories {
|
|
6
|
+
google()
|
|
7
|
+
mavenCentral()
|
|
8
|
+
}
|
|
9
|
+
dependencies {
|
|
10
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '1.9.22')}")
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
apply plugin: 'com.android.library'
|
|
15
|
+
apply plugin: 'kotlin-android'
|
|
16
|
+
|
|
17
|
+
android {
|
|
18
|
+
namespace 'com.loyalytics.swan'
|
|
19
|
+
compileSdkVersion safeExtGet('compileSdkVersion', 34)
|
|
20
|
+
|
|
21
|
+
defaultConfig {
|
|
22
|
+
minSdkVersion safeExtGet('minSdkVersion', 21)
|
|
23
|
+
targetSdkVersion safeExtGet('targetSdkVersion', 34)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
buildTypes {
|
|
27
|
+
release {
|
|
28
|
+
minifyEnabled false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def javaVersion = safeExtGet('jvmTargetVersion', '17')
|
|
33
|
+
|
|
34
|
+
compileOptions {
|
|
35
|
+
sourceCompatibility JavaVersion.toVersion(javaVersion)
|
|
36
|
+
targetCompatibility JavaVersion.toVersion(javaVersion)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
kotlinOptions {
|
|
40
|
+
jvmTarget = javaVersion
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
sourceSets {
|
|
44
|
+
main {
|
|
45
|
+
java.srcDirs += 'src/main/kotlin'
|
|
46
|
+
}
|
|
47
|
+
test {
|
|
48
|
+
java.srcDirs += 'src/test/kotlin'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
repositories {
|
|
54
|
+
google()
|
|
55
|
+
mavenCentral()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
dependencies {
|
|
59
|
+
implementation "com.facebook.react:react-native:+"
|
|
60
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:${safeExtGet('kotlinVersion', '1.9.22')}"
|
|
61
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
|
62
|
+
implementation "androidx.core:core-ktx:1.12.0"
|
|
63
|
+
|
|
64
|
+
testImplementation "junit:junit:4.13.2"
|
|
65
|
+
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
|
|
66
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package com.loyalytics.swan
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.*
|
|
4
|
+
import com.loyalytics.swan.templates.SwanTemplateRegistry
|
|
5
|
+
import com.loyalytics.swan.templates.carousel.CarouselTemplate
|
|
6
|
+
import kotlinx.coroutines.CoroutineScope
|
|
7
|
+
import kotlinx.coroutines.Dispatchers
|
|
8
|
+
import kotlinx.coroutines.launch
|
|
9
|
+
|
|
10
|
+
class SwanNotificationModule(reactContext: ReactApplicationContext) :
|
|
11
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
12
|
+
|
|
13
|
+
override fun getName(): String = "SwanNotificationModule"
|
|
14
|
+
|
|
15
|
+
init {
|
|
16
|
+
// Register built-in templates
|
|
17
|
+
SwanTemplateRegistry.register(CarouselTemplate())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@ReactMethod
|
|
21
|
+
fun displayTemplateNotification(config: ReadableMap, promise: Promise) {
|
|
22
|
+
val type = config.getString("notificationType")
|
|
23
|
+
if (type == null) {
|
|
24
|
+
promise.reject("MISSING_TYPE", "notificationType is required")
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
val template = SwanTemplateRegistry.findTemplate(type)
|
|
29
|
+
if (template == null) {
|
|
30
|
+
promise.reject("UNSUPPORTED_TYPE", "No template registered for: $type")
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
CoroutineScope(Dispatchers.IO).launch {
|
|
35
|
+
try {
|
|
36
|
+
template.display(reactApplicationContext, config)
|
|
37
|
+
promise.resolve(true)
|
|
38
|
+
} catch (e: Exception) {
|
|
39
|
+
promise.reject("DISPLAY_ERROR", "Template display failed: ${e.message}", e)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.loyalytics.swan
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class SwanNotificationPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
|
+
return listOf(SwanNotificationModule(reactContext))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
14
|
+
return emptyList()
|
|
15
|
+
}
|
|
16
|
+
}
|
package/android/src/main/kotlin/com/loyalytics/swan/templates/SwanNotificationActionReceiver.kt
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package com.loyalytics.swan.templates
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.os.Bundle
|
|
7
|
+
import android.util.Log
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Single BroadcastReceiver for all template notification actions.
|
|
11
|
+
* Routes actions to the correct template based on templateType extra.
|
|
12
|
+
*
|
|
13
|
+
* Intent extras:
|
|
14
|
+
* - templateType: String — which template to route to (e.g., "carousel")
|
|
15
|
+
* - templateAction: String — the action to perform (e.g., "NEXT", "PREV", "CLICK")
|
|
16
|
+
* - notificationId: Int — the notification ID for updating in-place
|
|
17
|
+
* - Additional extras specific to each template
|
|
18
|
+
*/
|
|
19
|
+
class SwanNotificationActionReceiver : BroadcastReceiver() {
|
|
20
|
+
companion object {
|
|
21
|
+
private const val TAG = "SwanActionReceiver"
|
|
22
|
+
const val EXTRA_TEMPLATE_TYPE = "templateType"
|
|
23
|
+
const val EXTRA_TEMPLATE_ACTION = "templateAction"
|
|
24
|
+
const val EXTRA_NOTIFICATION_ID = "notificationId"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
28
|
+
val templateType = intent.getStringExtra(EXTRA_TEMPLATE_TYPE)
|
|
29
|
+
val action = intent.getStringExtra(EXTRA_TEMPLATE_ACTION)
|
|
30
|
+
|
|
31
|
+
if (templateType == null || action == null) {
|
|
32
|
+
Log.w(TAG, "Missing templateType or templateAction in intent")
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Log.d(TAG, "Received action: $action for template: $templateType")
|
|
37
|
+
|
|
38
|
+
// Ensure templates are registered (covers process restart scenario)
|
|
39
|
+
SwanTemplateRegistry.ensureDefaults()
|
|
40
|
+
|
|
41
|
+
val template = SwanTemplateRegistry.findTemplate(templateType)
|
|
42
|
+
if (template == null) {
|
|
43
|
+
Log.w(TAG, "No template registered for type: $templateType")
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
template.handleAction(context, action, intent.extras ?: Bundle.EMPTY)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package com.loyalytics.swan.templates
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Bundle
|
|
5
|
+
import com.facebook.react.bridge.ReadableMap
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Interface for notification template handlers.
|
|
9
|
+
* Each notification type (carousel, timer, CTA) implements this.
|
|
10
|
+
*/
|
|
11
|
+
interface SwanNotificationTemplate {
|
|
12
|
+
/** Whether this template handles the given notificationType */
|
|
13
|
+
fun canHandle(notificationType: String): Boolean
|
|
14
|
+
|
|
15
|
+
/** Display the notification using custom RemoteViews */
|
|
16
|
+
suspend fun display(context: Context, config: ReadableMap)
|
|
17
|
+
|
|
18
|
+
/** Handle a user action (button press, swipe, etc.) routed from BroadcastReceiver */
|
|
19
|
+
fun handleAction(context: Context, action: String, extras: Bundle)
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package com.loyalytics.swan.templates
|
|
2
|
+
|
|
3
|
+
import com.loyalytics.swan.templates.carousel.CarouselTemplate
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Registry of notification template handlers.
|
|
7
|
+
* Templates register once at module init time.
|
|
8
|
+
* The BroadcastReceiver and bridge module look up templates here.
|
|
9
|
+
*
|
|
10
|
+
* ensureDefaults() is called by the BroadcastReceiver to guarantee
|
|
11
|
+
* templates are available even after a process restart (when the
|
|
12
|
+
* React Native module init may not have run yet).
|
|
13
|
+
*/
|
|
14
|
+
object SwanTemplateRegistry {
|
|
15
|
+
private val templates = mutableListOf<SwanNotificationTemplate>()
|
|
16
|
+
@Volatile
|
|
17
|
+
private var defaultsRegistered = false
|
|
18
|
+
|
|
19
|
+
fun register(template: SwanNotificationTemplate) {
|
|
20
|
+
// Remove existing handler for the same type to allow override
|
|
21
|
+
templates.removeAll { it::class == template::class }
|
|
22
|
+
templates.add(template)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ensure built-in templates are registered.
|
|
27
|
+
* Safe to call multiple times — only registers once.
|
|
28
|
+
*/
|
|
29
|
+
fun ensureDefaults() {
|
|
30
|
+
if (!defaultsRegistered) {
|
|
31
|
+
if (templates.none { it.canHandle("carousel") }) {
|
|
32
|
+
templates.add(CarouselTemplate())
|
|
33
|
+
}
|
|
34
|
+
defaultsRegistered = true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fun findTemplate(notificationType: String): SwanNotificationTemplate? {
|
|
39
|
+
return templates.find { it.canHandle(notificationType) }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Visible for testing — clears all registered templates */
|
|
43
|
+
internal fun clear() {
|
|
44
|
+
templates.clear()
|
|
45
|
+
defaultsRegistered = false
|
|
46
|
+
}
|
|
47
|
+
}
|
package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselAutoRemoteViews.kt
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
package com.loyalytics.swan.templates.carousel
|
|
2
|
+
|
|
3
|
+
import android.app.PendingIntent
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.graphics.Bitmap
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.os.Bundle
|
|
9
|
+
import android.widget.RemoteViews
|
|
10
|
+
import com.loyalytics.swan.R
|
|
11
|
+
import com.loyalytics.swan.templates.SwanNotificationActionReceiver
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builds RemoteViews for auto-carousel using ViewFlipper.
|
|
15
|
+
*
|
|
16
|
+
* All images are added as children to the ViewFlipper which handles
|
|
17
|
+
* cycling natively with slide animations. No AlarmManager needed.
|
|
18
|
+
*
|
|
19
|
+
* The entire flipper area gets a single click action (auto-carousel
|
|
20
|
+
* uses one deep link for the whole notification, same as CleverTap).
|
|
21
|
+
*
|
|
22
|
+
* Capped at MAX_FLIPPER_IMAGES to stay within RemoteViews ~1MB limit.
|
|
23
|
+
*/
|
|
24
|
+
object CarouselAutoRemoteViews {
|
|
25
|
+
|
|
26
|
+
private const val MAX_FLIPPER_IMAGES = 5
|
|
27
|
+
|
|
28
|
+
data class AutoCarouselViews(
|
|
29
|
+
val expanded: RemoteViews,
|
|
30
|
+
val collapsed: RemoteViews
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
fun build(
|
|
34
|
+
context: Context,
|
|
35
|
+
notificationId: Int,
|
|
36
|
+
title: String,
|
|
37
|
+
body: String,
|
|
38
|
+
bitmaps: List<Bitmap?>,
|
|
39
|
+
intervalMs: Int,
|
|
40
|
+
messageId: String,
|
|
41
|
+
defaultRoute: String
|
|
42
|
+
): AutoCarouselViews {
|
|
43
|
+
val packageName = context.packageName
|
|
44
|
+
|
|
45
|
+
val expanded = RemoteViews(packageName, R.layout.swan_carousel_auto_expanded).apply {
|
|
46
|
+
setTextViewText(R.id.swan_carousel_auto_title, title)
|
|
47
|
+
setTextViewText(R.id.swan_carousel_auto_body, body)
|
|
48
|
+
|
|
49
|
+
// autoStart and flipInterval are set in XML (RemoteViews doesn't allow setAutoStart)
|
|
50
|
+
|
|
51
|
+
// Add each bitmap as a child of the ViewFlipper
|
|
52
|
+
removeAllViews(R.id.swan_carousel_flipper)
|
|
53
|
+
val validBitmaps = bitmaps.filterNotNull().take(MAX_FLIPPER_IMAGES)
|
|
54
|
+
for (bitmap in validBitmaps) {
|
|
55
|
+
val childView = RemoteViews(packageName, R.layout.swan_carousel_flipper_item)
|
|
56
|
+
childView.setImageViewBitmap(R.id.swan_carousel_flipper_image, bitmap)
|
|
57
|
+
addView(R.id.swan_carousel_flipper, childView)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Single click action for the whole flipper
|
|
61
|
+
setOnClickPendingIntent(
|
|
62
|
+
R.id.swan_carousel_flipper,
|
|
63
|
+
createClickPendingIntent(
|
|
64
|
+
context, notificationId, messageId, defaultRoute
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
val collapsed = RemoteViews(packageName, R.layout.swan_carousel_collapsed).apply {
|
|
70
|
+
setTextViewText(R.id.swan_carousel_collapsed_title, title)
|
|
71
|
+
setTextViewText(R.id.swan_carousel_collapsed_body, body)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return AutoCarouselViews(expanded, collapsed)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private fun createClickPendingIntent(
|
|
78
|
+
context: Context,
|
|
79
|
+
notificationId: Int,
|
|
80
|
+
messageId: String,
|
|
81
|
+
route: String
|
|
82
|
+
): PendingIntent {
|
|
83
|
+
val intent = Intent(context, SwanNotificationActionReceiver::class.java).apply {
|
|
84
|
+
action = "com.loyalytics.swan.${CarouselTemplate.ACTION_CLICK}_$notificationId"
|
|
85
|
+
putExtra(SwanNotificationActionReceiver.EXTRA_TEMPLATE_TYPE, CarouselTemplate.TYPE)
|
|
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
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
package com.loyalytics.swan.templates.carousel
|
|
2
|
+
|
|
3
|
+
import android.app.PendingIntent
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.graphics.Bitmap
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.view.View
|
|
9
|
+
import android.widget.RemoteViews
|
|
10
|
+
import com.loyalytics.swan.R
|
|
11
|
+
import com.loyalytics.swan.templates.SwanNotificationActionReceiver
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builds RemoteViews for filmstrip carousel variant.
|
|
15
|
+
*
|
|
16
|
+
* Shows 3 images horizontally: left preview (dimmed) | center (full) | right preview (dimmed).
|
|
17
|
+
* Clicking side images navigates prev/next. Center click opens deep link.
|
|
18
|
+
*/
|
|
19
|
+
object CarouselFilmstripRemoteViews {
|
|
20
|
+
|
|
21
|
+
data class FilmstripViews(
|
|
22
|
+
val expanded: RemoteViews,
|
|
23
|
+
val collapsed: RemoteViews
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
fun build(
|
|
27
|
+
context: Context,
|
|
28
|
+
notificationId: Int,
|
|
29
|
+
title: String,
|
|
30
|
+
itemTitle: String,
|
|
31
|
+
itemBody: String,
|
|
32
|
+
leftBitmap: Bitmap?,
|
|
33
|
+
centerBitmap: Bitmap?,
|
|
34
|
+
rightBitmap: Bitmap?,
|
|
35
|
+
currentIndex: Int,
|
|
36
|
+
totalItems: Int,
|
|
37
|
+
messageId: String,
|
|
38
|
+
itemRoute: String
|
|
39
|
+
): FilmstripViews {
|
|
40
|
+
val packageName = context.packageName
|
|
41
|
+
|
|
42
|
+
val expanded = RemoteViews(packageName, R.layout.swan_carousel_filmstrip_expanded).apply {
|
|
43
|
+
setTextViewText(R.id.swan_filmstrip_title, title)
|
|
44
|
+
setTextViewText(R.id.swan_filmstrip_item_title, itemTitle)
|
|
45
|
+
setTextViewText(R.id.swan_filmstrip_item_body, itemBody)
|
|
46
|
+
setTextViewText(R.id.swan_filmstrip_counter, "${currentIndex + 1} / $totalItems")
|
|
47
|
+
|
|
48
|
+
// Left preview image
|
|
49
|
+
if (leftBitmap != null) {
|
|
50
|
+
setImageViewBitmap(R.id.swan_filmstrip_left, leftBitmap)
|
|
51
|
+
setViewVisibility(R.id.swan_filmstrip_left, View.VISIBLE)
|
|
52
|
+
setOnClickPendingIntent(
|
|
53
|
+
R.id.swan_filmstrip_left,
|
|
54
|
+
createCarouselActionIntent(
|
|
55
|
+
context, notificationId, CarouselTemplate.ACTION_PREV,
|
|
56
|
+
messageId, currentIndex, itemRoute
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
} else {
|
|
60
|
+
setViewVisibility(R.id.swan_filmstrip_left, View.INVISIBLE)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Center image
|
|
64
|
+
if (centerBitmap != null) {
|
|
65
|
+
setImageViewBitmap(R.id.swan_filmstrip_center, centerBitmap)
|
|
66
|
+
setViewVisibility(R.id.swan_filmstrip_center, View.VISIBLE)
|
|
67
|
+
} else {
|
|
68
|
+
setViewVisibility(R.id.swan_filmstrip_center, View.GONE)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Center click → open deep link
|
|
72
|
+
setOnClickPendingIntent(
|
|
73
|
+
R.id.swan_filmstrip_center,
|
|
74
|
+
createCarouselActionIntent(
|
|
75
|
+
context, notificationId, CarouselTemplate.ACTION_CLICK,
|
|
76
|
+
messageId, currentIndex, itemRoute
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
// Right preview image
|
|
81
|
+
if (rightBitmap != null) {
|
|
82
|
+
setImageViewBitmap(R.id.swan_filmstrip_right, rightBitmap)
|
|
83
|
+
setViewVisibility(R.id.swan_filmstrip_right, View.VISIBLE)
|
|
84
|
+
setOnClickPendingIntent(
|
|
85
|
+
R.id.swan_filmstrip_right,
|
|
86
|
+
createCarouselActionIntent(
|
|
87
|
+
context, notificationId, CarouselTemplate.ACTION_NEXT,
|
|
88
|
+
messageId, currentIndex, itemRoute
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
} else {
|
|
92
|
+
setViewVisibility(R.id.swan_filmstrip_right, View.INVISIBLE)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
val collapsed = RemoteViews(packageName, R.layout.swan_carousel_collapsed).apply {
|
|
97
|
+
setTextViewText(R.id.swan_carousel_collapsed_title, title)
|
|
98
|
+
setTextViewText(R.id.swan_carousel_collapsed_body, itemTitle)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return FilmstripViews(expanded, collapsed)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private fun createCarouselActionIntent(
|
|
105
|
+
context: Context,
|
|
106
|
+
notificationId: Int,
|
|
107
|
+
action: String,
|
|
108
|
+
messageId: String,
|
|
109
|
+
itemIndex: Int,
|
|
110
|
+
itemRoute: String
|
|
111
|
+
): PendingIntent {
|
|
112
|
+
val intent = Intent(context, SwanNotificationActionReceiver::class.java).apply {
|
|
113
|
+
this.action = "com.loyalytics.swan.${action}_$notificationId"
|
|
114
|
+
putExtra(SwanNotificationActionReceiver.EXTRA_TEMPLATE_TYPE, CarouselTemplate.TYPE)
|
|
115
|
+
putExtra(SwanNotificationActionReceiver.EXTRA_TEMPLATE_ACTION, action)
|
|
116
|
+
putExtra(SwanNotificationActionReceiver.EXTRA_NOTIFICATION_ID, notificationId)
|
|
117
|
+
putExtra(CarouselTemplate.EXTRA_MESSAGE_ID, messageId)
|
|
118
|
+
putExtra(CarouselTemplate.EXTRA_ITEM_INDEX, itemIndex)
|
|
119
|
+
putExtra(CarouselTemplate.EXTRA_ITEM_ROUTE, itemRoute)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
123
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
124
|
+
} else {
|
|
125
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return PendingIntent.getBroadcast(
|
|
129
|
+
context, "$notificationId$action$itemIndex".hashCode(), intent, flags
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
package com.loyalytics.swan.templates.carousel
|
|
2
|
+
|
|
3
|
+
import android.app.PendingIntent
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.graphics.Bitmap
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.view.View
|
|
9
|
+
import android.widget.RemoteViews
|
|
10
|
+
import com.loyalytics.swan.R
|
|
11
|
+
import com.loyalytics.swan.templates.SwanNotificationActionReceiver
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builds RemoteViews for the standard manual carousel.
|
|
15
|
+
*
|
|
16
|
+
* Uses a single ViewFlipper with all images pre-loaded.
|
|
17
|
+
* Navigation uses showNext()/showPrevious() with fade animation.
|
|
18
|
+
*/
|
|
19
|
+
object CarouselRemoteViews {
|
|
20
|
+
|
|
21
|
+
data class CarouselViews(
|
|
22
|
+
val expanded: RemoteViews,
|
|
23
|
+
val collapsed: RemoteViews
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
fun build(
|
|
27
|
+
context: Context,
|
|
28
|
+
notificationId: Int,
|
|
29
|
+
title: String,
|
|
30
|
+
body: String,
|
|
31
|
+
bitmaps: List<Bitmap?>,
|
|
32
|
+
currentIndex: Int,
|
|
33
|
+
totalItems: Int,
|
|
34
|
+
messageId: String,
|
|
35
|
+
itemRoute: String
|
|
36
|
+
): CarouselViews {
|
|
37
|
+
val packageName = context.packageName
|
|
38
|
+
|
|
39
|
+
val expanded = RemoteViews(packageName, R.layout.swan_carousel_expanded).apply {
|
|
40
|
+
setTextViewText(R.id.swan_carousel_title, title)
|
|
41
|
+
setTextViewText(R.id.swan_carousel_body, body)
|
|
42
|
+
setTextViewText(R.id.swan_carousel_counter, "${currentIndex + 1} / $totalItems")
|
|
43
|
+
|
|
44
|
+
// Populate ViewFlipper with all images
|
|
45
|
+
removeAllViews(R.id.swan_carousel_center)
|
|
46
|
+
for (bitmap in bitmaps) {
|
|
47
|
+
val child = RemoteViews(packageName, R.layout.swan_carousel_flipper_item)
|
|
48
|
+
if (bitmap != null) {
|
|
49
|
+
child.setImageViewBitmap(R.id.swan_carousel_flipper_image, bitmap)
|
|
50
|
+
}
|
|
51
|
+
addView(R.id.swan_carousel_center, child)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Advance ViewFlipper to currentIndex
|
|
55
|
+
for (i in 0 until currentIndex) {
|
|
56
|
+
showNext(R.id.swan_carousel_center)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Show/hide arrows
|
|
60
|
+
if (totalItems > 1) {
|
|
61
|
+
setViewVisibility(R.id.swan_carousel_btn_prev, View.VISIBLE)
|
|
62
|
+
setViewVisibility(R.id.swan_carousel_btn_next, View.VISIBLE)
|
|
63
|
+
|
|
64
|
+
setOnClickPendingIntent(
|
|
65
|
+
R.id.swan_carousel_btn_prev,
|
|
66
|
+
createActionIntent(
|
|
67
|
+
context, notificationId, CarouselTemplate.ACTION_PREV,
|
|
68
|
+
messageId, currentIndex, itemRoute
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
setOnClickPendingIntent(
|
|
72
|
+
R.id.swan_carousel_btn_next,
|
|
73
|
+
createActionIntent(
|
|
74
|
+
context, notificationId, CarouselTemplate.ACTION_NEXT,
|
|
75
|
+
messageId, currentIndex, itemRoute
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
} else {
|
|
79
|
+
setViewVisibility(R.id.swan_carousel_btn_prev, View.GONE)
|
|
80
|
+
setViewVisibility(R.id.swan_carousel_btn_next, View.GONE)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Image click → open deep link
|
|
84
|
+
setOnClickPendingIntent(
|
|
85
|
+
R.id.swan_carousel_center,
|
|
86
|
+
createActionIntent(
|
|
87
|
+
context, notificationId, CarouselTemplate.ACTION_CLICK,
|
|
88
|
+
messageId, currentIndex, itemRoute
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
val collapsed = RemoteViews(packageName, R.layout.swan_carousel_collapsed).apply {
|
|
94
|
+
setTextViewText(R.id.swan_carousel_collapsed_title, title)
|
|
95
|
+
setTextViewText(R.id.swan_carousel_collapsed_body, body)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return CarouselViews(expanded, collapsed)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private fun createActionIntent(
|
|
102
|
+
context: Context,
|
|
103
|
+
notificationId: Int,
|
|
104
|
+
action: String,
|
|
105
|
+
messageId: String,
|
|
106
|
+
itemIndex: Int,
|
|
107
|
+
itemRoute: String
|
|
108
|
+
): PendingIntent {
|
|
109
|
+
val intent = Intent(context, SwanNotificationActionReceiver::class.java).apply {
|
|
110
|
+
this.action = "com.loyalytics.swan.${action}_$notificationId"
|
|
111
|
+
putExtra(SwanNotificationActionReceiver.EXTRA_TEMPLATE_TYPE, CarouselTemplate.TYPE)
|
|
112
|
+
putExtra(SwanNotificationActionReceiver.EXTRA_TEMPLATE_ACTION, action)
|
|
113
|
+
putExtra(SwanNotificationActionReceiver.EXTRA_NOTIFICATION_ID, notificationId)
|
|
114
|
+
putExtra(CarouselTemplate.EXTRA_MESSAGE_ID, messageId)
|
|
115
|
+
putExtra(CarouselTemplate.EXTRA_ITEM_INDEX, itemIndex)
|
|
116
|
+
putExtra(CarouselTemplate.EXTRA_ITEM_ROUTE, itemRoute)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
120
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
121
|
+
} else {
|
|
122
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return PendingIntent.getBroadcast(
|
|
126
|
+
context, "$notificationId$action$itemIndex".hashCode(), intent, flags
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|