Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased
- Fixed IterableEmbeddedView not having an empty constructor and causing crashes

## [3.6.4]
### Fixed
- Updated `customPayload` of In-App Messages to be `@Nullable`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ class EmbeddedMessageIntegrationTest : BaseIntegrationTest() {
.firstOrNull() as? EmbeddedMessageTestActivity

if (activity != null) {
val fragment = IterableEmbeddedView(IterableEmbeddedViewType.BANNER, message, null)
val fragment =
IterableEmbeddedView.newInstance(IterableEmbeddedViewType.BANNER, message, null)
activity.supportFragmentManager.beginTransaction()
.replace(R.id.embedded_message_container, fragment)
.commitNow()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class EmbeddedMessageTestActivity : AppCompatActivity() {

if (messages.isNotEmpty()) {
val firstMessage = messages.first()
val fragment = IterableEmbeddedView(IterableEmbeddedViewType.BANNER, firstMessage, null)
val fragment = IterableEmbeddedView.newInstance(IterableEmbeddedViewType.BANNER, firstMessage, null)
supportFragmentManager.beginTransaction()
.replace(R.id.embedded_message_container, fragment)
.commitNowAllowingStateLoss()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,49 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
import com.google.android.flexbox.FlexboxLayout
import com.iterable.iterableapi.EmbeddedMessageElementsButton
import com.iterable.iterableapi.IterableApi
import com.iterable.iterableapi.IterableEmbeddedMessage
import com.iterable.iterableapi.ui.R

class IterableEmbeddedView(
private var viewType: IterableEmbeddedViewType,
private var message: IterableEmbeddedMessage,
private var config: IterableEmbeddedViewConfig?
): Fragment() {
class IterableEmbeddedView() : Fragment() {

private lateinit var viewType: IterableEmbeddedViewType
private lateinit var message: IterableEmbeddedMessage
private var config: IterableEmbeddedViewConfig? = null

/**
* @deprecated This constructor violates Android Fragment best practices and will cause crashes
* when the Fragment is recreated by the system (e.g., after configuration changes or process death).
* Use [newInstance] factory method instead.
*
* Migration example:
* ```
* // Old (unstable / not-recommended):
* val fragment = IterableEmbeddedView(viewType, message, config)
*
* // New (more stable / recommended):
* val fragment = IterableEmbeddedView.newInstance(viewType, message, config)
* ```
*
* This constructor will be removed in a future version.
*/
@Deprecated(
message = "Use newInstance() factory method instead. This constructor causes crashes when Fragment is recreated by the system.",
replaceWith = ReplaceWith("IterableEmbeddedView.newInstance(viewType, message, config)"),
level = DeprecationLevel.WARNING
)
constructor(
viewType: IterableEmbeddedViewType,
message: IterableEmbeddedMessage,
config: IterableEmbeddedViewConfig?
) : this() {
arguments = IterableEmbeddedViewArguments.toBundle(viewType, message, config)
}

private val defaultBackgroundColor : Int by lazy { getDefaultColor(viewType, R.color.notification_background_color, R.color.banner_background_color, R.color.banner_background_color) }
private val defaultBorderColor : Int by lazy { getDefaultColor(viewType, R.color.notification_border_color, R.color.banner_border_color, R.color.banner_border_color) }
Expand All @@ -35,6 +62,39 @@ class IterableEmbeddedView(
private val defaultBorderWidth = 1
private val defaultBorderCornerRadius = 8f

companion object {
/**
* Factory method to create a new instance of IterableEmbeddedView with the required parameters.
*
* @param viewType The type of embedded view to display
* @param message The embedded message to display
* @param config Optional configuration for customizing the view appearance
* @return A new instance of IterableEmbeddedView
*/
@JvmStatic
fun newInstance(
viewType: IterableEmbeddedViewType,
message: IterableEmbeddedMessage,
config: IterableEmbeddedViewConfig? = null
): IterableEmbeddedView {
return IterableEmbeddedView().apply {
arguments = IterableEmbeddedViewArguments.toBundle(viewType, message, config)
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

arguments?.let { args ->
viewType = IterableEmbeddedViewArguments.getViewType(args)
message = IterableEmbeddedViewArguments.getMessage(args)
config = IterableEmbeddedViewArguments.getConfig(args)
} ?: throw IllegalStateException(
"IterableEmbeddedView requires arguments. Use newInstance() factory method to create this fragment."
)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow a dedicated class for arguments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that was a lot of variables and it really clustered the fragment, let me know if it is ok

Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package com.iterable.iterableapi.ui.embedded

import android.os.Bundle
import com.iterable.iterableapi.IterableEmbeddedMessage
import com.iterable.iterableapi.IterableLogger
import org.json.JSONException
import org.json.JSONObject

internal object IterableEmbeddedViewArguments {

private const val TAG = "IterableEmbeddedViewArgs"

// Argument keys
private const val KEY_VIEW_TYPE = "view_type"
private const val KEY_MESSAGE_JSON = "message_json"
private const val KEY_BG_COLOR = "bg_color"
private const val KEY_BORDER_COLOR = "border_color"
private const val KEY_BORDER_WIDTH = "border_width"
private const val KEY_BORDER_RADIUS = "border_radius"
private const val KEY_PRIMARY_BTN_BG = "primary_btn_bg"
private const val KEY_PRIMARY_BTN_TEXT = "primary_btn_text"
private const val KEY_SECONDARY_BTN_BG = "secondary_btn_bg"
private const val KEY_SECONDARY_BTN_TEXT = "secondary_btn_text"
private const val KEY_TITLE_COLOR = "title_color"
private const val KEY_BODY_COLOR = "body_color"

fun toBundle(
viewType: IterableEmbeddedViewType,
message: IterableEmbeddedMessage,
config: IterableEmbeddedViewConfig?
): Bundle {
return Bundle().apply {
putString(KEY_VIEW_TYPE, viewType.name)
putString(KEY_MESSAGE_JSON, IterableEmbeddedMessage.toJSONObject(message).toString())
putConfig(config)
}
}

fun getViewType(arguments: Bundle): IterableEmbeddedViewType {
val viewTypeName = arguments.getString(KEY_VIEW_TYPE)
return viewTypeName?.let {
try {
IterableEmbeddedViewType.valueOf(it)
} catch (e: IllegalArgumentException) {
IterableLogger.e(TAG, "Invalid view type: $it, defaulting to BANNER")
IterableEmbeddedViewType.BANNER
}
} ?: IterableEmbeddedViewType.BANNER
}

fun getMessage(arguments: Bundle): IterableEmbeddedMessage {
val messageJsonString = arguments.getString(KEY_MESSAGE_JSON)
return if (messageJsonString != null) {
try {
val messageJson = JSONObject(messageJsonString)
IterableEmbeddedMessage.fromJSONObject(messageJson)
} catch (e: JSONException) {
IterableLogger.e(TAG, "Failed to parse message JSON", e)
throw IllegalStateException(
"IterableEmbeddedView failed to restore message from saved state. Use newInstance() factory method to create this fragment."
)
}
} else {
throw IllegalStateException(
"IterableEmbeddedView requires a message argument. Use newInstance() factory method to create this fragment."
)
}
}

fun getConfig(arguments: Bundle): IterableEmbeddedViewConfig? {
// Check if any config properties exist
val hasConfig = arguments.containsKey(KEY_BG_COLOR) ||
arguments.containsKey(KEY_BORDER_COLOR) ||
arguments.containsKey(KEY_BORDER_WIDTH) ||
arguments.containsKey(KEY_BORDER_RADIUS) ||
arguments.containsKey(KEY_PRIMARY_BTN_BG) ||
arguments.containsKey(KEY_PRIMARY_BTN_TEXT) ||
arguments.containsKey(KEY_SECONDARY_BTN_BG) ||
arguments.containsKey(KEY_SECONDARY_BTN_TEXT) ||
arguments.containsKey(KEY_TITLE_COLOR) ||
arguments.containsKey(KEY_BODY_COLOR)

return if (hasConfig) {
IterableEmbeddedViewConfig(
backgroundColor = arguments.getIntOrNull(KEY_BG_COLOR),
borderColor = arguments.getIntOrNull(KEY_BORDER_COLOR),
borderWidth = arguments.getIntOrNull(KEY_BORDER_WIDTH),
borderCornerRadius = arguments.getFloatOrNull(KEY_BORDER_RADIUS),
primaryBtnBackgroundColor = arguments.getIntOrNull(KEY_PRIMARY_BTN_BG),
primaryBtnTextColor = arguments.getIntOrNull(KEY_PRIMARY_BTN_TEXT),
secondaryBtnBackgroundColor = arguments.getIntOrNull(KEY_SECONDARY_BTN_BG),
secondaryBtnTextColor = arguments.getIntOrNull(KEY_SECONDARY_BTN_TEXT),
titleTextColor = arguments.getIntOrNull(KEY_TITLE_COLOR),
bodyTextColor = arguments.getIntOrNull(KEY_BODY_COLOR)
)
} else {
null
}
}

private fun Bundle.putConfig(config: IterableEmbeddedViewConfig?) {
config?.let { cfg ->
cfg.backgroundColor?.let { putInt(KEY_BG_COLOR, it) }
cfg.borderColor?.let { putInt(KEY_BORDER_COLOR, it) }
cfg.borderWidth?.let { putInt(KEY_BORDER_WIDTH, it) }
cfg.borderCornerRadius?.let { putFloat(KEY_BORDER_RADIUS, it) }
cfg.primaryBtnBackgroundColor?.let { putInt(KEY_PRIMARY_BTN_BG, it) }
cfg.primaryBtnTextColor?.let { putInt(KEY_PRIMARY_BTN_TEXT, it) }
cfg.secondaryBtnBackgroundColor?.let { putInt(KEY_SECONDARY_BTN_BG, it) }
cfg.secondaryBtnTextColor?.let { putInt(KEY_SECONDARY_BTN_TEXT, it) }
cfg.titleTextColor?.let { putInt(KEY_TITLE_COLOR, it) }
cfg.bodyTextColor?.let { putInt(KEY_BODY_COLOR, it) }
}
}

private fun Bundle.getIntOrNull(key: String): Int? {
return if (containsKey(key)) getInt(key) else null
}

private fun Bundle.getFloatOrNull(key: String): Float? {
return if (containsKey(key)) getFloat(key) else null
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.iterable.iterableapi.ui.embedded

import android.graphics.Color

data class IterableEmbeddedViewConfig(
val backgroundColor: Int?,
val borderColor: Int?,
Expand Down
Loading