@loyalytics/swan-react-native-sdk 2.5.1-beta.0 → 2.5.1-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.
@@ -7,6 +7,7 @@ buildscript {
7
7
  mavenCentral()
8
8
  }
9
9
  dependencies {
10
+ classpath("com.android.tools.build:gradle:${safeExtGet('androidGradlePluginVersion', '8.2.2')}")
10
11
  classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '1.9.22')}")
11
12
  }
12
13
  }
@@ -19,7 +20,7 @@ android {
19
20
  compileSdkVersion safeExtGet('compileSdkVersion', 34)
20
21
 
21
22
  defaultConfig {
22
- minSdkVersion safeExtGet('minSdkVersion', 21)
23
+ minSdkVersion safeExtGet('minSdkVersion', 24)
23
24
  targetSdkVersion safeExtGet('targetSdkVersion', 34)
24
25
  }
25
26
 
@@ -29,6 +30,12 @@ android {
29
30
  }
30
31
  }
31
32
 
33
+ testOptions {
34
+ unitTests {
35
+ includeAndroidResources = true
36
+ }
37
+ }
38
+
32
39
  def javaVersion = safeExtGet('jvmTargetVersion', '17')
33
40
 
34
41
  compileOptions {
@@ -55,6 +62,18 @@ repositories {
55
62
  mavenCentral()
56
63
  }
57
64
 
65
+ // For standalone builds (CI, local `./gradlew test`), react-native:+ can't resolve
66
+ // because the host app's Maven repo isn't available. Substitute with react-android
67
+ // from Maven Central. When consumed by an app, rootProject.name != 'swan-android'
68
+ // so this block is skipped and the app's own dependency resolution applies.
69
+ if (rootProject.name == 'swan-android') {
70
+ configurations.all {
71
+ resolutionStrategy.dependencySubstitution {
72
+ substitute module('com.facebook.react:react-native') using module('com.facebook.react:react-android:0.76.2')
73
+ }
74
+ }
75
+ }
76
+
58
77
  dependencies {
59
78
  implementation "com.facebook.react:react-native:+"
60
79
  implementation "org.jetbrains.kotlin:kotlin-stdlib:${safeExtGet('kotlinVersion', '1.9.22')}"
@@ -63,4 +82,7 @@ dependencies {
63
82
 
64
83
  testImplementation "junit:junit:4.13.2"
65
84
  testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
85
+ testImplementation "org.robolectric:robolectric:4.14.1"
86
+ testImplementation "androidx.test:core:1.6.1"
87
+ testImplementation "org.json:json:20231013"
66
88
  }
@@ -0,0 +1 @@
1
+ android.useAndroidX=true
@@ -0,0 +1 @@
1
+ rootProject.name = 'swan-android'
@@ -5,6 +5,7 @@ import android.content.Context
5
5
  import android.graphics.Bitmap
6
6
  import android.widget.RemoteViews
7
7
  import com.loyalytics.swan.R
8
+ import org.json.JSONObject
8
9
 
9
10
  /**
10
11
  * Builds RemoteViews for auto-carousel using ViewFlipper.
@@ -12,6 +13,9 @@ import com.loyalytics.swan.R
12
13
  * All images are added as children to the ViewFlipper which handles
13
14
  * cycling natively with slide animations. No AlarmManager needed.
14
15
  *
16
+ * Each child embeds per-item title and body text alongside the image,
17
+ * so text rotates together with the corresponding image.
18
+ *
15
19
  * The entire flipper area gets a single click action (auto-carousel
16
20
  * uses one deep link for the whole notification, same as CleverTap).
17
21
  *
@@ -31,6 +35,7 @@ object CarouselAutoRemoteViews {
31
35
  notificationId: Int,
32
36
  title: String,
33
37
  body: String,
38
+ items: List<JSONObject>,
34
39
  bitmaps: List<Bitmap?>,
35
40
  intervalMs: Int,
36
41
  messageId: String,
@@ -44,12 +49,16 @@ object CarouselAutoRemoteViews {
44
49
 
45
50
  // autoStart and flipInterval are set in XML (RemoteViews doesn't allow setAutoStart)
46
51
 
47
- // Add each bitmap as a child of the ViewFlipper
52
+ // Add each item as a child of the ViewFlipper with image + title + body
48
53
  removeAllViews(R.id.swan_carousel_flipper)
49
- val validBitmaps = bitmaps.filterNotNull().take(MAX_FLIPPER_IMAGES)
50
- for (bitmap in validBitmaps) {
51
- val childView = RemoteViews(packageName, R.layout.swan_carousel_flipper_item)
52
- childView.setImageViewBitmap(R.id.swan_carousel_flipper_image, bitmap)
54
+ val limit = minOf(items.size, bitmaps.size, MAX_FLIPPER_IMAGES)
55
+ for (i in 0 until limit) {
56
+ val bitmap = bitmaps[i] ?: continue
57
+ val item = items[i]
58
+ val childView = RemoteViews(packageName, R.layout.swan_carousel_auto_flipper_item)
59
+ childView.setImageViewBitmap(R.id.swan_carousel_auto_flipper_image, bitmap)
60
+ childView.setTextViewText(R.id.swan_carousel_auto_flipper_title, item.optString("title", ""))
61
+ childView.setTextViewText(R.id.swan_carousel_auto_flipper_body, item.optString("body", ""))
53
62
  addView(R.id.swan_carousel_flipper, childView)
54
63
  }
55
64
 
@@ -62,9 +71,10 @@ object CarouselAutoRemoteViews {
62
71
  )
63
72
  }
64
73
 
74
+ val firstItemTitle = items.firstOrNull()?.optString("title", "") ?: ""
65
75
  val collapsed = RemoteViews(packageName, R.layout.swan_carousel_collapsed).apply {
66
76
  setTextViewText(R.id.swan_carousel_collapsed_title, title)
67
- setTextViewText(R.id.swan_carousel_collapsed_body, body)
77
+ setTextViewText(R.id.swan_carousel_collapsed_body, firstItemTitle.ifEmpty { body })
68
78
  }
69
79
 
70
80
  return AutoCarouselViews(expanded, collapsed)
@@ -28,6 +28,8 @@ object CarouselRemoteViews {
28
28
  notificationId: Int,
29
29
  title: String,
30
30
  body: String,
31
+ itemTitle: String,
32
+ itemBody: String,
31
33
  bitmaps: List<Bitmap?>,
32
34
  currentIndex: Int,
33
35
  totalItems: Int,
@@ -39,6 +41,8 @@ object CarouselRemoteViews {
39
41
  val expanded = RemoteViews(packageName, R.layout.swan_carousel_expanded).apply {
40
42
  setTextViewText(R.id.swan_carousel_title, title)
41
43
  setTextViewText(R.id.swan_carousel_body, body)
44
+ setTextViewText(R.id.swan_carousel_item_title, itemTitle)
45
+ setTextViewText(R.id.swan_carousel_item_body, itemBody)
42
46
  setTextViewText(R.id.swan_carousel_counter, "${currentIndex + 1} / $totalItems")
43
47
 
44
48
  // Populate ViewFlipper with all images
@@ -92,7 +96,7 @@ object CarouselRemoteViews {
92
96
 
93
97
  val collapsed = RemoteViews(packageName, R.layout.swan_carousel_collapsed).apply {
94
98
  setTextViewText(R.id.swan_carousel_collapsed_title, title)
95
- setTextViewText(R.id.swan_carousel_collapsed_body, body)
99
+ setTextViewText(R.id.swan_carousel_collapsed_body, itemTitle.ifEmpty { body })
96
100
  }
97
101
 
98
102
  return CarouselViews(expanded, collapsed)
@@ -84,7 +84,8 @@ class CarouselTemplate : SwanNotificationTemplate {
84
84
  notificationId: Int,
85
85
  messageId: String,
86
86
  itemIndex: Int,
87
- itemRoute: String
87
+ itemRoute: String,
88
+ isContentIntent: Boolean = false
88
89
  ): PendingIntent {
89
90
  val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
90
91
  ?: Intent(Intent.ACTION_MAIN).apply {
@@ -107,8 +108,15 @@ class CarouselTemplate : SwanNotificationTemplate {
107
108
  PendingIntent.FLAG_UPDATE_CURRENT
108
109
  }
109
110
 
111
+ // Use different request codes for image click vs content intent to prevent collision
112
+ val requestCode = if (isContentIntent) {
113
+ "${notificationId}CONTENT_CLICK".hashCode()
114
+ } else {
115
+ "$notificationId${ACTION_CLICK}$itemIndex".hashCode()
116
+ }
117
+
110
118
  return PendingIntent.getActivity(
111
- context, "$notificationId${ACTION_CLICK}$itemIndex".hashCode(), launchIntent, flags
119
+ context, requestCode, launchIntent, flags
112
120
  )
113
121
  }
114
122
  }
@@ -321,7 +329,9 @@ class CarouselTemplate : SwanNotificationTemplate {
321
329
  context = context,
322
330
  notificationId = notificationId,
323
331
  title = title,
324
- body = currentItem.optString("title", body),
332
+ body = body,
333
+ itemTitle = currentItem.optString("title", ""),
334
+ itemBody = currentItem.optString("body", ""),
325
335
  bitmaps = scaledBitmaps,
326
336
  currentIndex = currentIndex,
327
337
  totalItems = items.size,
@@ -398,6 +408,7 @@ class CarouselTemplate : SwanNotificationTemplate {
398
408
  notificationId = notificationId,
399
409
  title = title,
400
410
  body = body,
411
+ items = items,
401
412
  bitmaps = scaledBitmaps,
402
413
  intervalMs = intervalMs,
403
414
  messageId = messageId,
@@ -444,13 +455,16 @@ class CarouselTemplate : SwanNotificationTemplate {
444
455
  extras: Bundle
445
456
  ): PendingIntent {
446
457
  // For CLICK, use PendingIntent.getActivity() to avoid Android 12+ trampoline restriction
458
+ // This is always the content intent (notification body tap) — image clicks go through
459
+ // createClickActivityPendingIntent() directly from RemoteViews builders
447
460
  if (action == ACTION_CLICK) {
448
461
  return createClickActivityPendingIntent(
449
462
  context,
450
463
  notificationId,
451
464
  extras.getString(EXTRA_MESSAGE_ID, ""),
452
465
  extras.getInt(EXTRA_ITEM_INDEX, 0),
453
- extras.getString(EXTRA_ITEM_ROUTE, "")
466
+ extras.getString(EXTRA_ITEM_ROUTE, ""),
467
+ isContentIntent = true
454
468
  )
455
469
  }
456
470
 
@@ -517,7 +531,9 @@ class CarouselTemplate : SwanNotificationTemplate {
517
531
  }
518
532
  else -> {
519
533
  expanded.setViewPadding(R.id.swan_carousel_header, px16, 0, px16, px8)
520
- expanded.setViewPadding(R.id.swan_carousel_counter, 0, px2, px16, px4)
534
+ expanded.setViewPadding(R.id.swan_carousel_item_title, px16, 0, px16, 0)
535
+ expanded.setViewPadding(R.id.swan_carousel_item_body, px16, 0, px16, 0)
536
+ expanded.setViewPadding(R.id.swan_carousel_counter, px16, 0, px16, 0)
521
537
  }
522
538
  }
523
539
  }
@@ -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="144dp"
44
+ android:layout_height="wrap_content"
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"
@@ -0,0 +1,33 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:layout_width="match_parent"
4
+ android:layout_height="wrap_content"
5
+ android:orientation="vertical">
6
+
7
+ <ImageView
8
+ android:id="@+id/swan_carousel_auto_flipper_image"
9
+ android:layout_width="match_parent"
10
+ android:layout_height="144dp"
11
+ android:scaleType="centerCrop"
12
+ android:contentDescription="Carousel image" />
13
+
14
+ <TextView
15
+ android:id="@+id/swan_carousel_auto_flipper_title"
16
+ android:layout_width="match_parent"
17
+ android:layout_height="wrap_content"
18
+ android:textSize="13sp"
19
+ android:textStyle="bold"
20
+ android:textColor="#DE000000"
21
+ android:maxLines="1"
22
+ android:ellipsize="end"
23
+ android:paddingTop="4dp" />
24
+
25
+ <TextView
26
+ android:id="@+id/swan_carousel_auto_flipper_body"
27
+ android:layout_width="match_parent"
28
+ android:layout_height="wrap_content"
29
+ android:textSize="12sp"
30
+ android:textColor="#8A000000"
31
+ android:maxLines="1"
32
+ android:ellipsize="end" />
33
+ </LinearLayout>
@@ -78,17 +78,51 @@
78
78
  android:contentDescription="Next" />
79
79
  </RelativeLayout>
80
80
 
81
- <!-- Counter -->
82
- <TextView
83
- android:id="@+id/swan_carousel_counter"
84
- android:layout_width="wrap_content"
81
+ <!-- Item title + body + counter -->
82
+ <LinearLayout
83
+ android:layout_width="match_parent"
85
84
  android:layout_height="wrap_content"
86
- android:layout_gravity="end"
87
- android:textSize="11sp"
88
- android:textColor="#8A000000"
85
+ android:orientation="horizontal"
89
86
  android:paddingStart="0dp"
90
87
  android:paddingEnd="0dp"
91
- android:paddingTop="2dp"
92
- android:paddingBottom="4dp" />
88
+ android:paddingTop="4dp"
89
+ android:paddingBottom="4dp"
90
+ android:gravity="center_vertical">
91
+
92
+ <LinearLayout
93
+ android:layout_width="0dp"
94
+ android:layout_height="wrap_content"
95
+ android:layout_weight="1"
96
+ android:orientation="vertical">
97
+
98
+ <TextView
99
+ android:id="@+id/swan_carousel_item_title"
100
+ android:layout_width="match_parent"
101
+ android:layout_height="wrap_content"
102
+ android:textSize="13sp"
103
+ android:textStyle="bold"
104
+ android:textColor="#DE000000"
105
+ android:maxLines="1"
106
+ android:ellipsize="end" />
107
+
108
+ <TextView
109
+ android:id="@+id/swan_carousel_item_body"
110
+ android:layout_width="match_parent"
111
+ android:layout_height="wrap_content"
112
+ android:textSize="12sp"
113
+ android:textColor="#8A000000"
114
+ android:maxLines="1"
115
+ android:ellipsize="end" />
116
+ </LinearLayout>
117
+
118
+ <TextView
119
+ android:id="@+id/swan_carousel_counter"
120
+ android:layout_width="wrap_content"
121
+ android:layout_height="wrap_content"
122
+ android:textSize="11sp"
123
+ android:textColor="#8A000000"
124
+ android:paddingStart="8dp"
125
+ android:paddingEnd="0dp" />
126
+ </LinearLayout>
93
127
 
94
128
  </LinearLayout>
@@ -8,5 +8,5 @@ exports.SDK_VERSION = void 0;
8
8
  // This file is generated from package.json version during build.
9
9
  // See scripts/generate-version.js
10
10
 
11
- const SDK_VERSION = exports.SDK_VERSION = '2.5.1-beta.0';
11
+ const SDK_VERSION = exports.SDK_VERSION = '2.5.1-beta.2';
12
12
  //# sourceMappingURL=version.js.map
@@ -4,5 +4,5 @@
4
4
  // This file is generated from package.json version during build.
5
5
  // See scripts/generate-version.js
6
6
 
7
- export const SDK_VERSION = '2.5.1-beta.0';
7
+ export const SDK_VERSION = '2.5.1-beta.2';
8
8
  //# sourceMappingURL=version.js.map
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "2.5.1-beta.0";
1
+ export declare const SDK_VERSION = "2.5.1-beta.2";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "2.5.1-beta.0";
1
+ export declare const SDK_VERSION = "2.5.1-beta.2";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loyalytics/swan-react-native-sdk",
3
- "version": "2.5.1-beta.0",
3
+ "version": "2.5.1-beta.2",
4
4
  "description": "React Native SDK for Swan",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/commonjs/index.js",