Skip to content

Commit f13f150

Browse files
abdulraqeeb33AR Abdul Azeez
andauthored
feature: Exposing accessors thru suspend (#2502)
Co-authored-by: AR Abdul Azeez <abdul@onesignal.com>
1 parent 8305074 commit f13f150

File tree

4 files changed

+359
-14
lines changed

4 files changed

+359
-14
lines changed

Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplicationKT.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ import com.onesignal.user.state.IUserStateObserver
3939
import com.onesignal.user.state.UserChangedState
4040
import com.onesignal.user.state.UserState
4141
import kotlinx.coroutines.CoroutineScope
42-
import kotlinx.coroutines.DelicateCoroutinesApi
4342
import kotlinx.coroutines.Dispatchers
4443
import kotlinx.coroutines.SupervisorJob
4544
import kotlinx.coroutines.launch

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt

Lines changed: 165 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,140 @@ object OneSignal {
152152
return oneSignal.initWithContextSuspend(context, appId)
153153
}
154154

155+
/**
156+
* Get the user manager without blocking the calling thread.
157+
* Suspends until the SDK is initialized if initialization is in progress.
158+
* This is the suspend-safe version of the [User] property accessor.
159+
*
160+
* @return The user manager for accessing user-scoped management.
161+
*/
162+
@JvmStatic
163+
suspend fun getUserSuspend(): IUserManager {
164+
return oneSignal.getUser()
165+
}
166+
167+
/**
168+
* Get the session manager without blocking the calling thread.
169+
* Suspends until the SDK is initialized if initialization is in progress.
170+
* This is the suspend-safe version of the [Session] property accessor.
171+
*
172+
* @return The session manager for accessing session-scoped management.
173+
*/
174+
@JvmStatic
175+
suspend fun getSessionSuspend(): ISessionManager {
176+
return oneSignal.getSession()
177+
}
178+
179+
/**
180+
* Get the notifications manager without blocking the calling thread.
181+
* Suspends until the SDK is initialized if initialization is in progress.
182+
* This is the suspend-safe version of the [Notifications] property accessor.
183+
*
184+
* @return The notification manager for accessing device-scoped notification management.
185+
*/
186+
@JvmStatic
187+
suspend fun getNotificationsSuspend(): INotificationsManager {
188+
return oneSignal.getNotifications()
189+
}
190+
191+
/**
192+
* Get the location manager without blocking the calling thread.
193+
* Suspends until the SDK is initialized if initialization is in progress.
194+
* This is the suspend-safe version of the [Location] property accessor.
195+
*
196+
* @return The location manager for accessing device-scoped location management.
197+
*/
198+
@JvmStatic
199+
suspend fun getLocationSuspend(): ILocationManager {
200+
return oneSignal.getLocation()
201+
}
202+
203+
/**
204+
* Get the in-app messages manager without blocking the calling thread.
205+
* Suspends until the SDK is initialized if initialization is in progress.
206+
* This is the suspend-safe version of the [InAppMessages] property accessor.
207+
*
208+
* @return The in-app messaging manager for accessing device-scoped IAM management.
209+
*/
210+
@JvmStatic
211+
suspend fun getInAppMessagesSuspend(): IInAppMessagesManager {
212+
return oneSignal.getInAppMessages()
213+
}
214+
215+
/**
216+
* Get the consent required flag in a thread-safe manner without blocking the calling thread.
217+
* Suspends until the SDK is initialized if initialization is in progress.
218+
* This is the suspend-safe version of the [consentRequired] property accessor.
219+
*
220+
* @return Whether a user must consent to privacy prior to their user data being sent to OneSignal.
221+
*/
222+
@JvmStatic
223+
suspend fun getConsentRequiredSuspend(): Boolean {
224+
return oneSignal.getConsentRequired()
225+
}
226+
227+
/**
228+
* Set the consent required flag in a thread-safe manner without blocking the calling thread.
229+
* Suspends until the SDK is initialized if initialization is in progress.
230+
* This is the suspend-safe version of the [consentRequired] property setter.
231+
*
232+
* @param required Whether a user must consent to privacy prior to their user data being sent to OneSignal.
233+
* Should be set to `true` prior to the invocation of [initWithContext] to ensure compliance.
234+
*/
235+
@JvmStatic
236+
suspend fun setConsentRequiredSuspend(required: Boolean) {
237+
oneSignal.setConsentRequired(required)
238+
}
239+
240+
/**
241+
* Get the consent given flag in a thread-safe manner without blocking the calling thread.
242+
* Suspends until the SDK is initialized if initialization is in progress.
243+
* This is the suspend-safe version of the [consentGiven] property accessor.
244+
*
245+
* @return Whether privacy consent has been granted. This field is only relevant when
246+
* the application has opted into data privacy protections. See [consentRequired].
247+
*/
248+
@JvmStatic
249+
suspend fun getConsentGivenSuspend(): Boolean {
250+
return oneSignal.getConsentGiven()
251+
}
252+
253+
/**
254+
* Set the consent given flag in a thread-safe manner without blocking the calling thread.
255+
* Suspends until the SDK is initialized if initialization is in progress.
256+
* This is the suspend-safe version of the [consentGiven] property setter.
257+
*
258+
* @param value Whether privacy consent has been granted.
259+
*/
260+
@JvmStatic
261+
suspend fun setConsentGivenSuspend(value: Boolean) {
262+
oneSignal.setConsentGiven(value)
263+
}
264+
265+
/**
266+
* Get the disable GMS missing prompt flag in a thread-safe manner without blocking the calling thread.
267+
* Suspends until the SDK is initialized if initialization is in progress.
268+
* This is the suspend-safe version of the [disableGMSMissingPrompt] property accessor.
269+
*
270+
* @return Whether to disable the "GMS is missing" prompt to the user.
271+
*/
272+
@JvmStatic
273+
suspend fun getDisableGMSMissingPromptSuspend(): Boolean {
274+
return oneSignal.getDisableGMSMissingPrompt()
275+
}
276+
277+
/**
278+
* Set the disable GMS missing prompt flag in a thread-safe manner without blocking the calling thread.
279+
* Suspends until the SDK is initialized if initialization is in progress.
280+
* This is the suspend-safe version of the [disableGMSMissingPrompt] property setter.
281+
*
282+
* @param value Whether to disable the "GMS is missing" prompt to the user.
283+
*/
284+
@JvmStatic
285+
suspend fun setDisableGMSMissingPromptSuspend(value: Boolean) {
286+
oneSignal.setDisableGMSMissingPrompt(value)
287+
}
288+
155289
/**
156290
* Login to OneSignal under the user identified by the [externalId] provided. The act of
157291
* logging a user into the OneSignal SDK will switch the [User] context to that specific user.
@@ -226,24 +360,49 @@ object OneSignal {
226360
}
227361

228362
/**
229-
* Login a user with external ID and optional JWT token (suspend version).
363+
* Login a user with external ID and optional JWT token without blocking the calling thread.
364+
* Suspends until the SDK is initialized if initialization is in progress.
365+
* This is the suspend-safe version of the [login] method.
366+
*
367+
* The act of logging a user into the OneSignal SDK will switch the [User] context to that specific user.
230368
*
231-
* @param externalId External user ID for login
232-
* @param jwtBearerToken Optional JWT token for authentication
369+
* * If the [externalId] exists the user will be retrieved and the context set from that
370+
* user information. If operations have already been performed under a guest user, they
371+
* *will not* be applied to the now logged in user (they will be lost).
372+
* * If the [externalId] does not exist the user will be created and the context set from
373+
* the current local state. If operations have already been performed under a guest user
374+
* those operations *will* be applied to the newly created user.
375+
*
376+
* *Push Notifications and In App Messaging*
377+
* Logging in a new user will automatically transfer push notification and in app messaging
378+
* subscriptions from the current user (if there is one) to the newly logged in user. This is
379+
* because both Push and IAM are owned by the device.
380+
*
381+
* @param externalId The external ID of the user that is to be logged in.
382+
* @param jwtBearerToken The optional JWT bearer token generated by your backend to establish
383+
* trust for the login operation. Required when identity verification has been enabled. See
384+
* [Identity Verification | OneSignal](https://documentation.onesignal.com/docs/identity-verification)
233385
*/
234386
@JvmStatic
235387
suspend fun loginSuspend(
236388
externalId: String,
237389
jwtBearerToken: String? = null,
238390
) {
239-
oneSignal.login(externalId, jwtBearerToken)
391+
oneSignal.loginSuspend(externalId, jwtBearerToken)
240392
}
241393

242394
/**
243-
* Logout the current user (suspend version).
395+
* Logout the current user without blocking the calling thread.
396+
* Suspends until the SDK is initialized if initialization is in progress.
397+
* This is the suspend-safe version of the [logout] method.
398+
*
399+
* The [User] property now references a new device-scoped user. A device-scoped user has no
400+
* user identity that can later be retrieved, except through this device as long as the app
401+
* remains installed and the app data is not cleared.
244402
*/
403+
@JvmStatic
245404
suspend fun logoutSuspend() {
246-
oneSignal.logout()
405+
oneSignal.logoutSuspend()
247406
}
248407

249408
/**
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package com.onesignal
2+
3+
import com.onesignal.inAppMessages.IInAppMessagesManager
4+
import com.onesignal.location.ILocationManager
5+
import com.onesignal.notifications.INotificationsManager
6+
import com.onesignal.session.ISessionManager
7+
import com.onesignal.user.IUserManager
8+
import io.kotest.core.spec.style.FunSpec
9+
import kotlin.reflect.full.memberFunctions
10+
11+
/**
12+
* Simple compilation tests to verify that all suspend methods exist in OneSignal class
13+
* with correct signatures. These tests verify the API surface but don't execute the methods.
14+
*/
15+
class OneSignalSuspendMethodsExistTest : FunSpec({
16+
17+
test("initWithContextSuspend exists with correct signature") {
18+
// This test compiles only if the method exists with correct signature
19+
val method: suspend (android.content.Context, String) -> Boolean = OneSignal::initWithContextSuspend
20+
21+
// Verify using reflection that it's actually a suspend function
22+
val kFunction = OneSignal::class.memberFunctions
23+
.find { it.name == "initWithContextSuspend" }
24+
25+
assert(kFunction != null) { "initWithContextSuspend not found" }
26+
assert(kFunction!!.isSuspend) { "initWithContextSuspend is not a suspend function" }
27+
}
28+
29+
test("getUserSuspend exists and returns IUserManager") {
30+
// Compilation check - this fails if method doesn't exist or has wrong return type
31+
val method: suspend () -> IUserManager = OneSignal::getUserSuspend
32+
33+
val kFunction = OneSignal::class.memberFunctions
34+
.find { it.name == "getUserSuspend" }
35+
36+
assert(kFunction != null) { "getUserSuspend not found" }
37+
assert(kFunction!!.isSuspend) { "getUserSuspend is not a suspend function" }
38+
}
39+
40+
test("getSessionSuspend exists and returns ISessionManager") {
41+
val method: suspend () -> ISessionManager = OneSignal::getSessionSuspend
42+
43+
val kFunction = OneSignal::class.memberFunctions
44+
.find { it.name == "getSessionSuspend" }
45+
46+
assert(kFunction != null) { "getSessionSuspend not found" }
47+
assert(kFunction!!.isSuspend) { "getSessionSuspend is not a suspend function" }
48+
}
49+
50+
test("getNotificationsSuspend exists and returns INotificationsManager") {
51+
val method: suspend () -> INotificationsManager = OneSignal::getNotificationsSuspend
52+
53+
val kFunction = OneSignal::class.memberFunctions
54+
.find { it.name == "getNotificationsSuspend" }
55+
56+
assert(kFunction != null) { "getNotificationsSuspend not found" }
57+
assert(kFunction!!.isSuspend) { "getNotificationsSuspend is not a suspend function" }
58+
}
59+
60+
test("getLocationSuspend exists and returns ILocationManager") {
61+
val method: suspend () -> ILocationManager = OneSignal::getLocationSuspend
62+
63+
val kFunction = OneSignal::class.memberFunctions
64+
.find { it.name == "getLocationSuspend" }
65+
66+
assert(kFunction != null) { "getLocationSuspend not found" }
67+
assert(kFunction!!.isSuspend) { "getLocationSuspend is not a suspend function" }
68+
}
69+
70+
test("getInAppMessagesSuspend exists and returns IInAppMessagesManager") {
71+
val method: suspend () -> IInAppMessagesManager = OneSignal::getInAppMessagesSuspend
72+
73+
val kFunction = OneSignal::class.memberFunctions
74+
.find { it.name == "getInAppMessagesSuspend" }
75+
76+
assert(kFunction != null) { "getInAppMessagesSuspend not found" }
77+
assert(kFunction!!.isSuspend) { "getInAppMessagesSuspend is not a suspend function" }
78+
}
79+
80+
test("getConsentRequiredSuspend exists and returns Boolean") {
81+
val method: suspend () -> Boolean = OneSignal::getConsentRequiredSuspend
82+
83+
val kFunction = OneSignal::class.memberFunctions
84+
.find { it.name == "getConsentRequiredSuspend" }
85+
86+
assert(kFunction != null) { "getConsentRequiredSuspend not found" }
87+
assert(kFunction!!.isSuspend) { "getConsentRequiredSuspend is not a suspend function" }
88+
}
89+
90+
test("setConsentRequiredSuspend exists with Boolean parameter") {
91+
val method: suspend (Boolean) -> Unit = OneSignal::setConsentRequiredSuspend
92+
93+
val kFunction = OneSignal::class.memberFunctions
94+
.find { it.name == "setConsentRequiredSuspend" }
95+
96+
assert(kFunction != null) { "setConsentRequiredSuspend not found" }
97+
assert(kFunction!!.isSuspend) { "setConsentRequiredSuspend is not a suspend function" }
98+
}
99+
100+
test("getConsentGivenSuspend exists and returns Boolean") {
101+
val method: suspend () -> Boolean = OneSignal::getConsentGivenSuspend
102+
103+
val kFunction = OneSignal::class.memberFunctions
104+
.find { it.name == "getConsentGivenSuspend" }
105+
106+
assert(kFunction != null) { "getConsentGivenSuspend not found" }
107+
assert(kFunction!!.isSuspend) { "getConsentGivenSuspend is not a suspend function" }
108+
}
109+
110+
test("setConsentGivenSuspend exists with Boolean parameter") {
111+
val method: suspend (Boolean) -> Unit = OneSignal::setConsentGivenSuspend
112+
113+
val kFunction = OneSignal::class.memberFunctions
114+
.find { it.name == "setConsentGivenSuspend" }
115+
116+
assert(kFunction != null) { "setConsentGivenSuspend not found" }
117+
assert(kFunction!!.isSuspend) { "setConsentGivenSuspend is not a suspend function" }
118+
}
119+
120+
test("getDisableGMSMissingPromptSuspend exists and returns Boolean") {
121+
val method: suspend () -> Boolean = OneSignal::getDisableGMSMissingPromptSuspend
122+
123+
val kFunction = OneSignal::class.memberFunctions
124+
.find { it.name == "getDisableGMSMissingPromptSuspend" }
125+
126+
assert(kFunction != null) { "getDisableGMSMissingPromptSuspend not found" }
127+
assert(kFunction!!.isSuspend) { "getDisableGMSMissingPromptSuspend is not a suspend function" }
128+
}
129+
130+
test("setDisableGMSMissingPromptSuspend exists with Boolean parameter") {
131+
val method: suspend (Boolean) -> Unit = OneSignal::setDisableGMSMissingPromptSuspend
132+
133+
val kFunction = OneSignal::class.memberFunctions
134+
.find { it.name == "setDisableGMSMissingPromptSuspend" }
135+
136+
assert(kFunction != null) { "setDisableGMSMissingPromptSuspend not found" }
137+
assert(kFunction!!.isSuspend) { "setDisableGMSMissingPromptSuspend is not a suspend function" }
138+
}
139+
140+
test("loginSuspend exists with String and optional String parameters") {
141+
// Verify the method exists with correct signature using reflection
142+
// Note: There's only one loginSuspend with a default parameter for jwtBearerToken
143+
val kFunction = OneSignal::class.memberFunctions
144+
.find { it.name == "loginSuspend" }
145+
146+
assert(kFunction != null) { "loginSuspend not found" }
147+
assert(kFunction!!.isSuspend) { "loginSuspend is not a suspend function" }
148+
assert(kFunction.parameters.size >= 2) { "loginSuspend should have at least 2 parameters (receiver + externalId)" }
149+
}
150+
151+
test("logoutSuspend exists with no parameters") {
152+
val method: suspend () -> Unit = OneSignal::logoutSuspend
153+
154+
val kFunction = OneSignal::class.memberFunctions
155+
.find { it.name == "logoutSuspend" }
156+
157+
assert(kFunction != null) { "logoutSuspend not found" }
158+
assert(kFunction!!.isSuspend) { "logoutSuspend is not a suspend function" }
159+
}
160+
161+
test("all suspend methods are marked with @JvmStatic") {
162+
// Get all suspend methods we added
163+
val suspendMethodNames = listOf(
164+
"getUserSuspend",
165+
"getSessionSuspend",
166+
"getNotificationsSuspend",
167+
"getLocationSuspend",
168+
"getInAppMessagesSuspend",
169+
"getConsentRequiredSuspend",
170+
"setConsentRequiredSuspend",
171+
"getConsentGivenSuspend",
172+
"setConsentGivenSuspend",
173+
"getDisableGMSMissingPromptSuspend",
174+
"setDisableGMSMissingPromptSuspend",
175+
"loginSuspend",
176+
"logoutSuspend"
177+
)
178+
179+
// Verify each exists and is a static method (accessible via companion object)
180+
suspendMethodNames.forEach { methodName ->
181+
val kFunction = OneSignal::class.memberFunctions
182+
.find { it.name == methodName }
183+
184+
assert(kFunction != null) { "$methodName not found" }
185+
assert(kFunction!!.isSuspend) { "$methodName is not a suspend function" }
186+
}
187+
}
188+
})

0 commit comments

Comments
 (0)