react-native-firework-sdk 2.18.12 → 2.19.0

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 (124) hide show
  1. package/android/gradle.properties +1 -1
  2. package/android/src/main/java/com/fireworksdk/bridge/components/storyblock/StoryBlockFragment.kt +2 -1
  3. package/android/src/main/java/com/fireworksdk/bridge/components/videofeed/FWVideoFeed.kt +2 -1
  4. package/android/src/main/java/com/fireworksdk/bridge/models/FWControlsInsetModel.kt +1 -1
  5. package/android/src/main/java/com/fireworksdk/bridge/models/FWControlsInsetModelDeserializer.kt +2 -0
  6. package/android/src/main/java/com/fireworksdk/bridge/models/FWControlsInsetModelSerializer.kt +2 -0
  7. package/android/src/main/java/com/fireworksdk/bridge/models/FWVideoPlayerConfigModel.kt +4 -0
  8. package/android/src/main/java/com/fireworksdk/bridge/models/FWVideoPlayerConfigModelDeserializer.kt +12 -0
  9. package/android/src/main/java/com/fireworksdk/bridge/models/FWVideoPlayerConfigModelSerializer.kt +7 -0
  10. package/android/src/main/java/com/fireworksdk/bridge/models/FWVideoShoppingProduct.kt +2 -2
  11. package/android/src/main/java/com/fireworksdk/bridge/models/FWVideoShoppingProductDeserializer.kt +2 -2
  12. package/android/src/main/java/com/fireworksdk/bridge/models/enums/FWFeedCompleteAction.kt +18 -0
  13. package/android/src/main/java/com/fireworksdk/bridge/reactnative/module/FWVideoShoppingModule.kt +52 -4
  14. package/android/src/main/java/com/fireworksdk/bridge/reactnative/module/FireworkSDKModule.kt +7 -5
  15. package/android/src/main/java/com/fireworksdk/bridge/utils/FWConfigUtil.kt +45 -3
  16. package/ios/Components/StoryBlock.swift +23 -2
  17. package/ios/Components/StoryBlockConfiguration.swift +3 -0
  18. package/ios/Components/StoryBlockManager.m +1 -1
  19. package/ios/Components/StoryBlockManager.swift +3 -4
  20. package/ios/Components/VideoFeed.swift +17 -2
  21. package/ios/Components/VideoFeedManager.m +1 -1
  22. package/ios/Components/VideoFeedManager.swift +3 -4
  23. package/ios/Components/VideoPlayerConfiguration.swift +7 -0
  24. package/ios/Models/Common/ButtonShape.swift +21 -0
  25. package/ios/Models/Common/ControlsInset.swift +1 -0
  26. package/ios/Modules/FireworkSDKModule/FireworkSDKModule.m +0 -1
  27. package/ios/Modules/FireworkSDKModule/FireworkSDKModule.swift +7 -20
  28. package/ios/Modules/Shopping/ShoppingModule.swift +6 -3
  29. package/ios/Utils/Extensions/UIWindowScene+Swizzle.swift +2 -2
  30. package/lib/commonjs/FireworkSDK.js +19 -27
  31. package/lib/commonjs/FireworkSDK.js.map +1 -1
  32. package/lib/commonjs/VideoShopping.js +6 -6
  33. package/lib/commonjs/VideoShopping.js.map +1 -1
  34. package/lib/commonjs/components/StoryBlock.js +15 -10
  35. package/lib/commonjs/components/StoryBlock.js.map +1 -1
  36. package/lib/commonjs/components/VideoFeed.js +14 -5
  37. package/lib/commonjs/components/VideoFeed.js.map +1 -1
  38. package/lib/commonjs/index.js.map +1 -1
  39. package/lib/commonjs/models/ButtonShape.js +2 -0
  40. package/lib/commonjs/models/ButtonShape.js.map +1 -0
  41. package/lib/commonjs/models/FeedCompleteAction.js +2 -0
  42. package/lib/commonjs/models/FeedCompleteAction.js.map +1 -0
  43. package/lib/commonjs/modules/FireworkSDKModule.js.map +1 -1
  44. package/lib/commonjs/utils/FWGlobalState.js +8 -8
  45. package/lib/commonjs/utils/FWGlobalState.js.map +1 -1
  46. package/lib/module/FireworkSDK.js +19 -27
  47. package/lib/module/FireworkSDK.js.map +1 -1
  48. package/lib/module/VideoShopping.js +6 -6
  49. package/lib/module/VideoShopping.js.map +1 -1
  50. package/lib/module/components/StoryBlock.js +15 -10
  51. package/lib/module/components/StoryBlock.js.map +1 -1
  52. package/lib/module/components/VideoFeed.js +14 -5
  53. package/lib/module/components/VideoFeed.js.map +1 -1
  54. package/lib/module/index.js.map +1 -1
  55. package/lib/module/models/ButtonShape.js +2 -0
  56. package/lib/module/models/ButtonShape.js.map +1 -0
  57. package/lib/module/models/FeedCompleteAction.js +2 -0
  58. package/lib/module/models/FeedCompleteAction.js.map +1 -0
  59. package/lib/module/modules/FireworkSDKModule.js.map +1 -1
  60. package/lib/module/utils/FWGlobalState.js +8 -8
  61. package/lib/module/utils/FWGlobalState.js.map +1 -1
  62. package/lib/typescript/commonjs/src/FireworkSDK.d.ts +0 -6
  63. package/lib/typescript/commonjs/src/FireworkSDK.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/src/components/StoryBlock.d.ts +3 -4
  65. package/lib/typescript/commonjs/src/components/StoryBlock.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/src/components/VideoFeed.d.ts +1 -1
  67. package/lib/typescript/commonjs/src/components/VideoFeed.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/src/index.d.ts +3 -1
  69. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/src/models/ButtonShape.d.ts +2 -0
  71. package/lib/typescript/commonjs/src/models/ButtonShape.d.ts.map +1 -0
  72. package/lib/typescript/commonjs/src/models/ControlsInset.d.ts +1 -0
  73. package/lib/typescript/commonjs/src/models/ControlsInset.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/src/models/FeedCompleteAction.d.ts +2 -0
  75. package/lib/typescript/commonjs/src/models/FeedCompleteAction.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/src/models/StoryBlockConfiguration.d.ts +21 -3
  77. package/lib/typescript/commonjs/src/models/StoryBlockConfiguration.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/src/models/VideoPlayerCTAStyle.d.ts +6 -3
  79. package/lib/typescript/commonjs/src/models/VideoPlayerCTAStyle.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/src/models/VideoPlayerConfiguration.d.ts +14 -1
  81. package/lib/typescript/commonjs/src/models/VideoPlayerConfiguration.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/src/modules/FireworkSDKModule.d.ts +0 -2
  83. package/lib/typescript/commonjs/src/modules/FireworkSDKModule.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/src/utils/FWGlobalState.d.ts +4 -4
  85. package/lib/typescript/commonjs/src/utils/FWGlobalState.d.ts.map +1 -1
  86. package/lib/typescript/module/src/FireworkSDK.d.ts +0 -6
  87. package/lib/typescript/module/src/FireworkSDK.d.ts.map +1 -1
  88. package/lib/typescript/module/src/components/StoryBlock.d.ts +3 -4
  89. package/lib/typescript/module/src/components/StoryBlock.d.ts.map +1 -1
  90. package/lib/typescript/module/src/components/VideoFeed.d.ts +1 -1
  91. package/lib/typescript/module/src/components/VideoFeed.d.ts.map +1 -1
  92. package/lib/typescript/module/src/index.d.ts +3 -1
  93. package/lib/typescript/module/src/index.d.ts.map +1 -1
  94. package/lib/typescript/module/src/models/ButtonShape.d.ts +2 -0
  95. package/lib/typescript/module/src/models/ButtonShape.d.ts.map +1 -0
  96. package/lib/typescript/module/src/models/ControlsInset.d.ts +1 -0
  97. package/lib/typescript/module/src/models/ControlsInset.d.ts.map +1 -1
  98. package/lib/typescript/module/src/models/FeedCompleteAction.d.ts +2 -0
  99. package/lib/typescript/module/src/models/FeedCompleteAction.d.ts.map +1 -0
  100. package/lib/typescript/module/src/models/StoryBlockConfiguration.d.ts +21 -3
  101. package/lib/typescript/module/src/models/StoryBlockConfiguration.d.ts.map +1 -1
  102. package/lib/typescript/module/src/models/VideoPlayerCTAStyle.d.ts +6 -3
  103. package/lib/typescript/module/src/models/VideoPlayerCTAStyle.d.ts.map +1 -1
  104. package/lib/typescript/module/src/models/VideoPlayerConfiguration.d.ts +14 -1
  105. package/lib/typescript/module/src/models/VideoPlayerConfiguration.d.ts.map +1 -1
  106. package/lib/typescript/module/src/modules/FireworkSDKModule.d.ts +0 -2
  107. package/lib/typescript/module/src/modules/FireworkSDKModule.d.ts.map +1 -1
  108. package/lib/typescript/module/src/utils/FWGlobalState.d.ts +4 -4
  109. package/lib/typescript/module/src/utils/FWGlobalState.d.ts.map +1 -1
  110. package/package.json +1 -1
  111. package/react_native_firework_sdk.podspec +1 -2
  112. package/src/FireworkSDK.ts +25 -29
  113. package/src/VideoShopping.ts +6 -6
  114. package/src/components/StoryBlock.tsx +20 -16
  115. package/src/components/VideoFeed.tsx +15 -6
  116. package/src/index.tsx +4 -0
  117. package/src/models/ButtonShape.ts +1 -0
  118. package/src/models/ControlsInset.ts +1 -0
  119. package/src/models/FeedCompleteAction.ts +1 -0
  120. package/src/models/StoryBlockConfiguration.ts +24 -3
  121. package/src/models/VideoPlayerCTAStyle.ts +6 -3
  122. package/src/models/VideoPlayerConfiguration.ts +16 -1
  123. package/src/modules/FireworkSDKModule.ts +0 -2
  124. package/src/utils/FWGlobalState.ts +10 -10
@@ -6,4 +6,4 @@ FireworkSDK_targetSdkVersion=33
6
6
  FireworkSDK_compileSdkVersion=33
7
7
  FireworkSDK_kotlinVersion=1.8.22
8
8
  FireworkSDK_fwPlayerLaunchMode=singleTask
9
- FireworkSDK_fwNativeVersion=6.30.2
9
+ FireworkSDK_fwNativeVersion=6.30.6
@@ -25,6 +25,7 @@ import com.fireworksdk.bridge.models.FWVideoFeedPropsModel
25
25
  import com.fireworksdk.bridge.models.FWVideoFeedPropsModelDeserializer
26
26
  import com.fireworksdk.bridge.models.FWVideoFeedPropsModelSerializer
27
27
  import com.fireworksdk.bridge.utils.FWCommonUtil
28
+ import com.fireworksdk.bridge.utils.FWComponentType
28
29
  import com.fireworksdk.bridge.utils.FWConfigUtil
29
30
  import com.fireworksdk.bridge.utils.FWGlobalDataUtil
30
31
  import com.fireworksdk.bridge.utils.FWLanguageUtil
@@ -124,7 +125,7 @@ class StoryBlockFragment : FWBaseFragment() {
124
125
  }
125
126
 
126
127
  private fun initStoryBlock() {
127
- val viewOptionsBuilder = FWConfigUtil.generateViewOptionsBuilder(context, videoFeedPropsModel)
128
+ val viewOptionsBuilder = FWConfigUtil.generateViewOptionsBuilder(context, videoFeedPropsModel, FWComponentType.STORY_BLOCK)
128
129
 
129
130
  fwStoryBlockView?.init(
130
131
  fragmentManager = childFragmentManager,
@@ -7,6 +7,7 @@ import com.firework.error.FwErrorListener
7
7
  import com.firework.videofeed.FeedViewStateListener
8
8
  import com.firework.videofeed.FwVideoFeedView
9
9
  import com.fireworksdk.bridge.models.*
10
+ import com.fireworksdk.bridge.utils.FWComponentType
10
11
  import com.fireworksdk.bridge.utils.FWConfigUtil
11
12
  import com.fireworksdk.bridge.utils.FWLanguageUtil
12
13
 
@@ -41,7 +42,7 @@ class FWVideoFeed(
41
42
  if (isInit) {
42
43
  return
43
44
  }
44
- val viewOptionsBuilder = FWConfigUtil.generateViewOptionsBuilder(context, videoFeedPropsModel)
45
+ val viewOptionsBuilder = FWConfigUtil.generateViewOptionsBuilder(context, videoFeedPropsModel, FWComponentType.VIDEO_FEED)
45
46
  videoFeedView.init(viewOptionsBuilder.build())
46
47
  isInit = true
47
48
  }
@@ -1,3 +1,3 @@
1
1
  package com.fireworksdk.bridge.models
2
2
 
3
- data class FWControlsInsetModel(val top: Int? = null)
3
+ data class FWControlsInsetModel(val top: Int? = null, val bottom: Int? = null)
@@ -4,12 +4,14 @@ import org.json.JSONObject
4
4
 
5
5
  object FWControlsInsetModelDeserializer {
6
6
  private const val TOP = "top"
7
+ private const val BOTTOM = "bottom"
7
8
 
8
9
  fun deserialize(jsonObject: JSONObject?): FWControlsInsetModel? {
9
10
  jsonObject ?: return null
10
11
 
11
12
  return FWControlsInsetModel(
12
13
  top = jsonObject.optInt(TOP, 0),
14
+ bottom = jsonObject.optInt(BOTTOM, 0),
13
15
  )
14
16
  }
15
17
  }
@@ -5,12 +5,14 @@ import org.json.JSONObject
5
5
  object FWControlsInsetModelSerializer {
6
6
 
7
7
  private const val TOP = "top"
8
+ private const val BOTTOM = "bottom"
8
9
 
9
10
  fun serialize(model: FWControlsInsetModel?): JSONObject? {
10
11
  model ?: return null
11
12
 
12
13
  val jsonObject = JSONObject()
13
14
  model.top?.let { jsonObject.put(TOP, it) }
15
+ model.bottom?.let { jsonObject.put(BOTTOM, it) }
14
16
 
15
17
  return jsonObject
16
18
  }
@@ -1,6 +1,7 @@
1
1
  package com.fireworksdk.bridge.models
2
2
 
3
3
  import com.fireworksdk.bridge.models.enums.FWCtaDelayType
4
+ import com.fireworksdk.bridge.models.enums.FWFeedCompleteAction
4
5
  import com.fireworksdk.bridge.models.enums.FWPlayerStyle
5
6
  import com.fireworksdk.bridge.models.enums.FWVideoCompleteAction
6
7
  import com.fireworksdk.bridge.models.enums.FWVideoPlayerCTAWidth
@@ -11,6 +12,7 @@ data class FWVideoPlayerConfigModel(
11
12
  val scrollDirection: FWVideoPlayerScrollDirection? = null,
12
13
  val enableScrollForVertical: Boolean? = null,
13
14
  val videoCompleteAction: FWVideoCompleteAction? = null,
15
+ val feedCompleteAction: FWFeedCompleteAction? = null,
14
16
  val showShareButton: Boolean? = null,
15
17
  val ctaButtonStyle: FWCtaButtonStyleModel? = null,
16
18
  val showMuteButton: Boolean? = null,
@@ -34,6 +36,8 @@ data class FWVideoPlayerConfigModel(
34
36
  val chatStyle: FWChatStyleModel? = null,
35
37
  val shouldExtendMediaOutsideSafeArea: Boolean? = null,
36
38
  val additionalControlsInset: FWControlsInsetModel? = null,
39
+ val isArrowButtonVisible: Boolean? = null,
40
+ val isFullscreenArrowButtonVisible: Boolean? = null,
37
41
  ) {
38
42
 
39
43
  data class FWCtaButtonStyleModel(
@@ -1,6 +1,7 @@
1
1
  package com.fireworksdk.bridge.models
2
2
 
3
3
  import com.fireworksdk.bridge.models.enums.FWCtaDelayType
4
+ import com.fireworksdk.bridge.models.enums.FWFeedCompleteAction
4
5
  import com.fireworksdk.bridge.models.enums.FWPlayerStyle
5
6
  import com.fireworksdk.bridge.models.enums.FWVideoCompleteAction
6
7
  import com.fireworksdk.bridge.models.enums.FWVideoPlayerCTAWidth
@@ -13,6 +14,7 @@ object FWVideoPlayerConfigModelDeserializer {
13
14
  private const val SCROLL_DIRECTION_KEY = "scrollDirection"
14
15
  private const val ENABLE_SCROLL_FOR_VERTICAL_KEY = "enableScrollForVertical"
15
16
  private const val VIDEO_COMPLETE_ACTION_KEY = "videoCompleteAction"
17
+ private const val FEED_COMPLETE_ACTION_KEY = "feedCompleteAction"
16
18
  private const val SHOW_SHARE_BUTTON_KEY = "showShareButton"
17
19
  private const val CTA_BUTTON_STYLE_KEY = "ctaButtonStyle"
18
20
  private const val SHOW_MUTE_BUTTON_KEY = "showMuteButton"
@@ -48,6 +50,8 @@ object FWVideoPlayerConfigModelDeserializer {
48
50
  private const val ENABLE_SMALL_SIZE_IN_COMPACT = "enableSmallSizeInCompact"
49
51
  private const val SHOULD_EXTEND_MEDIA_OUTSIDE_SAFE_AREA_KEY = "shouldExtendMediaOutsideSafeArea"
50
52
  private const val ADDITIONAL_CONTROLS_INSET_KEY = "additionalControlsInset"
53
+ private const val IS_ARROW_BUTTON_VISIBLE_KEY = "isArrowButtonVisible"
54
+ private const val IS_FULLSCREEN_ARROW_BUTTON_VISIBLE_KEY = "isFullscreenArrowButtonVisible"
51
55
 
52
56
  fun deserialize(responseJson: JSONObject?): FWVideoPlayerConfigModel? {
53
57
  responseJson ?: return null
@@ -56,6 +60,7 @@ object FWVideoPlayerConfigModelDeserializer {
56
60
  val scrollDirection = if (responseJson.has(SCROLL_DIRECTION_KEY)) responseJson.optString(SCROLL_DIRECTION_KEY) else null
57
61
  val enableScrollForVertical = if (responseJson.has(ENABLE_SCROLL_FOR_VERTICAL_KEY)) responseJson.optBoolean(ENABLE_SCROLL_FOR_VERTICAL_KEY) else null
58
62
  val videoCompleteAction = if (responseJson.has(VIDEO_COMPLETE_ACTION_KEY)) responseJson.optString(VIDEO_COMPLETE_ACTION_KEY) else null
63
+ val feedCompleteAction = if (responseJson.has(FEED_COMPLETE_ACTION_KEY)) responseJson.optString(FEED_COMPLETE_ACTION_KEY) else null
59
64
  val showShareButton = if (responseJson.has(SHOW_SHARE_BUTTON_KEY)) responseJson.optBoolean(SHOW_SHARE_BUTTON_KEY) else null
60
65
  val ctaButtonStyle = deserializeCtaButtonStyle(responseJson.optJSONObject(CTA_BUTTON_STYLE_KEY))
61
66
  val showMuteButton = if (responseJson.has(SHOW_MUTE_BUTTON_KEY)) responseJson.optBoolean(SHOW_MUTE_BUTTON_KEY) else null
@@ -86,12 +91,17 @@ object FWVideoPlayerConfigModelDeserializer {
86
91
  if (responseJson.has(SHOULD_EXTEND_MEDIA_OUTSIDE_SAFE_AREA_KEY)) responseJson.optBoolean(SHOULD_EXTEND_MEDIA_OUTSIDE_SAFE_AREA_KEY) else null
87
92
  val additionalControlsInset =
88
93
  if (responseJson.has(ADDITIONAL_CONTROLS_INSET_KEY)) FWControlsInsetModelDeserializer.deserialize(responseJson.optJSONObject(ADDITIONAL_CONTROLS_INSET_KEY)) else null
94
+ val isArrowButtonVisible =
95
+ if (responseJson.has(IS_ARROW_BUTTON_VISIBLE_KEY)) responseJson.optBoolean(IS_ARROW_BUTTON_VISIBLE_KEY) else null
96
+ val isFullscreenArrowButtonVisible =
97
+ if (responseJson.has(IS_FULLSCREEN_ARROW_BUTTON_VISIBLE_KEY)) responseJson.optBoolean(IS_FULLSCREEN_ARROW_BUTTON_VISIBLE_KEY) else null
89
98
 
90
99
  return FWVideoPlayerConfigModel(
91
100
  playerStyle = if (!playerStyle.isNullOrBlank()) FWPlayerStyle.deserialize(playerStyle) else null,
92
101
  scrollDirection = if (!scrollDirection.isNullOrBlank()) FWVideoPlayerScrollDirection.deserialize(scrollDirection) else null,
93
102
  enableScrollForVertical = enableScrollForVertical,
94
103
  videoCompleteAction = if (!videoCompleteAction.isNullOrBlank()) FWVideoCompleteAction.deserialize(videoCompleteAction) else null,
104
+ feedCompleteAction = if (!feedCompleteAction.isNullOrBlank()) FWFeedCompleteAction.deserialize(feedCompleteAction) else null,
95
105
  showShareButton = showShareButton,
96
106
  ctaButtonStyle = ctaButtonStyle,
97
107
  showMuteButton = showMuteButton,
@@ -115,6 +125,8 @@ object FWVideoPlayerConfigModelDeserializer {
115
125
  chatStyle = FWChatConfigModelDeserializer.deserialize(chatStyleJsonObject),
116
126
  shouldExtendMediaOutsideSafeArea = shouldExtendMediaOutsideSafeArea,
117
127
  additionalControlsInset = additionalControlsInset,
128
+ isArrowButtonVisible = isArrowButtonVisible,
129
+ isFullscreenArrowButtonVisible = isFullscreenArrowButtonVisible,
118
130
  )
119
131
  }
120
132
 
@@ -1,6 +1,7 @@
1
1
  package com.fireworksdk.bridge.models
2
2
 
3
3
  import com.fireworksdk.bridge.models.enums.FWCtaDelayType
4
+ import com.fireworksdk.bridge.models.enums.FWFeedCompleteAction
4
5
  import com.fireworksdk.bridge.models.enums.FWPlayerStyle
5
6
  import com.fireworksdk.bridge.models.enums.FWVideoCompleteAction
6
7
  import com.fireworksdk.bridge.models.enums.FWVideoPlayerCTAWidth
@@ -13,6 +14,7 @@ object FWVideoPlayerConfigModelSerializer {
13
14
  private const val SCROLL_DIRECTION_KEY = "scrollDirection"
14
15
  private const val ENABLE_SCROLL_FOR_VERTICAL_KEY = "enableScrollForVertical"
15
16
  private const val VIDEO_COMPLETE_ACTION_KEY = "videoCompleteAction"
17
+ private const val FEED_COMPLETE_ACTION_KEY = "feedCompleteAction"
16
18
  private const val SHOW_SHARE_BUTTON_KEY = "showShareButton"
17
19
  private const val CTA_BUTTON_STYLE_KEY = "ctaButtonStyle"
18
20
  private const val SHOW_MUTE_BUTTON_KEY = "showMuteButton"
@@ -52,6 +54,8 @@ object FWVideoPlayerConfigModelSerializer {
52
54
  private const val ENABLE_SMALL_SIZE_IN_COMPACT = "enableSmallSizeInCompact"
53
55
  private const val SHOULD_EXTEND_MEDIA_OUTSIDE_SAFE_AREA_KEY = "shouldExtendMediaOutsideSafeArea"
54
56
  private const val ADDITIONAL_CONTROLS_INSET_KEY = "additionalControlsInset"
57
+ private const val IS_ARROW_BUTTON_VISIBLE_KEY = "isArrowButtonVisible"
58
+ private const val IS_FULLSCREEN_ARROW_BUTTON_VISIBLE_KEY = "isFullscreenArrowButtonVisible"
55
59
 
56
60
 
57
61
  fun serialize(model: FWVideoPlayerConfigModel?): JSONObject? {
@@ -61,6 +65,7 @@ object FWVideoPlayerConfigModelSerializer {
61
65
  jsonObject.put(SCROLL_DIRECTION_KEY, FWVideoPlayerScrollDirection.serialize(model.scrollDirection))
62
66
  jsonObject.put(ENABLE_SCROLL_FOR_VERTICAL_KEY, model.enableScrollForVertical)
63
67
  jsonObject.put(VIDEO_COMPLETE_ACTION_KEY, FWVideoCompleteAction.serialize(model.videoCompleteAction))
68
+ jsonObject.put(FEED_COMPLETE_ACTION_KEY, FWFeedCompleteAction.serialize(model.feedCompleteAction))
64
69
  jsonObject.put(SHOW_SHARE_BUTTON_KEY, model.showShareButton)
65
70
  jsonObject.put(CTA_BUTTON_STYLE_KEY, serializeCtaButtonStyle(model.ctaButtonStyle))
66
71
  jsonObject.put(CHAT_STYLE_KEY, serializeChatStyle(model.chatStyle))
@@ -84,6 +89,8 @@ object FWVideoPlayerConfigModelSerializer {
84
89
  jsonObject.put(ENABLE_SMALL_SIZE_IN_COMPACT, model.enableSmallSizeInCompact)
85
90
  jsonObject.put(SHOULD_EXTEND_MEDIA_OUTSIDE_SAFE_AREA_KEY, model.shouldExtendMediaOutsideSafeArea)
86
91
  jsonObject.put(ADDITIONAL_CONTROLS_INSET_KEY, FWControlsInsetModelSerializer.serialize(model.additionalControlsInset))
92
+ jsonObject.put(IS_ARROW_BUTTON_VISIBLE_KEY, model.isArrowButtonVisible)
93
+ jsonObject.put(IS_FULLSCREEN_ARROW_BUTTON_VISIBLE_KEY, model.isFullscreenArrowButtonVisible)
87
94
  return jsonObject
88
95
  }
89
96
 
@@ -7,7 +7,7 @@ data class FWVideoShoppingProduct(
7
7
  val currency: String? = null,
8
8
  val description: String? = null,
9
9
  val units: List<FWVideoProductUnit>? = null,
10
- val isAvailable: Boolean = true,
10
+ val isAvailable: Boolean? = null,
11
11
  val mainProductImage: String? = null,
12
12
  val isDisabled: Boolean? = null,
13
13
  val hidePrice: Boolean? = null,
@@ -27,7 +27,7 @@ data class FWVideoShoppingProduct(
27
27
  val url: String? = null,
28
28
  val imageUrl: String? = null,
29
29
  val options: List<FWVideoProductOption>? = null,
30
- val isAvailable: Boolean = true,
30
+ val isAvailable: Boolean? = null,
31
31
  )
32
32
 
33
33
  data class FWVideoProductPrice(
@@ -53,7 +53,7 @@ object FWVideoShoppingProductDeserializer {
53
53
  val currency = if (responseJson.has(CURRENCY_KEY)) responseJson.optString(CURRENCY_KEY) else null
54
54
  val description = if (responseJson.has(DESCRIPTION_KEY)) responseJson.optString(DESCRIPTION_KEY) else null
55
55
  val units = deserializeUnits(responseJson.optJSONArray(UNITS_KEY))
56
- val isAvailable = responseJson.optBoolean(IS_AVAILABLE_KEY, true)
56
+ val isAvailable = if (responseJson.has(IS_AVAILABLE_KEY)) responseJson.optBoolean(IS_AVAILABLE_KEY) else null
57
57
  val mainProductImage = if (responseJson.has(MAIN_PRODUCT_IMAGE_KEY)) responseJson.optString(MAIN_PRODUCT_IMAGE_KEY) else null
58
58
  val isDisabled = if (responseJson.has(IS_DISABLED_KEY)) responseJson.optBoolean(IS_DISABLED_KEY) else null
59
59
  val hidePrice = if (responseJson.has(HIDE_PRICE_KEY)) responseJson.optBoolean(HIDE_PRICE_KEY) else null
@@ -104,7 +104,7 @@ object FWVideoShoppingProductDeserializer {
104
104
  val url = if (unitsJsonObject.has(URL_KEY)) unitsJsonObject.optString(URL_KEY) else null
105
105
  val imageUrl = if (unitsJsonObject.has(IMAGE_URL_KEY)) unitsJsonObject.optString(IMAGE_URL_KEY) else null
106
106
  val options = deserializeOptions(unitsJsonObject.optJSONArray(OPTIONS_KEY))
107
- val isAvailable = unitsJsonObject.optBoolean(IS_AVAILABLE_KEY, true)
107
+ val isAvailable = if (unitsJsonObject.has(IS_AVAILABLE_KEY)) unitsJsonObject.optBoolean(IS_AVAILABLE_KEY) else null
108
108
 
109
109
  return FWVideoShoppingProduct.FWVideoProductUnit(
110
110
  unitId = unitId,
@@ -0,0 +1,18 @@
1
+ package com.fireworksdk.bridge.models.enums
2
+
3
+ enum class FWFeedCompleteAction(val rawValue: String) {
4
+ Loop("loop"),
5
+ Dismiss("dismiss");
6
+
7
+ companion object {
8
+ fun deserialize(rawValue: String?): FWFeedCompleteAction? {
9
+ rawValue ?: return null
10
+ return FWFeedCompleteAction.values().firstOrNull { it.rawValue == rawValue }
11
+ }
12
+
13
+ fun serialize(model: FWFeedCompleteAction?): String? {
14
+ model ?: return null
15
+ return model.rawValue
16
+ }
17
+ }
18
+ }
@@ -5,7 +5,11 @@ import android.widget.Toast
5
5
  import com.facebook.react.bridge.*
6
6
  import com.firework.analyticsevents.VideoInfo
7
7
  import com.firework.common.product.CurrencyCode
8
+ import com.firework.common.product.Money
8
9
  import com.firework.common.product.Product
10
+ import com.firework.common.product.ProductImage
11
+ import com.firework.common.product.ProductUnit
12
+ import com.firework.common.product.ProductUnitOption
9
13
  import com.firework.error.shopping.ShoppingError
10
14
  import com.firework.sdk.FireworkSdk
11
15
  import com.firework.shopping.*
@@ -97,8 +101,8 @@ class FWVideoShoppingModule(
97
101
  }
98
102
 
99
103
  private fun hydrateProduct(
100
- product: Product,
101
- videoProduct: FWVideoShoppingProduct,
104
+ product: Product, // product from native
105
+ videoProduct: FWVideoShoppingProduct, // product from host app
102
106
  hydrator: ProductHydrator,
103
107
  position: Int
104
108
  ) {
@@ -121,6 +125,7 @@ class FWVideoShoppingModule(
121
125
  videoProduct.customCTAUrl?.let { customCTAUrl(it) }
122
126
  videoProduct.customCTATitle?.let { customCTATitle(it) }
123
127
  videoProduct.hidden?.let { hidden(it) }
128
+ val existingUnitIds = product.units.mapNotNull { it.id }.toSet()
124
129
  product.units.forEachIndexed { index, unit ->
125
130
  unit.id?.let { variantId ->
126
131
  val vpUnit = videoProduct.units?.find { vpu ->
@@ -137,14 +142,57 @@ class FWVideoShoppingModule(
137
142
  currency(currency)
138
143
  price(price = vpu.price?.amount ?: unit.price.amount)
139
144
  name((vpu.name ?: unit.name) ?: "")
140
- isAvailable(vpu.isAvailable)
145
+ vpu.isAvailable?.let { isAvailable(it) }
141
146
  vpu.originalPrice?.amount?.let { originalPrice(it) }
147
+ vpu.options?.let { opts ->
148
+ val unitOpts = opts.mapNotNull { opt ->
149
+ val name = opt.name ?: return@mapNotNull null
150
+ val value = opt.value ?: return@mapNotNull null
151
+ ProductUnitOption(name, value)
152
+ }
153
+ if (unitOpts.isNotEmpty()) unitOptions(unitOpts)
154
+ }
142
155
  this
143
156
  }
144
157
  }
145
158
  }
146
159
  }
147
- isAvailable(videoProduct.isAvailable)
160
+ videoProduct.units?.forEach { vpu ->
161
+ val newUnitId = vpu.unitId ?: return@forEach
162
+ if (newUnitId !in existingUnitIds) {
163
+ val currencyCode = if (vpu.price?.currencyCode != null && vpu.price.currencyCode.isNotBlank()) {
164
+ try { CurrencyCode.valueOf(vpu.price.currencyCode) } catch (e: IllegalArgumentException) { null }
165
+ } else null
166
+ val defaultCurrencyCode = currencyCode ?: CurrencyCode.USD
167
+ val price = Money(vpu.price?.amount ?: 0.0, defaultCurrencyCode)
168
+ val originalPrice = vpu.originalPrice?.amount?.let { amount ->
169
+ val opCurrencyCode = if (!vpu.originalPrice.currencyCode.isNullOrBlank()) {
170
+ try { CurrencyCode.valueOf(vpu.originalPrice.currencyCode) } catch (e: IllegalArgumentException) { defaultCurrencyCode }
171
+ } else defaultCurrencyCode
172
+ Money(amount, opCurrencyCode)
173
+ }
174
+ val unitOptions = vpu.options?.mapNotNull { opt ->
175
+ val name = opt.name ?: return@mapNotNull null
176
+ val value = opt.value ?: return@mapNotNull null
177
+ ProductUnitOption(name, value)
178
+ } ?: emptyList()
179
+ val image = vpu.imageUrl?.takeIf { it.isNotBlank() }?.let { ProductImage(it, 0) }
180
+ addVariant(
181
+ ProductUnit(
182
+ id = newUnitId,
183
+ name = vpu.name,
184
+ price = price,
185
+ originalPrice = originalPrice,
186
+ url = vpu.url ?: "",
187
+ options = unitOptions,
188
+ image = image,
189
+ isAvailable = vpu.isAvailable,
190
+ )
191
+ )
192
+ }
193
+ }
194
+ videoProduct.isAvailable?.let { isAvailable(it) }
195
+ this
148
196
  }
149
197
  }
150
198
  }
@@ -28,6 +28,7 @@ import com.fireworksdk.bridge.reactnative.FWReactNativeSDK
28
28
  import com.fireworksdk.bridge.reactnative.models.FireworkSDKInterface
29
29
  import com.fireworksdk.bridge.reactnative.utils.FWEventUtils
30
30
  import com.fireworksdk.bridge.utils.*
31
+ import com.fireworksdk.bridge.utils.FWComponentType
31
32
  import org.json.JSONObject
32
33
  import java.util.*
33
34
  import kotlin.system.exitProcess
@@ -48,12 +49,13 @@ class FireworkSDKModule(
48
49
  return if (sdkInitResultModel != null) { // init in application
49
50
  if (sdkInitResultModel.success == true) {
50
51
  FWEventUtils.sendInitSuccessEvent(reactApplicationContext)
51
- } else if (sdkInitResultModel.success == false) {
52
+ FireworkSdk.analytics.register(this)
53
+ true
54
+ } else {
52
55
  FWEventUtils.sendInitFailedEvent(reactApplicationContext, sdkInitResultModel.reason)
56
+ FireworkSdk.analytics.register(this)
57
+ false
53
58
  }
54
-
55
- FireworkSdk.analytics.register(this)
56
- true
57
59
  } else {
58
60
  false
59
61
  }
@@ -119,7 +121,7 @@ class FireworkSDKModule(
119
121
  videoPlayerConfiguration = videoPlayerConfigModel
120
122
  )
121
123
 
122
- val viewOptionsBuilder = FWConfigUtil.generateViewOptionsBuilder(activity, videoFeedPropsModel)
124
+ val viewOptionsBuilder = FWConfigUtil.generateViewOptionsBuilder(activity, videoFeedPropsModel, FWComponentType.VIDEO_PLAYER)
123
125
  FireworkSdk.startPlayer(viewOptionsBuilder.build(), url)
124
126
  }
125
127
 
@@ -20,6 +20,7 @@ import com.firework.common.feed.FeedResource
20
20
  import com.firework.common.feed.FeedTitlePosition
21
21
  import com.firework.common.player.ActionButtonOption
22
22
  import com.firework.common.player.CloseButtonOption
23
+ import com.firework.common.player.FeedCompleteAction
23
24
  import com.firework.common.player.LivestreamCountDownOption
24
25
  import com.firework.common.player.MuteButtonOption
25
26
  import com.firework.common.player.PipButtonOption
@@ -49,6 +50,7 @@ import com.fireworksdk.bridge.models.FWVideoFeedPropsModel
49
50
  import com.fireworksdk.bridge.models.enums.FWAppearanceMode
50
51
  import com.fireworksdk.bridge.models.enums.FWBadgeTextType
51
52
  import com.fireworksdk.bridge.models.enums.FWCtaDelayType
53
+ import com.fireworksdk.bridge.models.enums.FWFeedCompleteAction
52
54
  import com.fireworksdk.bridge.models.enums.FWGradientDrawableOrientation
53
55
  import com.fireworksdk.bridge.models.enums.FWPlayerStyle
54
56
  import com.fireworksdk.bridge.models.enums.FWSystemTypeface
@@ -60,9 +62,15 @@ import com.fireworksdk.bridge.models.enums.FWVideoPlayerCTAWidth
60
62
  import com.fireworksdk.bridge.models.enums.FWVideoPlayerLogoOption
61
63
  import com.fireworksdk.bridge.models.enums.FWVideoPlayerScrollDirection
62
64
 
65
+ enum class FWComponentType {
66
+ VIDEO_FEED,
67
+ STORY_BLOCK,
68
+ VIDEO_PLAYER,
69
+ }
70
+
63
71
  object FWConfigUtil {
64
72
 
65
- fun generateViewOptionsBuilder(context: Context?, videoFeedPropsModel: FWVideoFeedPropsModel?): ViewOptions.Builder {
73
+ fun generateViewOptionsBuilder(context: Context?, videoFeedPropsModel: FWVideoFeedPropsModel?, componentType: FWComponentType): ViewOptions.Builder {
66
74
  context ?: return ViewOptions.Builder()
67
75
  videoFeedPropsModel ?: return ViewOptions.Builder()
68
76
 
@@ -353,6 +361,16 @@ object FWConfigUtil {
353
361
  else -> {}
354
362
  }
355
363
 
364
+ when (videoFeedPropsModel.videoPlayerConfiguration?.feedCompleteAction) {
365
+ FWFeedCompleteAction.Loop -> {
366
+ playerOptionBuilder.feedCompleteAction(FeedCompleteAction.LOOP)
367
+ }
368
+ FWFeedCompleteAction.Dismiss -> {
369
+ playerOptionBuilder.feedCompleteAction(FeedCompleteAction.DISMISS)
370
+ }
371
+ else -> {}
372
+ }
373
+
356
374
  val showShareButton = videoFeedPropsModel.videoPlayerConfiguration?.showShareButton
357
375
  showShareButton?.let {
358
376
  playerOptionBuilder.showShareButton(it)
@@ -647,9 +665,12 @@ object FWConfigUtil {
647
665
  videoFeedPropsModel.videoPlayerConfiguration?.additionalControlsInset?.let { additionalControlsInset ->
648
666
  storyblockOptionBuilder.setImmersiveParam(
649
667
  ImmersiveParam(
650
- FWCommonUtil.dpToPx(
668
+ topControlsInset = FWCommonUtil.dpToPx(
651
669
  (additionalControlsInset.top ?: 0).toFloat(), context
652
- )
670
+ ),
671
+ bottomControlsInset = FWCommonUtil.dpToPx(
672
+ (additionalControlsInset.bottom ?: 0).toFloat(), context
673
+ ),
653
674
  )
654
675
  )
655
676
  }
@@ -657,6 +678,27 @@ object FWConfigUtil {
657
678
  playerOptionBuilder.enableImmersiveMode(shouldExtendMediaOutsideSafeArea)
658
679
  }
659
680
 
681
+ when (componentType) {
682
+ FWComponentType.VIDEO_FEED -> {
683
+ videoFeedPropsModel.videoPlayerConfiguration?.isArrowButtonVisible?.let { isArrowButtonVisible ->
684
+ playerOptionBuilder.isArrowButtonVisible(isArrowButtonVisible)
685
+ }
686
+ }
687
+ FWComponentType.STORY_BLOCK -> {
688
+ videoFeedPropsModel.videoPlayerConfiguration?.isArrowButtonVisible?.let { isArrowButtonVisible ->
689
+ storyblockOptionBuilder.isArrowButtonVisible(isArrowButtonVisible)
690
+ }
691
+ videoFeedPropsModel.videoPlayerConfiguration?.isFullscreenArrowButtonVisible?.let { isFullscreenArrowButtonVisible ->
692
+ playerOptionBuilder.isArrowButtonVisible(isFullscreenArrowButtonVisible)
693
+ }
694
+ }
695
+ FWComponentType.VIDEO_PLAYER -> {
696
+ videoFeedPropsModel.videoPlayerConfiguration?.isArrowButtonVisible?.let { isArrowButtonVisible ->
697
+ playerOptionBuilder.isArrowButtonVisible(isArrowButtonVisible)
698
+ }
699
+ }
700
+ }
701
+
660
702
  playerOptionBuilder.shareUrlCustomCallBack(FWGlobalDataUtil.customShareUrlCallback)
661
703
 
662
704
  return ViewOptions.Builder()
@@ -100,7 +100,7 @@ public class StoryBlock: UIView, StoryBlockViewControllerDelegate, PictureInPict
100
100
  }
101
101
 
102
102
  private func embed() {
103
- guard let parentVC = parentViewController else {
103
+ guard let parentVC = fws_parentViewController else {
104
104
  return
105
105
  }
106
106
  guard self.storyBlockVC == nil else {
@@ -254,6 +254,10 @@ public class StoryBlock: UIView, StoryBlockViewControllerDelegate, PictureInPict
254
254
  resultConfig.ctaButton.contentConfiguration.font = iOSFontInfo.getFont(fontSize)
255
255
  resultConfig.fullScreenPlayerView.ctaButton.contentConfiguration.font = iOSFontInfo.getFont(fontSize)
256
256
  }
257
+ if let shape = ctaButtonStyle.shape {
258
+ resultConfig.ctaButton.contentConfiguration.shape = shape.shape()
259
+ resultConfig.fullScreenPlayerView.ctaButton.contentConfiguration.shape = shape.shape()
260
+ }
257
261
  }
258
262
 
259
263
  if let showPlaybackButton = config.showPlaybackButton {
@@ -391,7 +395,7 @@ public class StoryBlock: UIView, StoryBlockViewControllerDelegate, PictureInPict
391
395
  resultConfig.additionalControlsInset = .init(
392
396
  top: additionalControlsInset.top ?? 0,
393
397
  left: 0,
394
- bottom: 0,
398
+ bottom: additionalControlsInset.bottom ?? 0,
395
399
  right: 0)
396
400
  }
397
401
 
@@ -426,6 +430,23 @@ public class StoryBlock: UIView, StoryBlockViewControllerDelegate, PictureInPict
426
430
  resultConfig.enableScrollForVertical = enableScrollForVertical
427
431
  }
428
432
 
433
+ if let isArrowButtonVisible = config.isArrowButtonVisible {
434
+ resultConfig.scrollNavigation.isArrowButtonVisible = isArrowButtonVisible
435
+ }
436
+
437
+ if let isFullscreenArrowButtonVisible = config.isFullscreenArrowButtonVisible {
438
+ resultConfig.fullScreenPlayerView.scrollNavigation.isArrowButtonVisible = isFullscreenArrowButtonVisible
439
+ }
440
+
441
+ if let feedCompleteAction = config.feedCompleteAction {
442
+ switch feedCompleteAction {
443
+ case .loop:
444
+ resultConfig.fullScreenPlayerView.feedCompleteAction = .loop
445
+ case .dismiss:
446
+ resultConfig.fullScreenPlayerView.feedCompleteAction = .dismiss
447
+ }
448
+ }
449
+
429
450
  if let storyBlockBehavior = gVideoLaunchBehavior?.storyBlockBehavior() {
430
451
  resultConfig.onFirstDisplay = storyBlockBehavior
431
452
  } else {
@@ -33,4 +33,7 @@ public class StoryBlockConfiguration: NSObject, Codable {
33
33
  var pipPlacement: PipPlacement?
34
34
  var scrollDirection: ScrollDirection?
35
35
  var enableScrollForVertical: Bool?
36
+ var isArrowButtonVisible: Bool?
37
+ var isFullscreenArrowButtonVisible: Bool?
38
+ var feedCompleteAction: FeedCompleteAction?
36
39
  }
@@ -16,7 +16,7 @@
16
16
  #endif
17
17
 
18
18
 
19
- @interface RCT_EXTERN_REMAP_MODULE(FWStoryBlock, StoryBlockManager, NSObject)
19
+ @interface RCT_EXTERN_REMAP_MODULE(FWStoryBlock, FWStoryBlockManager, NSObject)
20
20
 
21
21
  RCT_CUSTOM_VIEW_PROPERTY(source, StoryBlockSource, StoryBlock) {
22
22
  if (json) {
@@ -7,12 +7,11 @@
7
7
 
8
8
  import FireworkVideo
9
9
  import Foundation
10
- import FireworkVideoUI
11
10
 
12
- @objc(StoryBlockManager)
13
- class StoryBlockManager: RCTViewManager, StoryBlockViewDelegate {
11
+ @objc(FWStoryBlockManager)
12
+ class FWStoryBlockManager: RCTViewManager, StoryBlockViewDelegate {
14
13
  override func view() -> UIView! {
15
- _ = AppLanguageManager.shared
14
+ _ = FWSAppLanguageManager.shared
16
15
  let storyBlock = StoryBlock()
17
16
  storyBlock.delegate = self
18
17
 
@@ -9,7 +9,6 @@
9
9
 
10
10
  import FireworkVideo
11
11
  import UIKit
12
- import FireworkVideoUI
13
12
 
14
13
  @objc
15
14
  public enum VideoFeedMode: Int {
@@ -171,7 +170,7 @@ public class VideoFeed: UIView, VideoFeedViewControllerDelegate, PictureInPictur
171
170
  }
172
171
 
173
172
  private func embed() {
174
- guard let parentVC = parentViewController else {
173
+ guard let parentVC = fws_parentViewController else {
175
174
  return
176
175
  }
177
176
  guard self.feedVC == nil else {
@@ -422,6 +421,9 @@ extension VideoFeed {
422
421
  let iOSFontInfo = ctaButtonStyle.iOSFontInfo ?? FontInfo()
423
422
  vpcConfig.ctaButton.contentConfiguration.font = iOSFontInfo.getFont(fontSize)
424
423
  }
424
+ if let shape = ctaButtonStyle.shape {
425
+ vpcConfig.ctaButton.contentConfiguration.shape = shape.shape()
426
+ }
425
427
  }
426
428
  if let showPlaybackButton = config.showPlaybackButton {
427
429
  vpcConfig.playbackButton.isHidden = !showPlaybackButton
@@ -555,6 +557,19 @@ extension VideoFeed {
555
557
  }
556
558
  }
557
559
 
560
+ if let isArrowButtonVisible = config.isArrowButtonVisible {
561
+ vpcConfig.scrollNavigation.isArrowButtonVisible = isArrowButtonVisible
562
+ }
563
+
564
+ if let feedCompleteAction = config.feedCompleteAction {
565
+ switch feedCompleteAction {
566
+ case .loop:
567
+ vpcConfig.feedCompleteAction = .loop
568
+ case .dismiss:
569
+ vpcConfig.feedCompleteAction = .dismiss
570
+ }
571
+ }
572
+
558
573
  return vpcConfig
559
574
  }
560
575
 
@@ -16,7 +16,7 @@
16
16
  #import "react_native_firework_sdk-Swift.h"
17
17
  #endif
18
18
 
19
- @interface RCT_EXTERN_REMAP_MODULE(FWVideoFeed, VideoFeedManager, NSObject)
19
+ @interface RCT_EXTERN_REMAP_MODULE(FWVideoFeed, FWVideoFeedManager, NSObject)
20
20
 
21
21
  RCT_CUSTOM_VIEW_PROPERTY(source, VideFeedSourceType, VideoFeed) {
22
22
  if (json) {
@@ -8,13 +8,12 @@
8
8
 
9
9
  import FireworkVideo
10
10
  import Foundation
11
- import FireworkVideoUI
12
11
 
13
- @objc(VideoFeedManager)
14
- class VideoFeedManager: RCTViewManager, VideoFeedDelegate {
12
+ @objc(FWVideoFeedManager)
13
+ class FWVideoFeedManager: RCTViewManager, VideoFeedDelegate {
15
14
 
16
15
  override func view() -> UIView! {
17
- _ = AppLanguageManager.shared
16
+ _ = FWSAppLanguageManager.shared
18
17
  let videFeed = VideoFeed()
19
18
  videFeed.delegate = self
20
19