From 29e9bef65dab217c15752b2b1dc778c18d22f8e1 Mon Sep 17 00:00:00 2001 From: Jordan Haven Date: Wed, 9 Oct 2024 15:35:53 -0400 Subject: [PATCH 1/2] Implement custom lazy delegate to avoid SharedPreferences race condition --- .../com/stytch/sdk/b2b/StytchB2BClient.kt | 120 ++++++------------ .../com/stytch/sdk/consumer/StytchClient.kt | 92 +++++--------- .../com/stytch/sdk/b2b/StytchB2BClientTest.kt | 1 + .../stytch/sdk/consumer/StytchClientTest.kt | 1 + 4 files changed, 73 insertions(+), 141 deletions(-) diff --git a/source/sdk/src/main/java/com/stytch/sdk/b2b/StytchB2BClient.kt b/source/sdk/src/main/java/com/stytch/sdk/b2b/StytchB2BClient.kt index 30e32f5e1..5eecf2c5f 100644 --- a/source/sdk/src/main/java/com/stytch/sdk/b2b/StytchB2BClient.kt +++ b/source/sdk/src/main/java/com/stytch/sdk/b2b/StytchB2BClient.kt @@ -46,6 +46,7 @@ import com.stytch.sdk.common.QUERY_TOKEN_TYPE import com.stytch.sdk.common.StorageHelper import com.stytch.sdk.common.StytchClientOptions import com.stytch.sdk.common.StytchDispatchers +import com.stytch.sdk.common.StytchLazyDelegate import com.stytch.sdk.common.StytchResult import com.stytch.sdk.common.dfp.ActivityProvider import com.stytch.sdk.common.dfp.CaptchaProviderImpl @@ -83,7 +84,7 @@ import java.util.UUID public object StytchB2BClient { internal var dispatchers: StytchDispatchers = StytchDispatchers() internal var externalScope: CoroutineScope = GlobalScope // TODO: SDK-614 - internal val sessionStorage = B2BSessionStorage(StorageHelper, externalScope) + internal lateinit var sessionStorage: B2BSessionStorage internal var pkcePairManager: PKCEPairManager = PKCEPairManagerImpl(StorageHelper, EncryptionManager) internal lateinit var dfpProvider: DFPProvider internal var bootstrapData: BootstrapData = BootstrapData() @@ -126,6 +127,7 @@ public object StytchB2BClient { deviceInfo = context.getDeviceInfo() appSessionId = "app-session-id-${UUID.randomUUID()}" StorageHelper.initialize(context) + sessionStorage = B2BSessionStorage(StorageHelper, externalScope) StytchB2BApi.configure(publicToken, deviceInfo) val activityProvider = ActivityProvider(context.applicationContext as Application) dfpProvider = @@ -251,7 +253,7 @@ public object StytchB2BClient { } internal fun assertInitialized() { - if (!StytchB2BApi.isInitialized) { + if (!StytchB2BApi.isInitialized || !::sessionStorage.isInitialized) { throw StytchSDKNotConfiguredError("StytchB2BClient") } } @@ -264,7 +266,7 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val magicLinks: B2BMagicLinks = + public val magicLinks: B2BMagicLinks by StytchLazyDelegate(::assertInitialized) { B2BMagicLinksImpl( externalScope, dispatchers, @@ -273,10 +275,7 @@ public object StytchB2BClient { StytchB2BApi.MagicLinks.Discovery, pkcePairManager, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [B2BSessions] interface which provides methods for authenticating, updating, or @@ -286,17 +285,14 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val sessions: B2BSessions = + public val sessions: B2BSessions by StytchLazyDelegate(::assertInitialized) { B2BSessionsImpl( externalScope, dispatchers, sessionStorage, StytchB2BApi.Sessions, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [Organization] interface which provides methods for retrieving the current @@ -306,17 +302,14 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val organization: Organization = + public val organization: Organization by StytchLazyDelegate(::assertInitialized) { OrganizationImpl( externalScope, dispatchers, sessionStorage, StytchB2BApi.Organization, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [Member] interface which provides methods for retrieving the current authenticated @@ -326,17 +319,14 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val member: Member = + public val member: Member by StytchLazyDelegate(::assertInitialized) { MemberImpl( externalScope, dispatchers, sessionStorage, StytchB2BApi.Member, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [Passwords] interface which provides methods for authenticating passwords, resetting @@ -346,7 +336,7 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val passwords: Passwords = + public val passwords: Passwords by StytchLazyDelegate(::assertInitialized) { PasswordsImpl( externalScope, dispatchers, @@ -354,10 +344,7 @@ public object StytchB2BClient { StytchB2BApi.Passwords, pkcePairManager, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [Discovery] interface which provides methods for creating and discovering @@ -367,23 +354,20 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val discovery: Discovery = + public val discovery: Discovery by StytchLazyDelegate(::assertInitialized) { DiscoveryImpl( externalScope, dispatchers, sessionStorage, StytchB2BApi.Discovery, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [SSO] interface which provides methods for authenticating SSO sessions */ @JvmStatic - public val sso: SSO = + public val sso: SSO by StytchLazyDelegate(::assertInitialized) { SSOImpl( externalScope, dispatchers, @@ -391,10 +375,7 @@ public object StytchB2BClient { StytchB2BApi.SSO, pkcePairManager, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [DFP] interface which provides a method for retrieving a dfp_telemetry_id for use @@ -404,16 +385,13 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val dfp: DFP by lazy { - assertInitialized() + public val dfp: DFP by StytchLazyDelegate(::assertInitialized) { DFPImpl(dfpProvider, dispatchers, externalScope) } - internal val events: Events - get() { - assertInitialized() - return EventsImpl(deviceInfo, appSessionId, externalScope, dispatchers, StytchB2BApi.Events) - } + internal val events: Events by StytchLazyDelegate(::assertInitialized) { + EventsImpl(deviceInfo, appSessionId, externalScope, dispatchers, StytchB2BApi.Events) + } /** * Exposes an instance of the [OTP] interface which provides a method for sending and authenticating OTP codes @@ -422,11 +400,9 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val otp: OTP = OTPImpl(externalScope, dispatchers, sessionStorage, StytchB2BApi.OTP) - get() { - assertInitialized() - return field - } + public val otp: OTP by StytchLazyDelegate(::assertInitialized) { + OTPImpl(externalScope, dispatchers, sessionStorage, StytchB2BApi.OTP) + } /** * Exposes an instance of the [TOTP] interface which provides a method for creating and authenticating TOTP codes @@ -435,11 +411,9 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val totp: TOTP = TOTPImpl(externalScope, dispatchers, sessionStorage, StytchB2BApi.TOTP) - get() { - assertInitialized() - return field - } + public val totp: TOTP by StytchLazyDelegate(::assertInitialized) { + TOTPImpl(externalScope, dispatchers, sessionStorage, StytchB2BApi.TOTP) + } /** * Exposes an instance of the [RecoveryCodes] interface which provides methods for getting, rotating, and @@ -449,12 +423,9 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val recoveryCodes: RecoveryCodes = + public val recoveryCodes: RecoveryCodes by StytchLazyDelegate(::assertInitialized) { RecoveryCodesImpl(externalScope, dispatchers, sessionStorage, StytchB2BApi.RecoveryCodes) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [OAuth] interface which provides a method for starting and authenticating OAuth and @@ -464,12 +435,9 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val oauth: OAuth = + public val oauth: OAuth by StytchLazyDelegate(::assertInitialized) { OAuthImpl(externalScope, dispatchers, sessionStorage, StytchB2BApi.OAuth, pkcePairManager) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [RBAC] interface which provides methods for checking a member's permissions @@ -477,11 +445,9 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val rbac: RBAC = RBACImpl(externalScope, dispatchers, sessionStorage) - get() { - assertInitialized() - return field - } + public val rbac: RBAC by StytchLazyDelegate(::assertInitialized) { + RBACImpl(externalScope, dispatchers, sessionStorage) + } /** * Exposes an instance of the [SearchManager] interface which provides methods to search organizations and members @@ -489,11 +455,9 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val searchManager: SearchManager = SearchManagerImpl(externalScope, dispatchers, StytchB2BApi.SearchManager) - get() { - assertInitialized() - return field - } + public val searchManager: SearchManager by StytchLazyDelegate(::assertInitialized) { + SearchManagerImpl(externalScope, dispatchers, StytchB2BApi.SearchManager) + } /** * Exposes an instance of the [SCIM] interface which provides methods for creating, getting, updating, deleting, and @@ -502,11 +466,9 @@ public object StytchB2BClient { * StytchB2BClient.configure() */ @JvmStatic - public val scim: SCIM = SCIMImpl(externalScope, dispatchers, StytchB2BApi.SCIM) - get() { - assertInitialized() - return field - } + public val scim: SCIM by StytchLazyDelegate(::assertInitialized) { + SCIMImpl(externalScope, dispatchers, StytchB2BApi.SCIM) + } /** * Call this method to parse out and authenticate deeplinks that your application receives. The currently supported diff --git a/source/sdk/src/main/java/com/stytch/sdk/consumer/StytchClient.kt b/source/sdk/src/main/java/com/stytch/sdk/consumer/StytchClient.kt index 49fb7ebf5..2585ee6e7 100644 --- a/source/sdk/src/main/java/com/stytch/sdk/consumer/StytchClient.kt +++ b/source/sdk/src/main/java/com/stytch/sdk/consumer/StytchClient.kt @@ -15,6 +15,7 @@ import com.stytch.sdk.common.QUERY_TOKEN_TYPE import com.stytch.sdk.common.StorageHelper import com.stytch.sdk.common.StytchClientOptions import com.stytch.sdk.common.StytchDispatchers +import com.stytch.sdk.common.StytchLazyDelegate import com.stytch.sdk.common.StytchResult import com.stytch.sdk.common.dfp.ActivityProvider import com.stytch.sdk.common.dfp.CaptchaProviderImpl @@ -77,7 +78,7 @@ import java.util.UUID public object StytchClient { internal var dispatchers: StytchDispatchers = StytchDispatchers() internal var externalScope: CoroutineScope = GlobalScope // TODO: SDK-614 - internal val sessionStorage = ConsumerSessionStorage(StorageHelper, externalScope) + internal lateinit var sessionStorage: ConsumerSessionStorage internal var pkcePairManager: PKCEPairManager = PKCEPairManagerImpl(StorageHelper, EncryptionManager) internal lateinit var dfpProvider: DFPProvider internal var bootstrapData: BootstrapData = BootstrapData() @@ -122,6 +123,7 @@ public object StytchClient { deviceInfo = context.getDeviceInfo() appSessionId = "app-session-id-${UUID.randomUUID()}" StorageHelper.initialize(context) + sessionStorage = ConsumerSessionStorage(StorageHelper, externalScope) StytchApi.configure(publicToken, deviceInfo) val activityProvider = ActivityProvider(context.applicationContext as Application) dfpProvider = @@ -242,7 +244,7 @@ public object StytchClient { } internal fun assertInitialized() { - if (!StytchApi.isInitialized) { + if (!StytchApi.isInitialized || !::sessionStorage.isInitialized) { throw StytchSDKNotConfiguredError("StytchClient") } } @@ -255,7 +257,7 @@ public object StytchClient { * StytchClient.configure() */ @JvmStatic - public val magicLinks: MagicLinks = + public val magicLinks: MagicLinks by StytchLazyDelegate(::assertInitialized) { MagicLinksImpl( externalScope, dispatchers, @@ -263,10 +265,7 @@ public object StytchClient { StytchApi.MagicLinks.Email, pkcePairManager, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [OTP] interface which provides methods for sending and authenticating @@ -276,17 +275,14 @@ public object StytchClient { * StytchClient.configure() */ @JvmStatic - public val otps: OTP = + public val otps: OTP by StytchLazyDelegate(::assertInitialized) { OTPImpl( externalScope, dispatchers, sessionStorage, StytchApi.OTP, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [Passwords] interface which provides methods for authenticating, creating, resetting, @@ -296,7 +292,7 @@ public object StytchClient { * StytchClient.configure() */ @JvmStatic - public val passwords: Passwords = + public val passwords: Passwords by StytchLazyDelegate(::assertInitialized) { PasswordsImpl( externalScope, dispatchers, @@ -304,10 +300,7 @@ public object StytchClient { StytchApi.Passwords, pkcePairManager, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [Sessions] interface which provides methods for authenticating, updating, or revoking @@ -317,17 +310,14 @@ public object StytchClient { * StytchClient.configure() */ @JvmStatic - public val sessions: Sessions = + public val sessions: Sessions by StytchLazyDelegate(::assertInitialized) { SessionsImpl( externalScope, dispatchers, sessionStorage, StytchApi.Sessions, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [Biometrics] interface which provides methods for detecting biometric availability, @@ -337,7 +327,7 @@ public object StytchClient { * StytchClient.configure() */ @JvmStatic - public val biometrics: Biometrics = + public val biometrics: Biometrics by StytchLazyDelegate(::assertInitialized) { BiometricsImpl( externalScope, dispatchers, @@ -348,10 +338,7 @@ public object StytchClient { ) { biometricRegistrationId -> user.deleteFactor(UserAuthenticationFactor.BiometricRegistration(biometricRegistrationId)) } - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [UserManagement] interface which provides methods for retrieving an authenticated @@ -361,17 +348,14 @@ public object StytchClient { * StytchClient.configure() */ @JvmStatic - public val user: UserManagement = + public val user: UserManagement by StytchLazyDelegate(::assertInitialized) { UserManagementImpl( externalScope, dispatchers, sessionStorage, StytchApi.UserManagement, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [OAuth] interface which provides methods for authenticating a user via a native @@ -381,7 +365,7 @@ public object StytchClient { * StytchClient.configure() */ @JvmStatic - public val oauth: OAuth = + public val oauth: OAuth by StytchLazyDelegate(::assertInitialized) { OAuthImpl( externalScope, dispatchers, @@ -389,10 +373,7 @@ public object StytchClient { StytchApi.OAuth, pkcePairManager, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [Passkeys] interface which provides methods for registering and authenticating @@ -402,17 +383,14 @@ public object StytchClient { * StytchClient.configure() */ @JvmStatic - public val passkeys: Passkeys = + public val passkeys: Passkeys by StytchLazyDelegate(::assertInitialized) { PasskeysImpl( externalScope, dispatchers, sessionStorage, StytchApi.WebAuthn, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [DFP] interface which provides a method for retrieving a dfp_telemetry_id for use @@ -422,11 +400,9 @@ public object StytchClient { * StytchClient.configure() */ @JvmStatic - public val dfp: DFP - get() { - assertInitialized() - return DFPImpl(dfpProvider, dispatchers, externalScope) - } + public val dfp: DFP by StytchLazyDelegate(::assertInitialized) { + DFPImpl(dfpProvider, dispatchers, externalScope) + } /** * Exposes an instance of the [CryptoWallet] interface which provides methods for authenticating with a crypto @@ -436,17 +412,14 @@ public object StytchClient { * StytchClient.configure() */ @JvmStatic - public val crypto: CryptoWallet = + public val crypto: CryptoWallet by StytchLazyDelegate(::assertInitialized) { CryptoWalletImpl( externalScope, dispatchers, sessionStorage, StytchApi.Crypto, ) - get() { - assertInitialized() - return field - } + } /** * Exposes an instance of the [TOTP] interface which provides methods for creating, authenticating, and recovering @@ -456,23 +429,18 @@ public object StytchClient { * StytchClient.configure() */ @JvmStatic - public val totp: TOTP = + public val totp: TOTP by StytchLazyDelegate(::assertInitialized) { TOTPImpl( externalScope, dispatchers, sessionStorage, StytchApi.TOTP, ) - get() { - assertInitialized() - return field - } + } - internal val events: Events - get() { - assertInitialized() - return EventsImpl(deviceInfo, appSessionId, externalScope, dispatchers, StytchApi.Events) - } + internal val events: Events by StytchLazyDelegate(::assertInitialized) { + EventsImpl(deviceInfo, appSessionId, externalScope, dispatchers, StytchApi.Events) + } /** * Call this method to parse out and authenticate deeplinks that your application receives. The currently supported diff --git a/source/sdk/src/test/java/com/stytch/sdk/b2b/StytchB2BClientTest.kt b/source/sdk/src/test/java/com/stytch/sdk/b2b/StytchB2BClientTest.kt index 79fbb8eae..974655c1c 100644 --- a/source/sdk/src/test/java/com/stytch/sdk/b2b/StytchB2BClientTest.kt +++ b/source/sdk/src/test/java/com/stytch/sdk/b2b/StytchB2BClientTest.kt @@ -101,6 +101,7 @@ internal class StytchB2BClientTest { StytchB2BClient.dispatchers = StytchDispatchers(dispatcher, dispatcher) StytchB2BClient.dfpProvider = mockk() StytchB2BClient.pkcePairManager = mockPKCEPairManager + StytchB2BClient.sessionStorage = mockk(relaxed = true, relaxUnitFun = true) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/source/sdk/src/test/java/com/stytch/sdk/consumer/StytchClientTest.kt b/source/sdk/src/test/java/com/stytch/sdk/consumer/StytchClientTest.kt index 9dad0b91d..5f76ad6bf 100644 --- a/source/sdk/src/test/java/com/stytch/sdk/consumer/StytchClientTest.kt +++ b/source/sdk/src/test/java/com/stytch/sdk/consumer/StytchClientTest.kt @@ -100,6 +100,7 @@ internal class StytchClientTest { StytchClient.dispatchers = StytchDispatchers(dispatcher, dispatcher) StytchClient.dfpProvider = mockk() StytchClient.pkcePairManager = mockPKCEPairManager + StytchClient.sessionStorage = mockk(relaxed = true, relaxUnitFun = true) } @OptIn(ExperimentalCoroutinesApi::class) From 66c99d705e64d35c5b9ccd43ad64caae75871a3e Mon Sep 17 00:00:00 2001 From: Jordan Haven Date: Wed, 9 Oct 2024 15:36:13 -0400 Subject: [PATCH 2/2] Bump version --- source/sdk/build.gradle | 2 +- .../stytch/sdk/common/StytchLazyDelegate.kt | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 source/sdk/src/main/java/com/stytch/sdk/common/StytchLazyDelegate.kt diff --git a/source/sdk/build.gradle b/source/sdk/build.gradle index 383e72efc..6881a2890 100644 --- a/source/sdk/build.gradle +++ b/source/sdk/build.gradle @@ -10,7 +10,7 @@ plugins { ext { PUBLISH_GROUP_ID = 'com.stytch.sdk' - PUBLISH_VERSION = '0.29.0' + PUBLISH_VERSION = '0.29.1' PUBLISH_ARTIFACT_ID = 'sdk' } diff --git a/source/sdk/src/main/java/com/stytch/sdk/common/StytchLazyDelegate.kt b/source/sdk/src/main/java/com/stytch/sdk/common/StytchLazyDelegate.kt new file mode 100644 index 000000000..02b9b02a3 --- /dev/null +++ b/source/sdk/src/main/java/com/stytch/sdk/common/StytchLazyDelegate.kt @@ -0,0 +1,22 @@ +package com.stytch.sdk.common + +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +internal class StytchLazyDelegate( + private val assertInitialized: () -> Unit, + private val initializer: () -> T, +) : ReadOnlyProperty { + private var value: T? = null + + override fun getValue( + thisRef: Any?, + property: KProperty<*>, + ): T { + assertInitialized() + if (value == null) { + value = initializer() + } + return value!! + } +}