@loyalytics/swan-react-native-sdk 2.1.3-beta.0 → 2.1.3-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.
Files changed (89) hide show
  1. package/android/build.gradle +66 -0
  2. package/android/src/main/AndroidManifest.xml +10 -0
  3. package/android/src/main/kotlin/com/loyalytics/swan/SwanNotificationModule.kt +43 -0
  4. package/android/src/main/kotlin/com/loyalytics/swan/SwanNotificationPackage.kt +16 -0
  5. package/android/src/main/kotlin/com/loyalytics/swan/templates/SwanNotificationActionReceiver.kt +49 -0
  6. package/android/src/main/kotlin/com/loyalytics/swan/templates/SwanNotificationTemplate.kt +20 -0
  7. package/android/src/main/kotlin/com/loyalytics/swan/templates/SwanTemplateRegistry.kt +47 -0
  8. package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselAutoRemoteViews.kt +103 -0
  9. package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselFilmstripRemoteViews.kt +132 -0
  10. package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselRemoteViews.kt +129 -0
  11. package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselTemplate.kt +412 -0
  12. package/android/src/main/kotlin/com/loyalytics/swan/templates/common/NotificationBitmapCache.kt +70 -0
  13. package/android/src/main/kotlin/com/loyalytics/swan/templates/common/NotificationImageLoader.kt +97 -0
  14. package/android/src/main/kotlin/com/loyalytics/swan/templates/common/NotificationStateManager.kt +85 -0
  15. package/android/src/main/res/anim/swan_fade_in.xml +6 -0
  16. package/android/src/main/res/anim/swan_fade_out.xml +6 -0
  17. package/android/src/main/res/anim/swan_slide_in_right.xml +8 -0
  18. package/android/src/main/res/anim/swan_slide_out_left.xml +8 -0
  19. package/android/src/main/res/drawable/swan_ic_chevron_left.xml +11 -0
  20. package/android/src/main/res/drawable/swan_ic_chevron_right.xml +11 -0
  21. package/android/src/main/res/layout/swan_carousel_auto_expanded.xml +51 -0
  22. package/android/src/main/res/layout/swan_carousel_collapsed.xml +31 -0
  23. package/android/src/main/res/layout/swan_carousel_expanded.xml +96 -0
  24. package/android/src/main/res/layout/swan_carousel_filmstrip_expanded.xml +115 -0
  25. package/android/src/main/res/layout/swan_carousel_flipper_item.xml +7 -0
  26. package/android/src/test/kotlin/com/loyalytics/swan/templates/carousel/CarouselTemplateTest.kt +125 -0
  27. package/docs/SDK_INDUSTRY_REVIEW_REPORT.md +347 -0
  28. package/docs/Swan_Push_Notifications.postman_collection.json +330 -0
  29. package/docs/deep-link-attribution.md +281 -0
  30. package/ios/SwanNotificationContentExtension/Info.plist +40 -0
  31. package/ios/SwanNotificationContentExtension/MainInterface.storyboard +19 -0
  32. package/ios/SwanNotificationContentExtension/NotificationViewController.swift +190 -0
  33. package/ios/SwanNotificationContentExtension/SwanNotificationContentExtension.entitlements +10 -0
  34. package/ios/SwanNotificationContentExtension/common/ImageDownloader.swift +32 -0
  35. package/ios/SwanNotificationContentExtension/templates/CarouselView.swift +336 -0
  36. package/lib/commonjs/constants/ApiUrls.js.map +1 -1
  37. package/lib/commonjs/index.js +117 -35
  38. package/lib/commonjs/index.js.map +1 -1
  39. package/lib/commonjs/providers/NullPushProvider.js.map +1 -1
  40. package/lib/commonjs/services/DeviceRegistrationService.js.map +1 -1
  41. package/lib/commonjs/state/AuthStateMachine.js.map +1 -1
  42. package/lib/commonjs/state/DeviceStateMachine.js.map +1 -1
  43. package/lib/commonjs/state/PushStateMachine.js.map +1 -1
  44. package/lib/commonjs/utils/FirebaseNotificationManager.js.map +1 -1
  45. package/lib/commonjs/utils/Logger.js.map +1 -1
  46. package/lib/commonjs/utils/SharedCredentialsManager.js +28 -0
  47. package/lib/commonjs/utils/SharedCredentialsManager.js.map +1 -1
  48. package/lib/commonjs/version.js +1 -1
  49. package/lib/module/index.js +117 -35
  50. package/lib/module/index.js.map +1 -1
  51. package/lib/module/providers/NullPushProvider.js.map +1 -1
  52. package/lib/module/services/DeviceRegistrationService.js.map +1 -1
  53. package/lib/module/state/AuthStateMachine.js.map +1 -1
  54. package/lib/module/state/DeviceStateMachine.js.map +1 -1
  55. package/lib/module/state/PushStateMachine.js.map +1 -1
  56. package/lib/module/utils/FirebaseNotificationManager.js.map +1 -1
  57. package/lib/module/utils/Logger.js.map +1 -1
  58. package/lib/module/utils/SharedCredentialsManager.js +28 -0
  59. package/lib/module/utils/SharedCredentialsManager.js.map +1 -1
  60. package/lib/module/version.js +1 -1
  61. package/lib/typescript/commonjs/src/constants/ApiUrls.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/src/providers/NullPushProvider.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/src/services/DeviceRegistrationService.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/src/state/AuthStateMachine.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/src/state/DeviceStateMachine.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/src/state/PushStateMachine.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/src/utils/FirebaseNotificationManager.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/src/utils/Logger.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts +13 -0
  71. package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/src/version.d.ts +1 -1
  73. package/lib/typescript/module/src/constants/ApiUrls.d.ts.map +1 -1
  74. package/lib/typescript/module/src/index.d.ts.map +1 -1
  75. package/lib/typescript/module/src/providers/NullPushProvider.d.ts.map +1 -1
  76. package/lib/typescript/module/src/services/DeviceRegistrationService.d.ts.map +1 -1
  77. package/lib/typescript/module/src/state/AuthStateMachine.d.ts.map +1 -1
  78. package/lib/typescript/module/src/state/DeviceStateMachine.d.ts.map +1 -1
  79. package/lib/typescript/module/src/state/PushStateMachine.d.ts.map +1 -1
  80. package/lib/typescript/module/src/utils/FirebaseNotificationManager.d.ts.map +1 -1
  81. package/lib/typescript/module/src/utils/Logger.d.ts.map +1 -1
  82. package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts +13 -0
  83. package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts.map +1 -1
  84. package/lib/typescript/module/src/version.d.ts +1 -1
  85. package/package.json +7 -3
  86. package/react-native.config.json +12 -0
  87. package/scripts/setup-ios-extension.js +100 -20
  88. package/scripts/test-carousel-push.js +266 -0
  89. package/swan-react-native-sdk.podspec +18 -0
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <set xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <translate
4
+ android:fromXDelta="0"
5
+ android:toXDelta="-100%p"
6
+ android:duration="300"
7
+ android:interpolator="@android:anim/accelerate_interpolator" />
8
+ </set>
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:width="24dp"
4
+ android:height="24dp"
5
+ android:viewportWidth="24"
6
+ android:viewportHeight="24"
7
+ android:tint="#DE000000">
8
+ <path
9
+ android:fillColor="#FF000000"
10
+ android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z" />
11
+ </vector>
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:width="24dp"
4
+ android:height="24dp"
5
+ android:viewportWidth="24"
6
+ android:viewportHeight="24"
7
+ android:tint="#DE000000">
8
+ <path
9
+ android:fillColor="#FF000000"
10
+ android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z" />
11
+ </vector>
@@ -0,0 +1,51 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:id="@+id/swan_carousel_auto_big"
4
+ android:layout_width="match_parent"
5
+ android:layout_height="wrap_content"
6
+ android:background="@android:color/transparent">
7
+
8
+ <!-- Header -->
9
+ <RelativeLayout
10
+ android:id="@+id/swan_carousel_auto_header"
11
+ android:layout_width="match_parent"
12
+ android:layout_height="wrap_content"
13
+ android:paddingLeft="0dp"
14
+ android:paddingRight="0dp"
15
+ android:paddingTop="0dp"
16
+ android:paddingBottom="8dp">
17
+
18
+ <TextView
19
+ android:id="@+id/swan_carousel_auto_title"
20
+ android:layout_width="match_parent"
21
+ android:layout_height="wrap_content"
22
+ android:layout_marginTop="0dp"
23
+ android:textSize="14sp"
24
+ android:textStyle="bold"
25
+ android:textColor="#DE000000"
26
+ android:maxLines="1"
27
+ android:ellipsize="end" />
28
+
29
+ <TextView
30
+ android:id="@+id/swan_carousel_auto_body"
31
+ android:layout_width="match_parent"
32
+ android:layout_height="wrap_content"
33
+ android:layout_below="@id/swan_carousel_auto_title"
34
+ android:textSize="12sp"
35
+ android:textColor="#8A000000"
36
+ android:maxLines="2"
37
+ android:ellipsize="end" />
38
+ </RelativeLayout>
39
+
40
+ <!-- ViewFlipper auto-scrolls through images -->
41
+ <ViewFlipper
42
+ android:id="@+id/swan_carousel_flipper"
43
+ android:layout_width="match_parent"
44
+ android:layout_height="196dp"
45
+ android:layout_below="@id/swan_carousel_auto_header"
46
+ android:inAnimation="@anim/swan_slide_in_right"
47
+ android:outAnimation="@anim/swan_slide_out_left"
48
+ android:autoStart="true"
49
+ android:flipInterval="4000" />
50
+
51
+ </RelativeLayout>
@@ -0,0 +1,31 @@
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
+ android:paddingLeft="16dp"
7
+ android:paddingRight="16dp"
8
+ android:paddingTop="4dp"
9
+ android:paddingBottom="4dp">
10
+
11
+ <TextView
12
+ android:id="@+id/swan_carousel_collapsed_title"
13
+ android:layout_width="match_parent"
14
+ android:layout_height="wrap_content"
15
+ android:textSize="14sp"
16
+ android:textStyle="bold"
17
+ android:textColor="#DE000000"
18
+ android:maxLines="1"
19
+ android:ellipsize="end" />
20
+
21
+ <TextView
22
+ android:id="@+id/swan_carousel_collapsed_body"
23
+ android:layout_width="match_parent"
24
+ android:layout_height="wrap_content"
25
+ android:textSize="12sp"
26
+ android:textColor="#8A000000"
27
+ android:maxLines="1"
28
+ android:ellipsize="end"
29
+ android:paddingTop="2dp" />
30
+
31
+ </LinearLayout>
@@ -0,0 +1,96 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:id="@+id/swan_carousel_big"
4
+ android:layout_width="match_parent"
5
+ android:layout_height="wrap_content"
6
+ android:background="@android:color/transparent"
7
+ android:orientation="vertical">
8
+
9
+ <!-- Header: title + body -->
10
+ <RelativeLayout
11
+ android:id="@+id/swan_carousel_header"
12
+ android:layout_width="match_parent"
13
+ android:layout_height="wrap_content"
14
+ android:paddingStart="0dp"
15
+ android:paddingEnd="0dp"
16
+ android:paddingTop="0dp"
17
+ android:paddingBottom="8dp">
18
+
19
+ <TextView
20
+ android:id="@+id/swan_carousel_title"
21
+ android:layout_width="match_parent"
22
+ android:layout_height="wrap_content"
23
+ android:textSize="14sp"
24
+ android:textStyle="bold"
25
+ android:textColor="#DE000000"
26
+ android:maxLines="1"
27
+ android:ellipsize="end" />
28
+
29
+ <TextView
30
+ android:id="@+id/swan_carousel_body"
31
+ android:layout_width="match_parent"
32
+ android:layout_height="wrap_content"
33
+ android:layout_below="@id/swan_carousel_title"
34
+ android:textSize="12sp"
35
+ android:textColor="#8A000000"
36
+ android:maxLines="1"
37
+ android:ellipsize="end" />
38
+ </RelativeLayout>
39
+
40
+ <!-- Image area with ViewFlipper + arrow overlays -->
41
+ <RelativeLayout
42
+ android:id="@+id/swan_carousel_image_section"
43
+ android:layout_width="match_parent"
44
+ android:layout_height="0dp"
45
+ android:layout_weight="1"
46
+ android:minHeight="196dp">
47
+
48
+ <!-- Center ViewFlipper (all images pre-loaded) -->
49
+ <ViewFlipper
50
+ android:id="@+id/swan_carousel_center"
51
+ android:layout_width="match_parent"
52
+ android:layout_height="match_parent"
53
+ android:inAnimation="@anim/swan_fade_in"
54
+ android:outAnimation="@anim/swan_fade_out" />
55
+
56
+ <!-- Previous arrow -->
57
+ <ImageView
58
+ android:id="@+id/swan_carousel_btn_prev"
59
+ android:layout_width="36dp"
60
+ android:layout_height="36dp"
61
+ android:layout_alignParentStart="true"
62
+ android:layout_centerVertical="true"
63
+ android:layout_marginStart="4dp"
64
+ android:src="@drawable/swan_ic_chevron_left"
65
+ android:background="#80FFFFFF"
66
+ android:padding="6dp"
67
+ android:contentDescription="Previous" />
68
+
69
+ <!-- Next arrow -->
70
+ <ImageView
71
+ android:id="@+id/swan_carousel_btn_next"
72
+ android:layout_width="36dp"
73
+ android:layout_height="36dp"
74
+ android:layout_alignParentEnd="true"
75
+ android:layout_centerVertical="true"
76
+ android:layout_marginEnd="4dp"
77
+ android:src="@drawable/swan_ic_chevron_right"
78
+ android:background="#80FFFFFF"
79
+ android:padding="6dp"
80
+ android:contentDescription="Next" />
81
+ </RelativeLayout>
82
+
83
+ <!-- Counter -->
84
+ <TextView
85
+ android:id="@+id/swan_carousel_counter"
86
+ android:layout_width="wrap_content"
87
+ android:layout_height="wrap_content"
88
+ android:layout_gravity="end"
89
+ android:textSize="11sp"
90
+ android:textColor="#8A000000"
91
+ android:paddingStart="0dp"
92
+ android:paddingEnd="0dp"
93
+ android:paddingTop="2dp"
94
+ android:paddingBottom="4dp" />
95
+
96
+ </LinearLayout>
@@ -0,0 +1,115 @@
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
+ android:background="@android:color/transparent">
7
+
8
+ <!-- Notification title -->
9
+ <TextView
10
+ android:id="@+id/swan_filmstrip_title"
11
+ android:layout_width="match_parent"
12
+ android:layout_height="wrap_content"
13
+ android:textSize="14sp"
14
+ android:textStyle="bold"
15
+ android:textColor="#DE000000"
16
+ android:paddingStart="0dp"
17
+ android:paddingEnd="0dp"
18
+ android:paddingTop="0dp"
19
+ android:paddingBottom="8dp"
20
+ android:maxLines="1"
21
+ android:ellipsize="end" />
22
+
23
+ <!-- 3-image filmstrip: left preview | center | right preview -->
24
+ <LinearLayout
25
+ android:layout_width="match_parent"
26
+ android:layout_height="196dp"
27
+ android:orientation="horizontal"
28
+ android:gravity="center_vertical">
29
+
30
+ <!-- Left preview (dimmed, inset) -->
31
+ <ImageView
32
+ android:id="@+id/swan_filmstrip_left"
33
+ android:layout_width="0dp"
34
+ android:layout_height="match_parent"
35
+ android:layout_weight="1"
36
+ android:layout_marginTop="16dp"
37
+ android:layout_marginBottom="16dp"
38
+ android:layout_marginStart="2dp"
39
+ android:scaleType="centerCrop"
40
+ android:alpha="0.6"
41
+ android:contentDescription="Previous item" />
42
+
43
+ <!-- Center image (full size) -->
44
+ <ImageView
45
+ android:id="@+id/swan_filmstrip_center"
46
+ android:layout_width="0dp"
47
+ android:layout_height="match_parent"
48
+ android:layout_weight="3"
49
+ android:layout_marginStart="4dp"
50
+ android:layout_marginEnd="4dp"
51
+ android:scaleType="centerCrop"
52
+ android:contentDescription="Current item" />
53
+
54
+ <!-- Right preview (dimmed, inset) -->
55
+ <ImageView
56
+ android:id="@+id/swan_filmstrip_right"
57
+ android:layout_width="0dp"
58
+ android:layout_height="match_parent"
59
+ android:layout_weight="1"
60
+ android:layout_marginTop="16dp"
61
+ android:layout_marginBottom="16dp"
62
+ android:layout_marginEnd="2dp"
63
+ android:scaleType="centerCrop"
64
+ android:alpha="0.6"
65
+ android:contentDescription="Next item" />
66
+ </LinearLayout>
67
+
68
+ <!-- Item title + body + counter -->
69
+ <LinearLayout
70
+ android:layout_width="match_parent"
71
+ android:layout_height="wrap_content"
72
+ android:orientation="horizontal"
73
+ android:paddingStart="0dp"
74
+ android:paddingEnd="0dp"
75
+ android:paddingTop="4dp"
76
+ android:paddingBottom="4dp"
77
+ android:gravity="center_vertical">
78
+
79
+ <LinearLayout
80
+ android:layout_width="0dp"
81
+ android:layout_height="wrap_content"
82
+ android:layout_weight="1"
83
+ android:orientation="vertical">
84
+
85
+ <TextView
86
+ android:id="@+id/swan_filmstrip_item_title"
87
+ android:layout_width="match_parent"
88
+ android:layout_height="wrap_content"
89
+ android:textSize="13sp"
90
+ android:textStyle="bold"
91
+ android:textColor="#DE000000"
92
+ android:maxLines="1"
93
+ android:ellipsize="end" />
94
+
95
+ <TextView
96
+ android:id="@+id/swan_filmstrip_item_body"
97
+ android:layout_width="match_parent"
98
+ android:layout_height="wrap_content"
99
+ android:textSize="12sp"
100
+ android:textColor="#8A000000"
101
+ android:maxLines="1"
102
+ android:ellipsize="end" />
103
+ </LinearLayout>
104
+
105
+ <TextView
106
+ android:id="@+id/swan_filmstrip_counter"
107
+ android:layout_width="wrap_content"
108
+ android:layout_height="wrap_content"
109
+ android:textSize="11sp"
110
+ android:textColor="#8A000000"
111
+ android:paddingStart="8dp"
112
+ android:paddingEnd="0dp" />
113
+ </LinearLayout>
114
+
115
+ </LinearLayout>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <ImageView xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:id="@+id/swan_carousel_flipper_image"
4
+ android:layout_width="match_parent"
5
+ android:layout_height="match_parent"
6
+ android:scaleType="centerCrop"
7
+ android:contentDescription="Carousel image" />
@@ -0,0 +1,125 @@
1
+ package com.loyalytics.swan.templates.carousel
2
+
3
+ import org.junit.Assert.*
4
+ import org.junit.Test
5
+
6
+ class CarouselTemplateTest {
7
+
8
+ // --- wrapIndex ---
9
+
10
+ @Test
11
+ fun `wrapIndex forward at end wraps to first`() {
12
+ assertEquals(0, wrapIndex(3, 3))
13
+ }
14
+
15
+ @Test
16
+ fun `wrapIndex backward at start wraps to last`() {
17
+ assertEquals(2, wrapIndex(-1, 3))
18
+ }
19
+
20
+ @Test
21
+ fun `wrapIndex normal forward`() {
22
+ assertEquals(1, wrapIndex(1, 3))
23
+ assertEquals(2, wrapIndex(2, 3))
24
+ }
25
+
26
+ @Test
27
+ fun `wrapIndex normal backward`() {
28
+ assertEquals(1, wrapIndex(1, 3))
29
+ }
30
+
31
+ @Test
32
+ fun `wrapIndex with single item always returns 0`() {
33
+ assertEquals(0, wrapIndex(0, 1))
34
+ assertEquals(0, wrapIndex(1, 1))
35
+ assertEquals(0, wrapIndex(-1, 1))
36
+ }
37
+
38
+ @Test
39
+ fun `wrapIndex with size 0 returns 0`() {
40
+ assertEquals(0, wrapIndex(0, 0))
41
+ assertEquals(0, wrapIndex(5, 0))
42
+ }
43
+
44
+ @Test
45
+ fun `wrapIndex large negative wraps correctly`() {
46
+ // -5 with size 3: -5 mod 3 = -2, + 3 = 1
47
+ assertEquals(1, wrapIndex(-5, 3))
48
+ }
49
+
50
+ @Test
51
+ fun `wrapIndex large positive wraps correctly`() {
52
+ // 7 with size 3: 7 mod 3 = 1
53
+ assertEquals(1, wrapIndex(7, 3))
54
+ }
55
+
56
+ @Test
57
+ fun `wrapIndex full cycle returns to start`() {
58
+ // Going forward through all items returns to 0
59
+ val size = 5
60
+ for (i in 0 until size) {
61
+ assertEquals(i, wrapIndex(i, size))
62
+ }
63
+ assertEquals(0, wrapIndex(size, size))
64
+ }
65
+
66
+ @Test
67
+ fun `wrapIndex two items alternates correctly`() {
68
+ assertEquals(0, wrapIndex(0, 2))
69
+ assertEquals(1, wrapIndex(1, 2))
70
+ assertEquals(0, wrapIndex(2, 2))
71
+ assertEquals(1, wrapIndex(-1, 2))
72
+ }
73
+
74
+ @Test
75
+ fun `wrapIndex filmstrip prev-current-next for 3 items`() {
76
+ val size = 3
77
+ // At index 0: prev=2, current=0, next=1
78
+ assertEquals(2, wrapIndex(0 - 1, size))
79
+ assertEquals(0, wrapIndex(0, size))
80
+ assertEquals(1, wrapIndex(0 + 1, size))
81
+
82
+ // At index 2: prev=1, current=2, next=0
83
+ assertEquals(1, wrapIndex(2 - 1, size))
84
+ assertEquals(2, wrapIndex(2, size))
85
+ assertEquals(0, wrapIndex(2 + 1, size))
86
+ }
87
+
88
+ // --- canHandle ---
89
+
90
+ @Test
91
+ fun `canHandle returns true for carousel type`() {
92
+ val template = CarouselTemplate()
93
+ assertTrue(template.canHandle("carousel"))
94
+ }
95
+
96
+ @Test
97
+ fun `canHandle returns false for other types`() {
98
+ val template = CarouselTemplate()
99
+ assertFalse(template.canHandle("timer"))
100
+ assertFalse(template.canHandle("cta"))
101
+ assertFalse(template.canHandle(""))
102
+ assertFalse(template.canHandle("CAROUSEL"))
103
+ assertFalse(template.canHandle("Carousel"))
104
+ }
105
+
106
+ // --- CarouselAutoRemoteViews.MAX_FLIPPER_IMAGES cap ---
107
+ // (Cannot directly test private const, but we verify the build method
108
+ // is accessible and the type is correct)
109
+
110
+ @Test
111
+ fun `CarouselAutoRemoteViews is a singleton object`() {
112
+ // Verify the object exists and is accessible
113
+ assertNotNull(CarouselAutoRemoteViews)
114
+ }
115
+
116
+ @Test
117
+ fun `CarouselFilmstripRemoteViews is a singleton object`() {
118
+ assertNotNull(CarouselFilmstripRemoteViews)
119
+ }
120
+
121
+ @Test
122
+ fun `CarouselRemoteViews is a singleton object`() {
123
+ assertNotNull(CarouselRemoteViews)
124
+ }
125
+ }