Skip to content

Commit 7a71dcc

Browse files
authored
Merge pull request #2378 from OneSignal/feat/update_timezone
Feat: Detect for timezone changes and update the user
2 parents 17246b4 + 14b5d76 commit 7a71dcc

File tree

6 files changed

+178
-59
lines changed

6 files changed

+178
-59
lines changed

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package com.onesignal.user.internal
22

33
import com.onesignal.common.IDManager
44
import com.onesignal.common.OneSignalUtils
5+
import com.onesignal.common.TimeUtils
56
import com.onesignal.common.events.EventProducer
67
import com.onesignal.common.modeling.ISingletonModelStoreChangeHandler
78
import com.onesignal.common.modeling.ModelChangedArgs
9+
import com.onesignal.core.internal.application.IApplicationLifecycleHandler
10+
import com.onesignal.core.internal.application.IApplicationService
811
import com.onesignal.core.internal.language.ILanguageContext
912
import com.onesignal.debug.LogLevel
1013
import com.onesignal.debug.internal.logging.Logging
@@ -26,7 +29,8 @@ internal open class UserManager(
2629
private val _identityModelStore: IdentityModelStore,
2730
private val _propertiesModelStore: PropertiesModelStore,
2831
private val _languageContext: ILanguageContext,
29-
) : IUserManager, ISingletonModelStoreChangeHandler<IdentityModel> {
32+
private val _applicationService: IApplicationService,
33+
) : IUserManager, IApplicationLifecycleHandler, ISingletonModelStoreChangeHandler<IdentityModel> {
3034
override val onesignalId: String
3135
get() = if (IDManager.isLocalId(_identityModel.onesignalId)) "" else _identityModel.onesignalId
3236

@@ -55,6 +59,7 @@ internal open class UserManager(
5559
}
5660

5761
init {
62+
_applicationService.addApplicationLifecycleHandler(this)
5863
_identityModelStore.subscribe(this)
5964
}
6065

@@ -260,4 +265,11 @@ internal open class UserManager(
260265
}
261266
}
262267
}
268+
269+
override fun onFocus(firedOnSubscribe: Boolean) {
270+
// Detect any user properties updates that changed
271+
_propertiesModel.timezone = TimeUtils.getTimeZoneId()
272+
}
273+
274+
override fun onUnfocused() { }
263275
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ internal class LoginUserOperationExecutor(
146146
var identities = mapOf<String, String>()
147147
var subscriptions = mapOf<String, SubscriptionObject>()
148148
val properties = mutableMapOf<String, String>()
149-
properties["timezone_id"] = TimeUtils.getTimeZoneId()!!
149+
properties["timezone_id"] = TimeUtils.getTimeZoneId()
150150
properties["language"] = _languageContext.language
151151

152152
if (createUserOperation.externalId != null) {

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/RefreshUserOperationExecutor.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.onesignal.user.internal.operations.impl.executors
22

33
import com.onesignal.common.NetworkUtils
4+
import com.onesignal.common.TimeUtils
45
import com.onesignal.common.exceptions.BackendException
56
import com.onesignal.common.modeling.ModelChangeTags
67
import com.onesignal.core.internal.config.ConfigModelStore
@@ -89,9 +90,8 @@ internal class RefreshUserOperationExecutor(
8990
}
9091
}
9192

92-
if (response.properties.timezoneId != null) {
93-
propertiesModel.timezone = response.properties.timezoneId
94-
}
93+
// No longer hydrate timezone from remote, set locally
94+
propertiesModel.timezone = TimeUtils.getTimeZoneId()
9595

9696
val subscriptionModels = mutableListOf<SubscriptionModel>()
9797
for (subscription in response.subscriptions) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.onesignal.common
2+
3+
import android.os.Build
4+
import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest
5+
import io.kotest.core.spec.style.FunSpec
6+
import io.kotest.matchers.shouldBe
7+
import io.kotest.matchers.shouldNotBe
8+
import io.kotest.matchers.string.shouldNotBeEmpty
9+
import io.kotest.matchers.string.shouldNotContain
10+
import java.time.ZoneId
11+
import java.util.TimeZone
12+
13+
@RobolectricTest
14+
class TimeUtilsTest : FunSpec({
15+
16+
test("getTimeZoneId returns correct time zone id") {
17+
// Given
18+
val expected =
19+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
20+
ZoneId.systemDefault().id
21+
} else {
22+
TimeZone.getDefault().id
23+
}
24+
25+
// When
26+
val actual = TimeUtils.getTimeZoneId()
27+
28+
// Then
29+
actual shouldBe expected
30+
actual.shouldNotBeEmpty()
31+
}
32+
33+
test("getTimeZoneId returns valid timezone format") {
34+
// When
35+
val timeZoneId = TimeUtils.getTimeZoneId()
36+
37+
// Then
38+
timeZoneId.shouldNotBeEmpty()
39+
timeZoneId shouldNotBe ""
40+
41+
// Valid timezone IDs follow IANA format patterns:
42+
// - Continental zones: "America/New_York", "Europe/London"
43+
// - UTC variants: "UTC", "GMT"
44+
// - Offset formats: "GMT+05:30", "UTC-08:00"
45+
// Should not contain spaces or invalid characters
46+
timeZoneId.shouldNotContain(" ")
47+
}
48+
})

OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.onesignal.user.internal
22

3+
import com.onesignal.common.TimeUtils
34
import com.onesignal.core.internal.language.ILanguageContext
45
import com.onesignal.mocks.MockHelper
56
import com.onesignal.user.internal.subscriptions.ISubscriptionManager
@@ -10,8 +11,10 @@ import io.kotest.matchers.shouldNotBe
1011
import io.mockk.every
1112
import io.mockk.just
1213
import io.mockk.mockk
14+
import io.mockk.mockkObject
1315
import io.mockk.runs
1416
import io.mockk.slot
17+
import io.mockk.unmockkObject
1518
import io.mockk.verify
1619

1720
class UserManagerTests : FunSpec({
@@ -26,7 +29,7 @@ class UserManagerTests : FunSpec({
2629
every { languageContext.language = capture(languageSlot) } answers { }
2730

2831
val userManager =
29-
UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), MockHelper.propertiesModelStore(), languageContext)
32+
UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), MockHelper.propertiesModelStore(), languageContext, MockHelper.applicationService())
3033

3134
// When
3235
userManager.setLanguage("new-language")
@@ -44,7 +47,7 @@ class UserManagerTests : FunSpec({
4447
}
4548

4649
val userManager =
47-
UserManager(mockSubscriptionManager, identityModelStore, MockHelper.propertiesModelStore(), MockHelper.languageContext())
50+
UserManager(mockSubscriptionManager, identityModelStore, MockHelper.propertiesModelStore(), MockHelper.languageContext(), MockHelper.applicationService())
4851

4952
// When
5053
val externalId = userManager.externalId
@@ -63,7 +66,7 @@ class UserManagerTests : FunSpec({
6366
}
6467

6568
val userManager =
66-
UserManager(mockSubscriptionManager, identityModelStore, MockHelper.propertiesModelStore(), MockHelper.languageContext())
69+
UserManager(mockSubscriptionManager, identityModelStore, MockHelper.propertiesModelStore(), MockHelper.languageContext(), MockHelper.applicationService())
6770

6871
// When
6972
val alias1 = userManager.aliases["my-alias-key1"]
@@ -102,7 +105,7 @@ class UserManagerTests : FunSpec({
102105
}
103106

104107
val userManager =
105-
UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.languageContext())
108+
UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.languageContext(), MockHelper.applicationService())
106109

107110
// When
108111
val tag1 = propertiesModelStore.model.tags["my-tag-key1"]
@@ -141,7 +144,7 @@ class UserManagerTests : FunSpec({
141144
it.tags["my-tag-key1"] = "my-tag-value1"
142145
}
143146

144-
val userManager = UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.languageContext())
147+
val userManager = UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.languageContext(), MockHelper.applicationService())
145148

146149
// When
147150
val tagSnapshot1 = userManager.getTags()
@@ -174,6 +177,7 @@ class UserManagerTests : FunSpec({
174177
MockHelper.identityModelStore(),
175178
MockHelper.propertiesModelStore(),
176179
MockHelper.languageContext(),
180+
MockHelper.applicationService(),
177181
)
178182

179183
// When
@@ -191,4 +195,36 @@ class UserManagerTests : FunSpec({
191195
verify(exactly = 1) { mockSubscriptionManager.addSmsSubscription("+15558675309") }
192196
verify(exactly = 1) { mockSubscriptionManager.removeSmsSubscription("+15558675309") }
193197
}
198+
199+
test("onFocus updates timezone") {
200+
// Given
201+
val mockTimeZone = "Europe/Foo"
202+
mockkObject(TimeUtils)
203+
every { TimeUtils.getTimeZoneId() } returns mockTimeZone
204+
205+
val mockPropertiesModelStore = MockHelper.propertiesModelStore()
206+
207+
val userManager =
208+
UserManager(
209+
mockk<ISubscriptionManager>(),
210+
MockHelper.identityModelStore(),
211+
mockPropertiesModelStore,
212+
MockHelper.languageContext(),
213+
MockHelper.applicationService(),
214+
)
215+
216+
val propertiesModel = mockPropertiesModelStore.model
217+
propertiesModel.timezone shouldNotBe mockTimeZone
218+
219+
try {
220+
// When
221+
userManager.onFocus(firedOnSubscribe = false)
222+
223+
// Then
224+
propertiesModel.timezone shouldBe mockTimeZone
225+
} finally {
226+
// Clean up the mock
227+
unmockkObject(TimeUtils)
228+
}
229+
}
194230
})

0 commit comments

Comments
 (0)