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
1 change: 1 addition & 0 deletions OneSignalSDK/detekt/detekt-baseline-core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
<ID>LongParameterList:IOutcomeEventsBackendService.kt$IOutcomeEventsBackendService$( appId: String, userId: String, subscriptionId: String, deviceType: String, direct: Boolean?, event: OutcomeEvent, )</ID>
<ID>LongParameterList:IUserBackendService.kt$IUserBackendService$( appId: String, aliasLabel: String, aliasValue: String, properties: PropertiesObject, refreshDeviceMetadata: Boolean, propertyiesDelta: PropertiesDeltasObject, jwt: String? = null, )</ID>
<ID>LongParameterList:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$( private val _identityOperationExecutor: IdentityOperationExecutor, private val _application: IApplicationService, private val _deviceService: IDeviceService, private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _languageContext: ILanguageContext, private val _jwtTokenStore: JwtTokenStore, private val _identityVerificationService: IdentityVerificationService, private val _consistencyManager: IConsistencyManager, )</ID>
<ID>LongParameterList:OneSignalUserProfile.kt$OneSignalUserProfile.Companion$( email: String? = null, sms: String? = null, tags: Map&lt;String, String> = emptyMap(), aliases: Map&lt;String, String> = emptyMap(), emailEnabled: Boolean? = null, smsEnabled: Boolean? = null, )</ID>
<ID>LongParameterList:OutcomeEventsController.kt$OutcomeEventsController$( private val _session: ISessionService, private val _influenceManager: IInfluenceManager, private val _outcomeEventsCache: IOutcomeEventsRepository, private val _outcomeEventsPreferences: IOutcomeEventsPreferences, private val _outcomeEventsBackend: IOutcomeEventsBackendService, private val _configModelStore: ConfigModelStore, private val _identityModelStore: IdentityModelStore, private val _subscriptionManager: ISubscriptionManager, private val _deviceService: IDeviceService, private val _time: ITime, )</ID>
<ID>LongParameterList:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$( private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _jwtTokenStore: JwtTokenStore, private val _identityVerificationService: IdentityVerificationService, )</ID>
<ID>LongParameterList:SubscriptionObject.kt$SubscriptionObject$( val id: String? = null, val type: SubscriptionObjectType? = null, val token: String? = null, val enabled: Boolean? = null, val notificationTypes: Int? = null, val sdk: String? = null, val deviceModel: String? = null, val deviceOS: String? = null, val rooted: Boolean? = null, val netType: Int? = null, val carrier: String? = null, val appVersion: String? = null, )</ID>
Expand Down
2 changes: 2 additions & 0 deletions OneSignalSDK/onesignal/core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ public final class com/onesignal/OneSignalUserProfile {
public final fun getEmail ()Ljava/lang/String;
public final fun getEmailEnabled ()Ljava/lang/Boolean;
public final fun getSms ()Ljava/lang/String;
public final fun getSmsEnabled ()Ljava/lang/Boolean;
public final fun getTags ()Ljava/util/Map;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
Expand All @@ -208,6 +209,7 @@ public final class com/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 setSmsEnabled (Z)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 @@ -13,8 +13,11 @@ 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. */
/** Subscription status for [email]. Null sends no status. See [Builder.setEmailEnabled]. */
val emailEnabled: Boolean? = builder.emailEnabled

/** Subscription status for [sms]. Null sends no status. See [Builder.setSmsEnabled]. */
val smsEnabled: Boolean? = builder.smsEnabled
val tags: Map<String, String> = Collections.unmodifiableMap(builder.tags.toMap())
val aliases: Map<String, String> = Collections.unmodifiableMap(builder.aliases.toMap())

Expand Down Expand Up @@ -43,6 +46,7 @@ class OneSignalUserProfile private constructor(builder: Builder) {
return email == other.email &&
sms == other.sms &&
emailEnabled == other.emailEnabled &&
smsEnabled == other.smsEnabled &&
tags == other.tags &&
aliases == other.aliases
}
Expand All @@ -51,13 +55,15 @@ class OneSignalUserProfile private constructor(builder: Builder) {
var result = email?.hashCode() ?: 0
result = 31 * result + (sms?.hashCode() ?: 0)
result = 31 * result + (emailEnabled?.hashCode() ?: 0)
result = 31 * result + (smsEnabled?.hashCode() ?: 0)
Comment thread
fadi-george marked this conversation as resolved.
result = 31 * result + tags.hashCode()
result = 31 * result + aliases.hashCode()
return result
}

override fun toString(): String =
"OneSignalUserProfile(email=${redact(email)}, sms=${redact(sms)}, tags=${tags.size}, aliases=${aliases.size})"
"OneSignalUserProfile(email=${redact(email)}, emailEnabled=$emailEnabled, sms=${redact(sms)}, " +
"smsEnabled=$smsEnabled, tags=${tags.size}, aliases=${aliases.size})"

/** Builds a [OneSignalUserProfile]. Unset fields stay empty. */
class Builder {
Expand All @@ -67,6 +73,8 @@ class OneSignalUserProfile private constructor(builder: Builder) {
private set
internal var emailEnabled: Boolean? = null
private set
internal var smsEnabled: Boolean? = null
private set
internal var tags: Map<String, String> = emptyMap()
private set
internal var aliases: Map<String, String> = emptyMap()
Expand All @@ -75,12 +83,25 @@ 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. */
/**
* Subscription status for the email. Subscribes when true, unsubscribes when false.
* Unset sends no status, so an address that already exists keeps the status it has,
* unless a pending [com.onesignal.user.IUserManager.addEmail] for it sends one.
* Ignored without [setEmail]. Cannot be returned to unset once called.
*/
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 }

/**
* Subscription status for the SMS number. Subscribes when true, unsubscribes when false.
* Unset sends no status, so a number that already exists keeps the status it has,
* unless a pending [com.onesignal.user.IUserManager.addSms] for it sends one.
* Ignored without [setSms]. Cannot be returned to unset once called.
*/
fun setSmsEnabled(enabled: Boolean) = apply { this.smsEnabled = enabled }

/** Sets tags to apply at login. The map is copied. */
fun setTags(tags: Map<String, String>) = apply { this.tags = tags.toMap() }

Expand All @@ -103,8 +124,10 @@ class OneSignalUserProfile private constructor(builder: Builder) {
tags: Map<String, String> = emptyMap(),
aliases: Map<String, String> = emptyMap(),
emailEnabled: Boolean? = null,
smsEnabled: Boolean? = null,
) = Builder().setEmail(email).setSms(sms).setTags(tags).setAliases(aliases).apply {
if (emailEnabled != null) setEmailEnabled(emailEnabled)
if (smsEnabled != null) setSmsEnabled(smsEnabled)
}.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 @@ -67,6 +67,12 @@ class LoginUserOperation() : Operation(LoginUserOperationExecutor.LOGIN_USER) {
setOptBooleanProperty(PROFILE_EMAIL_ENABLED, value)
}

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

internal var tags: Map<String, String>
get() = decodeMap(getOptStringProperty(PROFILE_TAGS))
set(value) {
Expand Down Expand Up @@ -100,6 +106,7 @@ class LoginUserOperation() : Operation(LoginUserOperationExecutor.LOGIN_USER) {
this.email = profile.email?.takeIf { it.isNotBlank() }
this.sms = profile.sms?.takeIf { it.isNotBlank() }
this.emailEnabled = profile.emailEnabled
this.smsEnabled = profile.smsEnabled
this.tags = profile.tags
this.aliases = profile.aliases
}
Expand All @@ -108,12 +115,25 @@ class LoginUserOperation() : Operation(LoginUserOperationExecutor.LOGIN_USER) {
internal fun hasProfileFields(): Boolean =
!email.isNullOrBlank() || !sms.isNullOrBlank() || tags.isNotEmpty() || aliases.isNotEmpty()

/**
* A status belongs to the address it was stated for. Keeping it across a change would
* apply an opt-out the caller never asked for on the new address.
*/
private fun mergedStatus(
sameAddress: Boolean,
current: Boolean?,
incoming: Boolean?,
): Boolean? = if (sameAddress) incoming ?: current else incoming

internal fun mergeProfileFrom(other: LoginUserOperation) {
if (!other.email.isNullOrBlank()) {
emailEnabled = mergedStatus(sameSubscriptionAddress(other.email, email, isEmail = true), emailEnabled, other.emailEnabled)
email = other.email
emailEnabled = other.emailEnabled
}
if (!other.sms.isNullOrBlank()) sms = other.sms
if (!other.sms.isNullOrBlank()) {
smsEnabled = mergedStatus(sameSubscriptionAddress(other.sms, sms, isEmail = false), smsEnabled, other.smsEnabled)
sms = other.sms
}
if (other.tags.isNotEmpty()) tags = tags + other.tags
if (other.aliases.isNotEmpty()) aliases = aliases + other.aliases
}
Expand All @@ -134,12 +154,20 @@ class LoginUserOperation() : Operation(LoginUserOperationExecutor.LOGIN_USER) {
}
}

/** Mailboxes are case-insensitive, phone numbers are not. One rule so every caller dedupes alike. */
internal fun sameSubscriptionAddress(
left: String?,
right: String?,
isEmail: Boolean,
): Boolean = if (isEmail) left.equals(right, ignoreCase = true) else left == right

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_SMS_ENABLED = "profileSmsEnabled"
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 @@ -8,6 +8,7 @@ import com.onesignal.user.internal.backend.SubscriptionObjectType
import com.onesignal.user.internal.identity.IdentityModelStore
import com.onesignal.user.internal.operations.LoginUserOperation
import com.onesignal.user.internal.operations.reservedLoginAliasLabel
import com.onesignal.user.internal.operations.sameSubscriptionAddress
import com.onesignal.user.internal.properties.PropertiesModel
import com.onesignal.user.internal.properties.PropertiesModelStore
import com.onesignal.user.internal.subscriptions.SubscriptionModel
Expand All @@ -21,23 +22,53 @@ internal object LoginProfileApplier {

fun isProfileSubscriptionKey(key: String): Boolean = key == PROFILE_EMAIL_KEY || key == PROFILE_SMS_KEY

/**
* [profileSubscription] receives the grouped entry for the same address when there is one, and
* returns null to leave it as it is.
*/
fun addSubscriptions(
op: LoginUserOperation,
subscriptions: Map<String, SubscriptionObject>,
buildProfileSubscription: (SubscriptionObjectType, String) -> SubscriptionObject,
profileSubscription: (SubscriptionObjectType, String, SubscriptionObject?) -> 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] = 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] = buildProfileSubscription(SubscriptionObjectType.SMS, sms)
for ((type, token, profileKey) in profileAddresses(op)) {
val grouped =
mutable.entries.firstOrNull {
it.value.type == type &&
sameSubscriptionAddress(it.value.token, token, isEmail = type == SubscriptionObjectType.EMAIL)
}
val subscription = profileSubscription(type, token, grouped?.value) ?: continue
mutable[grouped?.key ?: profileKey] = subscription
}
return mutable
}

private fun profileAddresses(op: LoginUserOperation): List<Triple<SubscriptionObjectType, String, String>> =
buildList {
op.email?.takeIf { it.isNotBlank() }?.let { add(Triple(SubscriptionObjectType.EMAIL, it, PROFILE_EMAIL_KEY)) }
op.sms?.takeIf { it.isNotBlank() }?.let { add(Triple(SubscriptionObjectType.SMS, it, PROFILE_SMS_KEY)) }
}

fun withStatus(
subscription: SubscriptionObject,
enabled: Boolean,
): SubscriptionObject =
SubscriptionObject(
id = subscription.id,
type = subscription.type,
token = subscription.token,
enabled = enabled,
notificationTypes = if (enabled) SubscriptionStatus.SUBSCRIBED.value else SubscriptionStatus.UNSUBSCRIBE.value,
sdk = subscription.sdk,
deviceModel = subscription.deviceModel,
deviceOS = subscription.deviceOS,
rooted = subscription.rooted,
netType = subscription.netType,
carrier = subscription.carrier,
appVersion = subscription.appVersion,
)

/**
* The backend wins where it spoke, the request fills the rest. Never deletes: Create User can
* echo a partial body, and dropping keys it omitted would discard state we just sent.
Expand Down Expand Up @@ -70,11 +101,12 @@ internal object LoginProfileApplier {
}
}

/** [optedIn] null means the caller stated no status, so an existing record keeps the one it has. */
fun persistSubscription(
backend: SubscriptionObject,
fallbackToken: String?,
subscriptionsModelStore: SubscriptionModelStore,
optedIn: Boolean = true,
optedIn: Boolean?,
) {
val id = backend.id
val token = backend.token ?: fallbackToken
Expand All @@ -89,17 +121,30 @@ internal object LoginProfileApplier {
}
if (existing != null) {
existing.setStringProperty(SubscriptionModel::id.name, id, ModelChangeTags.HYDRATE)
if (optedIn != null) applyStatus(existing, optedIn)
return
}
val model = SubscriptionModel()
model.id = id
model.type = type
model.address = token
model.status = if (optedIn) SubscriptionStatus.SUBSCRIBED else SubscriptionStatus.UNSUBSCRIBE
model.optedIn = optedIn
applyStatus(model, optedIn ?: true)
subscriptionsModelStore.add(model, ModelChangeTags.HYDRATE)
}

/**
* HYDRATE, not NORMAL: this records what Create User already applied, and a NORMAL write on an
* attached model would make [ModelStoreListener] enqueue a redundant subscription update.
*/
fun applyStatus(
model: SubscriptionModel,
optedIn: Boolean,
) {
val status = if (optedIn) SubscriptionStatus.SUBSCRIBED else SubscriptionStatus.UNSUBSCRIBE
model.setEnumProperty(SubscriptionModel::status.name, status, ModelChangeTags.HYDRATE)
model.setBooleanProperty(SubscriptionModel::optedIn.name, optedIn, ModelChangeTags.HYDRATE)
}

fun subscriptionType(type: SubscriptionObjectType?): SubscriptionType? =
when (type) {
SubscriptionObjectType.EMAIL -> SubscriptionType.EMAIL
Expand Down
Loading
Loading