Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e1d88f9

Browse files
committedMay 17, 2025·
feat: add android support
1 parent 8065ea4 commit e1d88f9

File tree

3 files changed

+415
-0
lines changed

3 files changed

+415
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package com.amazonaws.amplify.rtnpasskeys
2+
3+
import androidx.annotation.ChecksSdkIntAtLeast
4+
import androidx.credentials.CreateCredentialResponse
5+
import androidx.credentials.CreatePublicKeyCredentialRequest
6+
import androidx.credentials.CreatePublicKeyCredentialResponse
7+
import androidx.credentials.CredentialManager
8+
import androidx.credentials.GetCredentialRequest
9+
import androidx.credentials.GetCredentialResponse
10+
import androidx.credentials.GetPublicKeyCredentialOption
11+
import androidx.credentials.PublicKeyCredential
12+
import androidx.credentials.exceptions.CreateCredentialCancellationException
13+
import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
14+
import androidx.credentials.exceptions.CreateCredentialUnsupportedException
15+
import androidx.credentials.exceptions.GetCredentialCancellationException
16+
import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
17+
import androidx.credentials.exceptions.GetCredentialUnsupportedException
18+
import androidx.credentials.exceptions.domerrors.DataError
19+
import androidx.credentials.exceptions.domerrors.InvalidStateError
20+
import androidx.credentials.exceptions.domerrors.NotAllowedError
21+
import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException
22+
import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException
23+
24+
import com.facebook.fbreact.specs.NativeAmplifyRtnPasskeysSpec
25+
import com.facebook.react.bridge.JSONArguments
26+
import com.facebook.react.bridge.Promise
27+
import com.facebook.react.bridge.ReactApplicationContext
28+
import com.facebook.react.bridge.ReadableMap
29+
import com.facebook.react.module.annotations.ReactModule
30+
import kotlinx.coroutines.CoroutineDispatcher
31+
32+
import kotlinx.coroutines.CoroutineScope
33+
import kotlinx.coroutines.Dispatchers
34+
import kotlinx.coroutines.launch
35+
36+
import org.json.JSONObject
37+
38+
@ReactModule(name = AmplifyRtnPasskeysModule.NAME)
39+
class AmplifyRtnPasskeysModule(
40+
reactContext: ReactApplicationContext,
41+
dispatcher: CoroutineDispatcher = Dispatchers.Default
42+
) :
43+
NativeAmplifyRtnPasskeysSpec(reactContext) {
44+
45+
private val moduleScope = CoroutineScope(dispatcher)
46+
47+
override fun getName(): String {
48+
return NAME
49+
}
50+
51+
companion object {
52+
const val NAME = "AmplifyRtnPasskeys"
53+
}
54+
55+
@ChecksSdkIntAtLeast(api = android.os.Build.VERSION_CODES.P)
56+
override fun getIsPasskeySupported(): Boolean {
57+
// Requires Android SDK >= 28 (PIE)
58+
return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P
59+
}
60+
61+
override fun createPasskey(input: ReadableMap, promise: Promise) {
62+
if (!isPasskeySupported) {
63+
return promise.reject(
64+
"NOT_SUPPORTED",
65+
CreateCredentialUnsupportedException("CreatePasskeyNotSupported")
66+
)
67+
}
68+
69+
val credentialManager = CredentialManager.create(reactApplicationContext.applicationContext)
70+
71+
val requestJson = JSONObject(input.toHashMap()).toString()
72+
val request =
73+
CreatePublicKeyCredentialRequest(requestJson = requestJson)
74+
75+
moduleScope.launch {
76+
try {
77+
val result: CreateCredentialResponse =
78+
credentialManager.createCredential(
79+
context = currentActivity ?: reactApplicationContext,
80+
request = request
81+
)
82+
83+
val publicKeyResult =
84+
result as? CreatePublicKeyCredentialResponse
85+
?: throw Exception("CreatePasskeyFailed")
86+
87+
val jsonObject = JSONObject(publicKeyResult.registrationResponseJson)
88+
89+
promise.resolve(JSONArguments.fromJSONObject(jsonObject))
90+
} catch (e: Exception) {
91+
val errorCode = handlePasskeyFailure(e)
92+
promise.reject(errorCode, e)
93+
}
94+
}
95+
}
96+
97+
override fun getPasskey(input: ReadableMap, promise: Promise) {
98+
if (!isPasskeySupported) {
99+
return promise.reject(
100+
"NOT_SUPPORTED",
101+
GetCredentialUnsupportedException("GetPasskeyNotSupported")
102+
)
103+
}
104+
105+
val credentialManager = CredentialManager.create(reactApplicationContext.applicationContext)
106+
107+
val requestJson = JSONObject(input.toHashMap()).toString()
108+
val options =
109+
GetPublicKeyCredentialOption(requestJson = requestJson)
110+
val request = GetCredentialRequest(credentialOptions = listOf(options))
111+
112+
moduleScope.launch {
113+
try {
114+
val result: GetCredentialResponse =
115+
credentialManager.getCredential(
116+
context = currentActivity ?: reactApplicationContext,
117+
request = request
118+
)
119+
120+
val publicKeyResult =
121+
result.credential as? PublicKeyCredential ?: throw Exception("GetPasskeyFailed")
122+
123+
val jsonObject = JSONObject(publicKeyResult.authenticationResponseJson)
124+
125+
promise.resolve(JSONArguments.fromJSONObject(jsonObject))
126+
} catch (e: Exception) {
127+
val errorCode = handlePasskeyFailure(e)
128+
promise.reject(errorCode, e)
129+
}
130+
}
131+
}
132+
133+
private fun handlePasskeyFailure(e: Exception): String {
134+
return when (e) {
135+
is CreatePublicKeyCredentialDomException -> {
136+
when (e.domError) {
137+
is NotAllowedError -> "CANCELED"
138+
is InvalidStateError -> "DUPLICATE"
139+
is DataError -> "RELYING_PARTY_MISMATCH"
140+
else -> "FAILED"
141+
}
142+
}
143+
144+
is GetPublicKeyCredentialDomException -> {
145+
when (e.domError) {
146+
is NotAllowedError -> "CANCELED"
147+
is DataError -> "RELYING_PARTY_MISMATCH"
148+
else -> "FAILED"
149+
}
150+
}
151+
152+
is CreateCredentialCancellationException,
153+
is GetCredentialCancellationException -> "CANCELED"
154+
155+
is CreateCredentialUnsupportedException,
156+
is CreateCredentialProviderConfigurationException,
157+
is GetCredentialUnsupportedException,
158+
is GetCredentialProviderConfigurationException -> "NOT_SUPPORTED"
159+
160+
else -> "FAILED"
161+
}
162+
}
163+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.amazonaws.amplify.rtnpasskeys
2+
3+
import com.facebook.react.BaseReactPackage
4+
import com.facebook.react.bridge.NativeModule
5+
import com.facebook.react.bridge.ReactApplicationContext
6+
import com.facebook.react.module.model.ReactModuleInfo
7+
import com.facebook.react.module.model.ReactModuleInfoProvider
8+
9+
class AmplifyRtnPasskeysPackage : BaseReactPackage() {
10+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
11+
return if (name == AmplifyRtnPasskeysModule.NAME) {
12+
AmplifyRtnPasskeysModule(reactContext)
13+
} else {
14+
null
15+
}
16+
}
17+
18+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
19+
return ReactModuleInfoProvider {
20+
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
21+
moduleInfos[AmplifyRtnPasskeysModule.NAME] = ReactModuleInfo(
22+
AmplifyRtnPasskeysModule.NAME,
23+
AmplifyRtnPasskeysModule.NAME,
24+
false, // canOverrideExistingModule
25+
false, // needsEagerInit
26+
false, // isCxxModule
27+
true // isTurboModule
28+
)
29+
moduleInfos
30+
}
31+
}
32+
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import android.os.Build
2+
3+
import androidx.credentials.CreateCredentialRequest
4+
import androidx.credentials.CreatePublicKeyCredentialResponse
5+
import androidx.credentials.CredentialManager
6+
import androidx.credentials.GetCredentialRequest
7+
import androidx.credentials.GetCredentialResponse
8+
import androidx.credentials.PublicKeyCredential
9+
import androidx.credentials.exceptions.CreateCredentialUnsupportedException
10+
import androidx.credentials.exceptions.GetCredentialUnsupportedException
11+
import androidx.credentials.exceptions.domerrors.DataError
12+
import androidx.credentials.exceptions.domerrors.NotAllowedError
13+
import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException
14+
import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException
15+
16+
import com.amazonaws.amplify.rtnpasskeys.AmplifyRtnPasskeysModule
17+
18+
import com.facebook.react.bridge.JSONArguments
19+
import com.facebook.react.bridge.Promise
20+
21+
import com.facebook.react.bridge.ReactApplicationContext
22+
import com.facebook.react.bridge.ReadableMap
23+
24+
import io.mockk.coEvery
25+
import io.mockk.coVerify
26+
import io.mockk.every
27+
import io.mockk.mockk
28+
import io.mockk.mockkObject
29+
import io.mockk.mockkStatic
30+
import io.mockk.verify
31+
32+
import kotlinx.coroutines.ExperimentalCoroutinesApi
33+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
34+
import kotlinx.coroutines.test.runTest
35+
36+
import org.junit.Before
37+
import org.junit.Test
38+
39+
import org.junit.runner.RunWith
40+
41+
import org.robolectric.RobolectricTestRunner
42+
import org.robolectric.annotation.Config
43+
44+
@RunWith(RobolectricTestRunner::class)
45+
@OptIn(ExperimentalCoroutinesApi::class)
46+
@Config(sdk = [Build.VERSION_CODES.P])
47+
class AmplifyRtnPasskeysModuleTest {
48+
private val responseJson = """{"response":"json"}"""
49+
50+
private val context = mockk<ReactApplicationContext>(relaxed = true)
51+
52+
private val promise = mockk<Promise>(relaxed = true)
53+
54+
private val readableMap = mockk<ReadableMap> {
55+
every { toHashMap() } returns hashMapOf("user" to hashMapOf("name" to "james"))
56+
}
57+
58+
private val credentialManager = mockk<CredentialManager>()
59+
60+
private val module = AmplifyRtnPasskeysModule(context)
61+
62+
@Before
63+
fun setup() {
64+
// setup CredentialManager
65+
mockkObject(CredentialManager)
66+
every { CredentialManager.create(any()) } returns credentialManager
67+
68+
// setup JSONArguments
69+
mockkStatic(JSONArguments::class)
70+
every { JSONArguments.fromJSONObject(any()) } returns readableMap
71+
}
72+
73+
@Test
74+
fun getName_returnsCorrectName() {
75+
assert(module.name == "AmplifyRtnPasskeys")
76+
}
77+
78+
@Config(sdk = [Build.VERSION_CODES.O])
79+
@Test
80+
fun getIsPasskeySupported_returnsFalse_onUnsupportedDevice() {
81+
assert(!module.isPasskeySupported)
82+
}
83+
84+
@Test
85+
fun getIsPasskeySupported_returnsTrue_onSupportedDevice() {
86+
assert(module.isPasskeySupported)
87+
}
88+
89+
@Config(sdk = [Build.VERSION_CODES.O])
90+
@Test
91+
fun createPasskey_rejectsWithError_onUnsupportedDevice() {
92+
module.createPasskey(readableMap, promise)
93+
verify { promise.reject("NOT_SUPPORTED", any<CreateCredentialUnsupportedException>()) }
94+
}
95+
96+
@Test
97+
fun createPasskey_resolvesWithOutput_onSupportedDevice() = runTest {
98+
coEvery {
99+
credentialManager.getCredential(
100+
any(),
101+
any<GetCredentialRequest>()
102+
)
103+
} returns GetCredentialResponse(
104+
PublicKeyCredential(responseJson)
105+
)
106+
coEvery {
107+
credentialManager.createCredential(
108+
any(),
109+
any()
110+
)
111+
} returns CreatePublicKeyCredentialResponse(
112+
responseJson
113+
)
114+
AmplifyRtnPasskeysModule(context, UnconfinedTestDispatcher(testScheduler)).createPasskey(
115+
readableMap,
116+
promise
117+
)
118+
verify { promise.resolve(readableMap) }
119+
}
120+
121+
@Test
122+
fun createPasskey_rejectsWithError_whenCreateCredentialResultIsInvalid() = runTest {
123+
coEvery { credentialManager.createCredential(any(), any()) } returns mockk()
124+
AmplifyRtnPasskeysModule(context, UnconfinedTestDispatcher(testScheduler)).createPasskey(
125+
readableMap,
126+
promise
127+
)
128+
coVerify { credentialManager.createCredential(any(), any<CreateCredentialRequest>()) }
129+
verify { promise.reject("FAILED", any<Exception>()) }
130+
}
131+
132+
@Test
133+
fun createPasskey_rejectsWithError_whenDomExceptionThrown() = runTest {
134+
coEvery {
135+
credentialManager.createCredential(
136+
any(),
137+
any()
138+
)
139+
} throws CreatePublicKeyCredentialDomException(NotAllowedError())
140+
AmplifyRtnPasskeysModule(context, UnconfinedTestDispatcher(testScheduler)).createPasskey(
141+
readableMap,
142+
promise
143+
)
144+
coVerify { credentialManager.createCredential(any(), any<CreateCredentialRequest>()) }
145+
verify { promise.reject("CANCELED", any<CreatePublicKeyCredentialDomException>()) }
146+
}
147+
148+
@Config(sdk = [Build.VERSION_CODES.O])
149+
@Test
150+
fun getPasskey_rejectsWithError_onUnsupportedDevice() = runTest {
151+
module.getPasskey(readableMap, promise)
152+
verify { promise.reject("NOT_SUPPORTED", any<GetCredentialUnsupportedException>()) }
153+
}
154+
155+
@Test
156+
fun getPasskey_resolvesWithOutput_onSupportedDevice() = runTest {
157+
coEvery {
158+
credentialManager.getCredential(
159+
any(),
160+
any<GetCredentialRequest>()
161+
)
162+
} returns GetCredentialResponse(
163+
PublicKeyCredential(responseJson)
164+
)
165+
coEvery {
166+
credentialManager.createCredential(
167+
any(),
168+
any()
169+
)
170+
} returns CreatePublicKeyCredentialResponse(
171+
responseJson
172+
)
173+
AmplifyRtnPasskeysModule(context, UnconfinedTestDispatcher(testScheduler)).getPasskey(
174+
readableMap,
175+
promise
176+
)
177+
verify { promise.resolve(readableMap) }
178+
}
179+
180+
@Test
181+
fun getPasskey_rejectsWithError_whenGetCredentialResultIsInvalid() = runTest {
182+
coEvery {
183+
credentialManager.getCredential(
184+
any(),
185+
any<GetCredentialRequest>()
186+
)
187+
} returns mockk<GetCredentialResponse> {
188+
coEvery { credential } returns mockk()
189+
}
190+
AmplifyRtnPasskeysModule(context, UnconfinedTestDispatcher(testScheduler)).getPasskey(
191+
readableMap,
192+
promise
193+
)
194+
coVerify { credentialManager.getCredential(any(), any<GetCredentialRequest>()) }
195+
verify { promise.reject("FAILED", any<Exception>()) }
196+
}
197+
198+
@Test
199+
fun getPasskey_rejectsWithError_whenDomExceptionThrown() = runTest {
200+
coEvery {
201+
credentialManager.getCredential(
202+
any(),
203+
any<GetCredentialRequest>()
204+
)
205+
} throws GetPublicKeyCredentialDomException(DataError())
206+
207+
AmplifyRtnPasskeysModule(context, UnconfinedTestDispatcher(testScheduler)).getPasskey(
208+
readableMap,
209+
promise
210+
)
211+
coVerify { credentialManager.getCredential(any(), any<GetCredentialRequest>()) }
212+
verify {
213+
promise.reject(
214+
"RELYING_PARTY_MISMATCH",
215+
any<GetPublicKeyCredentialDomException>()
216+
)
217+
}
218+
}
219+
220+
}

0 commit comments

Comments
 (0)
Please sign in to comment.