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
2 changes: 2 additions & 0 deletions OneSignalSDK/onesignal/core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ public final class com/onesignal/OneSignalUserProfile {
public fun equals (Ljava/lang/Object;)Z
public final fun getAliases ()Ljava/util/Map;
public final fun getEmail ()Ljava/lang/String;
public final fun getEmailEnabled ()Ljava/lang/Boolean;
public final fun getSms ()Ljava/lang/String;
public final fun getTags ()Ljava/util/Map;
public fun hashCode ()I
Expand All @@ -205,6 +206,7 @@ public final class com/onesignal/OneSignalUserProfile$Builder {
public final fun build ()Lcom/onesignal/OneSignalUserProfile;
public final fun setAliases (Ljava/util/Map;)Lcom/onesignal/OneSignalUserProfile$Builder;
public final fun setEmail (Ljava/lang/String;)Lcom/onesignal/OneSignalUserProfile$Builder;
public final fun setEmailEnabled (Z)Lcom/onesignal/OneSignalUserProfile$Builder;
public final fun setSms (Ljava/lang/String;)Lcom/onesignal/OneSignalUserProfile$Builder;
public final fun setTags (Ljava/util/Map;)Lcom/onesignal/OneSignalUserProfile$Builder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import java.util.Collections
class OneSignalUserProfile private constructor(builder: Builder) {
val email: String? = builder.email?.takeIf { it.isNotBlank() }
val sms: String? = builder.sms?.takeIf { it.isNotBlank() }

/** False stores the email unsubscribed. True is not sent. Null omits status. */
val emailEnabled: Boolean? = builder.emailEnabled
val tags: Map<String, String> = Collections.unmodifiableMap(builder.tags.toMap())
val aliases: Map<String, String> = Collections.unmodifiableMap(builder.aliases.toMap())

Expand Down Expand Up @@ -39,13 +42,15 @@ class OneSignalUserProfile private constructor(builder: Builder) {
if (other !is OneSignalUserProfile) return false
return email == other.email &&
sms == other.sms &&
emailEnabled == other.emailEnabled &&
tags == other.tags &&
aliases == other.aliases
}

override fun hashCode(): Int {
var result = email?.hashCode() ?: 0
result = 31 * result + (sms?.hashCode() ?: 0)
result = 31 * result + (emailEnabled?.hashCode() ?: 0)
result = 31 * result + tags.hashCode()
result = 31 * result + aliases.hashCode()
return result
Expand All @@ -60,6 +65,8 @@ class OneSignalUserProfile private constructor(builder: Builder) {
private set
internal var sms: String? = null
private set
internal var emailEnabled: Boolean? = null
private set
internal var tags: Map<String, String> = emptyMap()
private set
internal var aliases: Map<String, String> = emptyMap()
Expand All @@ -68,6 +75,9 @@ class OneSignalUserProfile private constructor(builder: Builder) {
/** Email to create or transfer onto this user at login. */
fun setEmail(email: String?) = apply { this.email = email }

/** Store the email unsubscribed when false. True is not sent on Create User. */
fun setEmailEnabled(enabled: Boolean) = apply { this.emailEnabled = enabled }

/** SMS to create or transfer onto this user at login. Must be E.164 (e.g. +14155552671). */
fun setSms(sms: String?) = apply { this.sms = sms }

Expand All @@ -92,7 +102,10 @@ class OneSignalUserProfile private constructor(builder: Builder) {
sms: String? = null,
tags: Map<String, String> = emptyMap(),
aliases: Map<String, String> = emptyMap(),
) = Builder().setEmail(email).setSms(sms).setTags(tags).setAliases(aliases).build()
emailEnabled: Boolean? = null,
) = Builder().setEmail(email).setSms(sms).setTags(tags).setAliases(aliases).apply {
if (emailEnabled != null) setEmailEnabled(emailEnabled)
}.build()

private fun redact(value: String?): String = if (value == null) "null" else "<set>"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ class LoginUserOperation() : Operation(LoginUserOperationExecutor.LOGIN_USER) {
setOptStringProperty(PROFILE_SMS, value)
}

internal var emailEnabled: Boolean?
get() = getOptBooleanProperty(PROFILE_EMAIL_ENABLED)
set(value) {
setOptBooleanProperty(PROFILE_EMAIL_ENABLED, value)
}

internal var tags: Map<String, String>
get() = decodeMap(getOptStringProperty(PROFILE_TAGS))
set(value) {
Expand Down Expand Up @@ -93,6 +99,7 @@ class LoginUserOperation() : Operation(LoginUserOperationExecutor.LOGIN_USER) {
if (profile != null) {
this.email = profile.email?.takeIf { it.isNotBlank() }
this.sms = profile.sms?.takeIf { it.isNotBlank() }
this.emailEnabled = profile.emailEnabled
this.tags = profile.tags
this.aliases = profile.aliases
}
Expand All @@ -102,7 +109,10 @@ class LoginUserOperation() : Operation(LoginUserOperationExecutor.LOGIN_USER) {
!email.isNullOrBlank() || !sms.isNullOrBlank() || tags.isNotEmpty() || aliases.isNotEmpty()

internal fun mergeProfileFrom(other: LoginUserOperation) {
if (!other.email.isNullOrBlank()) email = other.email
if (!other.email.isNullOrBlank()) {
email = other.email
emailEnabled = other.emailEnabled
}
if (!other.sms.isNullOrBlank()) sms = other.sms
if (other.tags.isNotEmpty()) tags = tags + other.tags
if (other.aliases.isNotEmpty()) aliases = aliases + other.aliases
Expand All @@ -128,6 +138,7 @@ internal fun reservedLoginAliasLabel(label: String): Boolean =
label.isBlank() || label == IdentityConstants.ONESIGNAL_ID || label == IdentityConstants.EXTERNAL_ID

private const val PROFILE_EMAIL = "profileEmail"
private const val PROFILE_EMAIL_ENABLED = "profileEmailEnabled"
private const val PROFILE_SMS = "profileSms"
private const val PROFILE_TAGS = "profileTags"
private const val PROFILE_ALIASES = "profileAliases"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@ internal object LoginProfileApplier {
fun addSubscriptions(
op: LoginUserOperation,
subscriptions: Map<String, SubscriptionObject>,
buildProfileSubscription: (SubscriptionObjectType, String) -> SubscriptionObject,
): Map<String, SubscriptionObject> {
val mutable = subscriptions.toMutableMap()
val email = op.email?.takeIf { it.isNotBlank() }
if (email != null && mutable.values.none { it.type == SubscriptionObjectType.EMAIL && it.token == email }) {
mutable[PROFILE_EMAIL_KEY] = SubscriptionObject(type = SubscriptionObjectType.EMAIL, token = email)
mutable[PROFILE_EMAIL_KEY] = buildProfileSubscription(SubscriptionObjectType.EMAIL, email)
}
val sms = op.sms?.takeIf { it.isNotBlank() }
if (sms != null && mutable.values.none { it.type == SubscriptionObjectType.SMS && it.token == sms }) {
mutable[PROFILE_SMS_KEY] = SubscriptionObject(type = SubscriptionObjectType.SMS, token = sms)
mutable[PROFILE_SMS_KEY] = buildProfileSubscription(SubscriptionObjectType.SMS, sms)
}
return mutable
}
Expand Down Expand Up @@ -73,6 +74,7 @@ internal object LoginProfileApplier {
backend: SubscriptionObject,
fallbackToken: String?,
subscriptionsModelStore: SubscriptionModelStore,
optedIn: Boolean = true,
) {
val id = backend.id
val token = backend.token ?: fallbackToken
Expand All @@ -93,8 +95,8 @@ internal object LoginProfileApplier {
model.id = id
model.type = type
model.address = token
model.status = SubscriptionStatus.SUBSCRIBED
model.optedIn = true
model.status = if (optedIn) SubscriptionStatus.SUBSCRIBED else SubscriptionStatus.UNSUBSCRIBE
model.optedIn = optedIn
subscriptionsModelStore.add(model, ModelChangeTags.HYDRATE)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ import com.onesignal.user.internal.properties.PropertiesModel
import com.onesignal.user.internal.properties.PropertiesModelStore
import com.onesignal.user.internal.subscriptions.SubscriptionModel
import com.onesignal.user.internal.subscriptions.SubscriptionModelStore
import com.onesignal.user.internal.subscriptions.SubscriptionStatus
import com.onesignal.user.internal.subscriptions.SubscriptionType

@Suppress("TooManyFunctions")
internal class LoginUserOperationExecutor(
private val _identityOperationExecutor: IdentityOperationExecutor,
private val _application: IApplicationService,
Expand Down Expand Up @@ -208,7 +210,10 @@ internal class LoginUserOperationExecutor(
else -> throw Exception("Unrecognized operation: $operation")
}
}
subscriptions = LoginProfileApplier.addSubscriptions(createUserOperation, subscriptions)
subscriptions =
LoginProfileApplier.addSubscriptions(createUserOperation, subscriptions) { type, token ->
profileSubscription(type, token, createUserOperation.emailEnabled)
}

try {
val subscriptionList = subscriptions.toList()
Expand Down Expand Up @@ -259,6 +264,7 @@ internal class LoginUserOperationExecutor(
backendSubscription,
LoginProfileApplier.fallbackToken(backendSubscription.type, createUserOperation),
_subscriptionsModelStore,
optedIn = !emailStoredUnsubscribed(backendSubscription.type, createUserOperation),
)
}
when (LoginProfileApplier.subscriptionType(backendSubscription.type)) {
Expand Down Expand Up @@ -293,14 +299,6 @@ internal class LoginUserOperationExecutor(
Logging.warn("LoginUserOperationExecutor: skipped profile hydration because the current identity is not the created user")
}

if (!_identityVerificationService.ivBehaviorActive &&
(!createUserOperation.email.isNullOrBlank() || !createUserOperation.sms.isNullOrBlank())
) {
Logging.warn(
"LoginUserOperationExecutor: email or SMS sent without identity verification. If that address belonged to another user it was transferred.",
)
}

val wasPossiblyAnUpsert = identities.isNotEmpty()
val followUpOperations =
if (wasPossiblyAnUpsert) {
Expand Down Expand Up @@ -377,24 +375,60 @@ internal class LoginUserOperationExecutor(
}
val subId = if (!IDManager.isLocalId(operation.subscriptionId)) operation.subscriptionId else null
mutableSubscriptions[operation.subscriptionId] =
SubscriptionObject(
id = subId,
subscriptionWithDevice(
subscriptionType,
operation.address,
operation.enabled,
operation.status.value,
OneSignalUtils.sdkVersion,
Build.MODEL,
Build.VERSION.RELEASE,
RootToolsInternalMethods.isRooted,
DeviceUtils.getNetType(_application.appContext),
DeviceUtils.getCarrierName(_application.appContext),
AndroidUtils.getAppVersion(_application.appContext),
subId,
)

return mutableSubscriptions
}

// Create User applies a subscribed status to an address that already opted out.
// Send enabled false only when the customer asked, so they can collect the email
// without marketing consent. Anything else stays omitted.
private fun profileSubscription(
type: SubscriptionObjectType,
token: String,
emailEnabled: Boolean?,
): SubscriptionObject {
val unsubscribed = type == SubscriptionObjectType.EMAIL && emailEnabled == false
return subscriptionWithDevice(
type,
token,
enabled = if (unsubscribed) false else null,
notificationTypes = if (unsubscribed) SubscriptionStatus.UNSUBSCRIBE.value else null,
)
}

private fun emailStoredUnsubscribed(
type: SubscriptionObjectType?,
operation: LoginUserOperation,
): Boolean = LoginProfileApplier.subscriptionType(type) == SubscriptionType.EMAIL && operation.emailEnabled == false

private fun subscriptionWithDevice(
type: SubscriptionObjectType,
token: String,
enabled: Boolean? = null,
notificationTypes: Int? = null,
id: String? = null,
) = SubscriptionObject(
id = id,
type = type,
token = token,
enabled = enabled,
notificationTypes = notificationTypes,
sdk = OneSignalUtils.sdkVersion,
deviceModel = Build.MODEL,
deviceOS = Build.VERSION.RELEASE,
rooted = RootToolsInternalMethods.isRooted,
netType = DeviceUtils.getNetType(_application.appContext),
carrier = DeviceUtils.getCarrierName(_application.appContext),
appVersion = AndroidUtils.getAppVersion(_application.appContext),
)

private fun createSubscriptionsFromOperation(
operation: UpdateSubscriptionOperation,
subscriptions: Map<String, SubscriptionObject>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,36 @@ class OneSignalUserProfileTests : FunSpec({

profile.email.shouldBeNull()
profile.sms.shouldBeNull()
profile.emailEnabled.shouldBeNull()
profile.tags shouldBe emptyMap()
profile.aliases shouldBe emptyMap()
profile.hasFields shouldBe false
}

test("emailEnabled false is kept and true is distinct from unset") {
val unsubscribed = OneSignalUserProfile(email = "bob@example.com", emailEnabled = false)
val subscribed = OneSignalUserProfile(email = "bob@example.com", emailEnabled = true)
val unset = OneSignalUserProfile(email = "bob@example.com")

unsubscribed.emailEnabled shouldBe false
subscribed.emailEnabled shouldBe true
unset.emailEnabled.shouldBeNull()
unsubscribed shouldBe OneSignalUserProfile(email = "bob@example.com", emailEnabled = false)
(unsubscribed == unset) shouldBe false
(unsubscribed == subscribed) shouldBe false
}

test("Java builder can store an email unsubscribed") {
val profile =
OneSignalUserProfile.builder()
.setEmail("bob@example.com")
.setEmailEnabled(false)
.build()

profile.email shouldBe "bob@example.com"
profile.emailEnabled shouldBe false
}

test("Kotlin named args set only the fields that were passed") {
val profile =
OneSignalUserProfile(
Expand Down
Loading
Loading