diff --git a/CHANGELOG.md b/CHANGELOG.md index a5f8663f9..1acc64b9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ [Migration Guides](https://github.com/urbanairship/android-library/tree/main/documentation/migration) +## Version 18.0.0, June 14, 2024 +Major SDK release with several breaking changes. +See the [Migration Guides](https://github.com/urbanairship/android-library/tree/main/documentation/migration/migration-guide-17-18.md) for more info. + +### Changes +- The Airship SDK now requires `compileSdk` version 34 (Android 14) or higher. +- New Automation module + - Check schedule’s start date before executing, to better handle updates to the scheduled start date + - Improved image loading for In-App messages, Scenes, and Surveys + - Reset GIF animations on visibility change in Scenes and Surveys + - Pause Story progress while videos are loading + - Concurrent automation processing to reduce latency if more than one automation is triggered at the same time + - Embedded Scenes & Survey support + - New module `urbanairship-automation-compose` to support embedding a Scene & Survey in compose + - Added new compound triggers and IAX event triggers + - Ban lists support +- Added new `PrivacyManager.Feature.FEATURE_FLAGS` to control access to feature flags +- Added support for multiple deferred feature flag resolution +- Added contact management support in preference centers +- Migrated to non-transitive R classes +- Removed `urbanairship-ads-identifier` and `urbanairship-preference` modules + ## Version 17.8.1, May 13, 2024 Patch release that improves first run display times for Scenes, Surveys, and In-App Automations. diff --git a/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/data/Item.kt b/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/data/Item.kt index f6f95f9ce..0a63e8f8d 100644 --- a/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/data/Item.kt +++ b/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/data/Item.kt @@ -149,7 +149,6 @@ public sealed class Item( val addPrompt: AddPrompt, val removePrompt: RemovePrompt, val emptyLabel: String?, - val registrationOptions: RegistrationOptions, override val conditions: Conditions, ) : Item( type = TYPE_CONTACT_MANAGEMENT, @@ -160,52 +159,67 @@ public sealed class Item( @Throws(JsonException::class) override fun toJson(): JsonMap = jsonMapBuilder() - .put(KEY_PLATFORM, platform.toJson()) + .apply { + when (platform) { + is Platform.Sms -> { + this.put(KEY_PLATFORM, PLATFORM_SMS) + .put(KEY_REGISTRATION_OPTIONS, platform.registrationOptions.toJson()) + } + is Platform.Email -> { + this.put(KEY_PLATFORM, PLATFORM_EMAIL) + .put(KEY_REGISTRATION_OPTIONS, platform.registrationOptions.toJson()) + } + } + } .put(KEY_ADD, addPrompt.toJson()) .put(KEY_REMOVE, removePrompt.toJson()) .put(KEY_EMPTY_LABEL, emptyLabel) - .put(KEY_REGISTRATION_OPTIONS, registrationOptions.toJson()) .build() internal companion object { + + private const val PLATFORM_SMS = "sms" + private const val PLATFORM_EMAIL = "email" + @Throws(JsonException::class) fun fromJson(json: JsonMap): ContactManagement { return ContactManagement( id = json.requireField(KEY_ID), - platform = Platform.fromJson(json.requireField(KEY_PLATFORM)), + platform = json.requireField(KEY_PLATFORM).let { + when(it) { + PLATFORM_SMS -> Platform.Sms(RegistrationOptions.Sms.fromJson(json.requireField(KEY_REGISTRATION_OPTIONS))) + PLATFORM_EMAIL -> Platform.Email(RegistrationOptions.Email.fromJson(json.requireField(KEY_REGISTRATION_OPTIONS))) + else -> throw JsonException("Invalid registration type: $it") + } + }, display = CommonDisplay.parse(json.requireField(KEY_DISPLAY)), addPrompt = AddPrompt.fromJson(json.requireField(KEY_ADD)), removePrompt = RemovePrompt.fromJson(json.requireField(KEY_REMOVE)), emptyLabel = json.optionalField(KEY_EMPTY_LABEL), - registrationOptions = RegistrationOptions.fromJson(json.requireField(KEY_REGISTRATION_OPTIONS)), - conditions = Condition.parse(json.requireField(KEY_CONDITIONS)) + conditions = Condition.parse(json.opt(KEY_CONDITIONS)) ) } } - public enum class Platform(public val jsonValue: String) { - SMS("sms"), - EMAIL("email"); - - public fun toJson(): JsonValue = JsonValue.wrap(jsonValue) + public sealed class Platform(public val channelType: ChannelType) { + public class Sms(public val registrationOptions: RegistrationOptions.Sms): Platform(ChannelType.SMS) + public class Email(public val registrationOptions: RegistrationOptions.Email): Platform(ChannelType.EMAIL) - internal companion object { - @Throws(JsonException::class) - fun fromJson(jsonValue: JsonValue): Platform { - val valueString = jsonValue.optString() - for (platform in entries) { - if (platform.jsonValue.equals(valueString, true)) { - return platform - } + internal val resendOptions: ResendOptions + get() { + return when (this) { + is Sms -> this.registrationOptions.resendOptions + is Email -> this.registrationOptions.resendOptions } - throw JsonException("Invalid platform: $valueString") } - } - internal fun toChannelType(): ChannelType = when (this) { - SMS -> ChannelType.SMS - EMAIL -> ChannelType.EMAIL - } + internal val errorMessages: ErrorMessages + get() { + return when (this) { + is Sms -> this.registrationOptions.errorMessages + is Email -> this.registrationOptions.errorMessages + } + } } public data class AddPrompt( @@ -254,6 +268,7 @@ public sealed class Item( val type: String, val display: PromptDisplay, val submitButton: LabeledButton, + val closeButton: IconButton?, val cancelButton: LabeledButton?, val onSubmit: ActionableMessage?, ) { @@ -263,6 +278,7 @@ public sealed class Item( KEY_DISPLAY to display.toJson(), KEY_SUBMIT_BUTTON to submitButton.toJson(), KEY_CANCEL_BUTTON to cancelButton?.toJson(), + KEY_CLOSE_BUTTON to closeButton?.toJson(), KEY_ON_SUBMIT to onSubmit?.toJson(), ) @@ -273,6 +289,7 @@ public sealed class Item( type = json.requireField(KEY_TYPE), display = PromptDisplay.fromJson(json.requireField(KEY_DISPLAY)), submitButton = json.requireMap(KEY_SUBMIT_BUTTON).let { LabeledButton.fromJson(it) }, + closeButton = json.optionalMap(KEY_CLOSE_BUTTON)?.let { IconButton.fromJson(it) }, cancelButton = json.optionalMap(KEY_CANCEL_BUTTON)?.let { LabeledButton.fromJson(it) }, onSubmit = json.optionalMap(KEY_ON_SUBMIT)?.let { ActionableMessage.fromJson(it) }, ) @@ -284,6 +301,7 @@ public sealed class Item( val type: String, val display: PromptDisplay, val submitButton: LabeledButton, + val closeButton: IconButton?, val cancelButton: LabeledButton?, val onSubmit: ActionableMessage?, ) { @@ -293,6 +311,7 @@ public sealed class Item( KEY_DISPLAY to display.toJson(), KEY_SUBMIT_BUTTON to submitButton.toJson(), KEY_CANCEL_BUTTON to cancelButton?.toJson(), + KEY_CLOSE_BUTTON to closeButton?.toJson(), KEY_ON_SUBMIT to onSubmit?.toJson(), ) @@ -303,6 +322,7 @@ public sealed class Item( type = json.requireField(KEY_TYPE), display = PromptDisplay.fromJson(json.requireField(KEY_DISPLAY)), submitButton = json.requireMap(KEY_SUBMIT_BUTTON).let { LabeledButton.fromJson(it) }, + closeButton = json.optionalMap(KEY_CLOSE_BUTTON)?.let { IconButton.fromJson(it) }, cancelButton = json.optionalMap(KEY_CANCEL_BUTTON)?.let { LabeledButton.fromJson(it) }, onSubmit = json.optionalMap(KEY_ON_SUBMIT)?.let { ActionableMessage.fromJson(it) }, ) @@ -310,50 +330,16 @@ public sealed class Item( } } - public data class FormattedText( - val text: String, - val type: FormatType - ) { - public enum class FormatType(public val value: String) { - PLAIN("string"), - MARKDOWN("markdown"), - UNKNOWN("unknown"); - - internal companion object { - fun from(value: String): FormatType = - entries.firstOrNull { it.value.equals(value, true) } ?: UNKNOWN - } - } - - internal val isMarkdown = type == FormatType.MARKDOWN - - @Throws(JsonException::class) - public fun toJson(): JsonMap = jsonMapOf( - KEY_DESCRIPTION to text, - KEY_TYPE to type.value - ) - - internal companion object { - @Throws(JsonException::class) - fun fromJson(json: JsonMap): FormattedText { - return FormattedText( - text = json.requireField(KEY_DESCRIPTION), - type = FormatType.from(json.requireField(KEY_TYPE)) - ) - } - } - } - public data class PromptDisplay( val title: String, val description: String?, - val footer: FormattedText?, + val footer: String?, ) { @Throws(JsonException::class) public fun toJson(): JsonMap = jsonMapOf( KEY_TITLE to title, KEY_DESCRIPTION to description, - KEY_FOOTER to footer?.toJson() + KEY_FOOTER to footer ) internal companion object { @@ -365,7 +351,7 @@ public sealed class Item( return PromptDisplay( title = json.requireField(KEY_TITLE), description = json.optionalField(KEY_DESCRIPTION), - footer = json.optionalMap(KEY_FOOTER)?.let { FormattedText.fromJson(it) }, + footer = json.optionalField(KEY_FOOTER), ) } } @@ -475,12 +461,13 @@ public sealed class Item( KEY_INTERVAL to interval, KEY_MESSAGE to message, KEY_BUTTON to button.toJson(), - KEY_ON_SUBMIT to onSuccess?.toJson() + KEY_ON_SUCCESS to onSuccess?.toJson() ) internal companion object { private const val KEY_INTERVAL = "interval" private const val KEY_MESSAGE = "message" + private const val KEY_ON_SUCCESS = "on_success" @Throws(JsonException::class) fun fromJson(json: JsonMap): ResendOptions { @@ -488,7 +475,7 @@ public sealed class Item( interval = json.requireField(KEY_INTERVAL), message = json.requireField(KEY_MESSAGE), button = LabeledButton.fromJson(json.requireField(KEY_BUTTON)), - onSuccess = json.optionalMap(KEY_ON_SUBMIT)?.let { ActionableMessage.fromJson(it) } + onSuccess = json.optionalMap(KEY_ON_SUCCESS)?.let { ActionableMessage.fromJson(it) } ) } } @@ -572,17 +559,6 @@ public sealed class Item( } } } - - internal companion object { - @Throws(JsonException::class) - fun fromJson(json: JsonMap): RegistrationOptions { - return when (val type = json.requireField(KEY_TYPE)) { - Platform.SMS.jsonValue -> Sms.fromJson(json) - Platform.EMAIL.jsonValue -> Email.fromJson(json) - else -> throw JsonException("Invalid registration type: $type") - } - } - } } public data class SmsSenderInfo( @@ -644,6 +620,7 @@ public sealed class Item( private const val KEY_ON_SUBMIT = "on_submit" private const val KEY_CANCEL_BUTTON = "cancel_button" private const val KEY_SUBMIT_BUTTON = "submit_button" + private const val KEY_CLOSE_BUTTON = "close_button" private const val KEY_NAME = "name" private const val KEY_DESCRIPTION = "description" @@ -695,16 +672,7 @@ public sealed class Item( button = Button.parse(json.optionalField(KEY_BUTTON)), conditions = Condition.parse(json.get(KEY_CONDITIONS)) ) - TYPE_CONTACT_MANAGEMENT -> ContactManagement( - id = id, - platform = ContactManagement.Platform.fromJson(json.requireField(KEY_PLATFORM)), - display = CommonDisplay.parse(json.get(KEY_DISPLAY)), - emptyLabel = json.optionalField(KEY_EMPTY_LABEL), - conditions = Condition.parse(json.get(KEY_CONDITIONS)), - addPrompt = ContactManagement.AddPrompt.fromJson(json.requireField(KEY_ADD)), - removePrompt = ContactManagement.RemovePrompt.fromJson(json.requireField(KEY_REMOVE)), - registrationOptions = ContactManagement.RegistrationOptions.fromJson(json.requireField(KEY_REGISTRATION_OPTIONS)) - ) + TYPE_CONTACT_MANAGEMENT -> ContactManagement.fromJson(json) else -> throw JsonException("Unknown Preference Center Item type: '$type'") } } diff --git a/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/ui/PreferenceCenterFragment.kt b/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/ui/PreferenceCenterFragment.kt index 1023a9c2a..ed33d8b26 100644 --- a/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/ui/PreferenceCenterFragment.kt +++ b/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/ui/PreferenceCenterFragment.kt @@ -235,7 +235,7 @@ public class PreferenceCenterFragment : Fragment(R.layout.ua_fragment_preference is Effect.ShowContactManagementRemoveDialog -> showContactManagementRemoveDialog(effect.item, effect.channel, viewModel::handle) is Effect.ShowChannelVerificationResentDialog -> - effect.item.registrationOptions.resendOptions.onSuccess?.let { message -> + effect.item.platform.resendOptions.onSuccess?.let { message -> showContactManagementResentDialog(message) } Effect.DismissContactManagementAddDialog -> diff --git a/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/ui/PreferenceCenterViewModel.kt b/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/ui/PreferenceCenterViewModel.kt index ebe9f74d2..8d1852137 100644 --- a/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/ui/PreferenceCenterViewModel.kt +++ b/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/ui/PreferenceCenterViewModel.kt @@ -206,7 +206,7 @@ internal class PreferenceCenterViewModel @JvmOverloads constructor( ) } } else { - val message = action.item.registrationOptions.errorMessages.invalidMessage + val message = action.item.platform.errorMessages.invalidMessage Effect.ShowContactManagementAddDialogError(message) } ) @@ -225,14 +225,14 @@ internal class PreferenceCenterViewModel @JvmOverloads constructor( } ?: emptyFlow() } is Action.RegisterChannel.Email -> { - val options = action.item.registrationOptions as? RegistrationOptions.Email + val emailPlatform = action.item.platform as? Item.ContactManagement.Platform.Email contact.registerEmail( action.address, EmailRegistrationOptions.options( transactionalOptedIn = null, doubleOptIn = true, - properties = options?.properties + properties = emailPlatform?.registrationOptions?.properties ) ) @@ -251,7 +251,7 @@ internal class PreferenceCenterViewModel @JvmOverloads constructor( ContactChannelState(showPendingButton = true, showResendButton = false) )) - val resendInterval = action.item.registrationOptions.resendOptions.interval.seconds + val resendInterval = action.item.platform.resendOptions.interval.seconds val resendDelay = resendInterval.coerceAtLeast(defaultResendLabelHideDelay) delay(resendDelay) diff --git a/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/ui/item/ContactManagementItem.kt b/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/ui/item/ContactManagementItem.kt index c78770a4e..df79d9d6c 100644 --- a/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/ui/item/ContactManagementItem.kt +++ b/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/ui/item/ContactManagementItem.kt @@ -92,7 +92,7 @@ internal data class ContactManagementItem( descriptionView.setTextOrHide(item.display.description) val channels = contactChannelsProvider.get() - .filter { it.key.channelType == item.platform.toChannelType() } + .filter { it.key.channelType == item.platform.channelType } widget.removeAllViews() if (channels.isEmpty()) { @@ -164,7 +164,7 @@ internal data class ContactManagementItem( item.removePrompt.button.contentDescription?.let { contentDescription = it } } - val resendOptions = item.registrationOptions.resendOptions + val resendOptions = item.platform.resendOptions // Set up pending view pending.isVisible = if (state.showPendingButton) { @@ -212,8 +212,8 @@ internal data class ContactManagementItem( } } - val resendDescription = item.registrationOptions.resendOptions.button.contentDescription - ?: item.registrationOptions.resendOptions.button.text + val resendDescription = item.platform.resendOptions.button.contentDescription + ?: item.platform.resendOptions.button.text val clickAction = if (!isOptedIn) { resendDescription } else null @@ -245,8 +245,8 @@ internal data class ContactManagementItem( } private fun platformDescription(platform: Platform): String = when (platform) { - Platform.EMAIL -> context.getString(R.string.ua_preference_center_contact_management_email_description) - Platform.SMS -> context.getString(R.string.ua_preference_center_contact_management_sms_description) + is Platform.Email -> context.getString(R.string.ua_preference_center_contact_management_email_description) + is Platform.Sms -> context.getString(R.string.ua_preference_center_contact_management_sms_description) } private fun addressDescription(maskedAddress: String) = @@ -256,14 +256,12 @@ internal data class ContactManagementItem( ) @DrawableRes - private fun itemIcon(item: Item.ContactManagement): Int { - return if (item.platform == Platform.EMAIL) { - R.drawable.ua_ic_preference_center_email - } else { - R.drawable.ua_ic_preference_center_phone - } + private fun itemIcon(item: Item.ContactManagement): Int = when (item.platform) { + is Platform.Email -> R.drawable.ua_ic_preference_center_email + is Platform.Sms -> R.drawable.ua_ic_preference_center_phone } + private companion object { private val contentDescriptionRedactedPattern = """\*+""".toRegex() } diff --git a/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/widget/ContactChannelDialogInputView.kt b/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/widget/ContactChannelDialogInputView.kt index 263e98497..dc706f02f 100644 --- a/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/widget/ContactChannelDialogInputView.kt +++ b/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/widget/ContactChannelDialogInputView.kt @@ -81,16 +81,16 @@ internal class ContactChannelDialogInputView@JvmOverloads constructor( ArrayAdapter(context, android.R.layout.simple_dropdown_item_1line) } - private var options: RegistrationOptions? = null + private var platform: Item.ContactManagement.Platform? = null private var selectedSender: SmsSenderInfo? = null private val validator: (input: String?) -> Boolean = { input -> - when (options) { - is RegistrationOptions.Email -> { + when (platform) { + is Item.ContactManagement.Platform.Email -> { val formatted = formatEmail(input) !input.isNullOrBlank() && emailRegex.matches(formatted) } - is RegistrationOptions.Sms -> { + is Item.ContactManagement.Platform.Sms -> { val formatted = formatPhone(selectedSender?.dialingCode, input) !input.isNullOrBlank() && phoneRegex.matches(formatted) } @@ -98,19 +98,19 @@ internal class ContactChannelDialogInputView@JvmOverloads constructor( } } - fun setOptions(options: RegistrationOptions, display: PromptDisplay) { - this.options = options + fun setPlatform(platform: Item.ContactManagement.Platform, display: PromptDisplay) { + this.platform = platform - when (options) { - is RegistrationOptions.Email -> { - setAddressLabel(options.addressLabel) - options.placeholder?.let(::setAddressPlaceholder) + when (platform) { + is Item.ContactManagement.Platform.Email -> { + setAddressLabel(platform.registrationOptions.addressLabel) + platform.registrationOptions.placeholder?.let(::setAddressPlaceholder) textInputView.editText?.inputType = InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS } - is RegistrationOptions.Sms -> { - setAddressLabel(options.phoneLabel) - setCountryPickerLabel(options.countryLabel) - setCountryCodes(options.senders) + is Item.ContactManagement.Platform.Sms -> { + setAddressLabel(platform.registrationOptions.phoneLabel) + setCountryPickerLabel(platform.registrationOptions.countryLabel) + setCountryCodes(platform.registrationOptions.senders) textInputView.editText?.inputType = InputType.TYPE_CLASS_PHONE } } @@ -129,9 +129,9 @@ internal class ContactChannelDialogInputView@JvmOverloads constructor( fun getResult(): DialogResult? { val address = getFormattedAddress() ?: return null - return when (options) { - is RegistrationOptions.Email -> DialogResult.Email(address = address) - is RegistrationOptions.Sms -> selectedSender?.senderId?.let { senderId -> + return when (platform) { + is Item.ContactManagement.Platform.Email -> DialogResult.Email(address = address) + is Item.ContactManagement.Platform.Sms -> selectedSender?.senderId?.let { senderId -> DialogResult.Sms(address = address, senderId = senderId) } else -> null @@ -187,13 +187,8 @@ internal class ContactChannelDialogInputView@JvmOverloads constructor( countryPickerInputView.hint = text } - private fun setFooter(formattedText: Item.ContactManagement.FormattedText) { - if (formattedText.isMarkdown) { - footerView.setHtml(formattedText.text.markdownToHtml()) - } else { - footerView.text = formattedText.text - } - footerView.isVisible = true + private fun setFooter(formattedText: String) { + footerView.setHtml(formattedText.markdownToHtml()) } private companion object { diff --git a/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/widget/ContactChannelManagementDialogs.kt b/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/widget/ContactChannelManagementDialogs.kt index f06e339a0..13d402a38 100644 --- a/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/widget/ContactChannelManagementDialogs.kt +++ b/urbanairship-preference-center/src/main/java/com/urbanairship/preferencecenter/widget/ContactChannelManagementDialogs.kt @@ -32,7 +32,7 @@ internal fun PreferenceCenterFragment.showContactManagementAddDialog( val cancelButtonLabel = view.cancelButton?.text ?: context.getString(R.string.ua_cancel) val inputView = ContactChannelDialogInputView(context).apply { - setOptions(item.registrationOptions, view.display) + setPlatform(item.platform, view.display) } val dialog = MaterialAlertDialogBuilder(context) @@ -52,7 +52,7 @@ internal fun PreferenceCenterFragment.showContactManagementAddDialog( // We shouldn't get null here, since the submit button is only enabled once // validation passes, but just in case... UALog.e { "Add contact channel dialog result was null!" } - inputView.setError(item.registrationOptions.errorMessages.defaultMessage) + inputView.setError(item.platform.errorMessages.defaultMessage) } else { // Map the dialog result to an action and pass it back to the Fragment to update // the ViewModel. diff --git a/urbanairship-preference-center/src/test/java/com/urbanairship/preferencecenter/data/PreferenceCenterConfigTest.kt b/urbanairship-preference-center/src/test/java/com/urbanairship/preferencecenter/data/PreferenceCenterConfigTest.kt new file mode 100644 index 000000000..b381e4e8f --- /dev/null +++ b/urbanairship-preference-center/src/test/java/com/urbanairship/preferencecenter/data/PreferenceCenterConfigTest.kt @@ -0,0 +1,227 @@ +package com.urbanairship.preferencecenter.data + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.urbanairship.json.JsonValue +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +public class PreferenceCenterConfigTest { + + @Test + public fun testParse() { + val json = JsonValue.parseString(json) + val result = PreferenceCenterConfig.parse(json.requireMap()) + assertNotNull(result) + } + + private val json = """ + { + "id": "cool-prefs", + "display": { + "name": "Cool Prefs", + "description": "Preferences but they're cool" + }, + "sections": [ + { + "id": "a2db6801-c766-44d7-b5d6-070ca64421b2", + "type": "section", + "items": [ + { + "id": "a2db6801-c766-44d7-b5d6-070ca64421b3", + "type": "contact_management", + "platform": "email", + "display": { + "name": "Email Addresses", + "description": "Addresses associated with your account." + }, + "registration_options": { + "address_label": "Email address", + "resend": { + "interval": 1000, + "message": "Pending verification", + "button": { + "text": "Resend", + "content_description": "Resend a verification message to this email address" + }, + "on_success": { + "name": "Verification resent", + "description": "Check your inbox for a new confirmation email.", + "button": { + "text": "Ok", + "content_description": "Close prompt" + } + } + }, + "error_messages": { + "invalid": "Please enter a valid email address.", + "default": "Uh oh, something went wrong." + } + }, + "add": { + "button": { + "text": "Add email", + "content_description": "Add a new email address" + }, + "view": { + "type": "prompt", + "display": { + "title": "Add an email address", + "description": "You will receive a confirmation email to verify your address.", + "footer": "Does anyone read our [Terms and Conditions](https://example.com) and [Privacy Policy](https://example.com)?" + }, + "submit_button": { + "text": "Send", + "content_description": "Send a message to this email address" + }, + "cancel_button": { + "text": "Cancel" + }, + "close_button": { + "content_description": "Close" + }, + "on_submit": { + "name": "Oh no, it worked.", + "description": "Hope you like emails.", + "button": { + "text": "Ok, dang" + } + } + } + }, + "remove": { + "button": { + "content_description": "Opt out and remove this email address" + }, + "view": { + "type": "prompt", + "display": { + "title": "Remove email address?", + "description": "I thought you liked emails." + }, + "submit_button": { + "text": "Yes", + "content_description": "Confirm opt out" + }, + "cancel_button": { + "text": "No", + "content_description": "Cancel opt out" + }, + "close_button": { + "content_description": "Close" + }, + "on_submit": { + "name": "Success", + "description": "Bye!", + "button": { + "text": "Ok", + "content_description": "Close prompt" + } + } + } + } + }, + { + "id": "a2db6801-c766-44d7-b5d6-070ca64421b4", + "type": "contact_management", + "platform": "sms", + "display": { + "name": "Mobile Numbers" + }, + "registration_options": { + "country_label": "Country", + "msisdn_label": "Phone number", + "resend": { + "interval": 1000, + "message": "Pending verification", + "button": { + "text": "Resend", + "content_description": "Resend a verification message to this phone number" + } + }, + "senders": [ + { + "country_code": "+44", + "display_name": "United Kingdom", + "placeholder_text": "7010 111222", + "sender_id": "23450" + } + ], + "error_messages": { + "invalid": "Please enter a valid phone number.", + "default": "Uh oh, something went wrong." + } + }, + "add": { + "view": { + "type": "prompt", + "display": { + "title": "Add a phone number", + "description": "You will receive a text message with further details.", + "footer": "By opting in you give us the OK to hound you forever." + }, + "submit_button": { + "text": "Send", + "content_description": "Send a message to this phone number" + }, + "cancel_button": { + "text": "Cancel" + }, + "close_button": { + "content_description": "Close" + }, + "on_submit": { + "name": "Oh no, it worked.", + "description": "Hope you like text messages.", + "button": { + "text": "Ok, dang" + } + } + }, + "button": { + "text": "Add SMS", + "content_description": "Add a new phone number" + } + }, + "remove": { + "button": { + "content_description": "Opt out and remove this phone number" + }, + "view": { + "type": "prompt", + "display": { + "title": "Remove phone number?", + "description": "Your phone will buzz less." + }, + "submit_button": { + "text": "Yes", + "content_description": "Confirm opt out" + }, + "cancel_button": { + "text": "No", + "content_description": "Cancel opt out" + }, + "close_button": { + "content_description": "Close" + }, + "on_submit": { + "name": "Success", + "description": "Bye!", + "button": { + "text": "Ok", + "content_description": "Close prompt" + } + } + } + } + } + ] + } + ] + } + """.trimIndent() + + +} diff --git a/urbanairship-preference-center/src/test/java/com/urbanairship/preferencecenter/ui/PreferenceCenterViewModelTest.kt b/urbanairship-preference-center/src/test/java/com/urbanairship/preferencecenter/ui/PreferenceCenterViewModelTest.kt index 242f3dd86..abfbad3f9 100644 --- a/urbanairship-preference-center/src/test/java/com/urbanairship/preferencecenter/ui/PreferenceCenterViewModelTest.kt +++ b/urbanairship-preference-center/src/test/java/com/urbanairship/preferencecenter/ui/PreferenceCenterViewModelTest.kt @@ -954,9 +954,11 @@ public class PreferenceCenterViewModelTest { val mockItem: Item.ContactManagement = mockk { every { addPrompt } returns mockk(relaxed = true) - every { registrationOptions } returns mockk { - every { properties } returns optInProperties - } + every { platform } returns Item.ContactManagement.Platform.Email( + registrationOptions = mockk { + every { properties } returns optInProperties + } + ) } viewModel().run { @@ -992,9 +994,11 @@ public class PreferenceCenterViewModelTest { public fun testValidateSmsChannelValid(): TestResult = runTest { val item: Item.ContactManagement = mockk { every { addPrompt } returns mockk(relaxed = true) - every { registrationOptions } returns mockk { - every { errorMessages } returns mockk(relaxed = true) - } + every { platform } returns Item.ContactManagement.Platform.Sms( + registrationOptions = mockk { + every { errorMessages } returns mockk(relaxed = true) + } + ) } val address = "15031112222" val senderId = "123456" @@ -1024,12 +1028,13 @@ public class PreferenceCenterViewModelTest { val invalidMessage = "Invalid message" val item: Item.ContactManagement = mockk { every { addPrompt } returns mockk(relaxed = true) - every { registrationOptions } returns mockk { - every { errorMessages } returns Item.ContactManagement.ErrorMessages( - invalidMessage = invalidMessage, - defaultMessage = "Default message" - ) - } + every { platform } returns Item.ContactManagement.Platform.Sms( + registrationOptions = mockk { + every { errorMessages } returns Item.ContactManagement.ErrorMessages( + invalidMessage = invalidMessage, defaultMessage = "Default message" + ) + } + ) } val address = "15031112222" val senderId = "123456" @@ -1057,12 +1062,14 @@ public class PreferenceCenterViewModelTest { @Test public fun testResendChannelVerification(): TestResult = runTest { val item: Item.ContactManagement = mockk { - every { registrationOptions } returns mockk { - every { properties } returns null - every { resendOptions } returns mockk { - every { interval } returns 3 + every { platform } returns Item.ContactManagement.Platform.Email( + registrationOptions = mockk { + every { properties } returns null + every { resendOptions } returns mockk { + every { interval } returns 3 + } } - } + ) } val contactChannel: ContactChannel = mockk { }