@algorandfoundation/react-native-passkey-autofill 0.0.1 → 1.0.0-canary.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +8 -0
- package/.releaserc +23 -0
- package/ARCHITECTURE.md +67 -0
- package/CHANGELOG.md +42 -0
- package/CONTRIBUTING.md +82 -0
- package/LICENSE +201 -0
- package/README.md +135 -4
- package/android/build.gradle +86 -0
- package/android/libs/dP256Android-release.aar +0 -0
- package/android/src/main/AndroidManifest.xml +37 -0
- package/android/src/main/java/co/algorand/passkeyautofill/CreatePasskeyActivity.kt +318 -0
- package/android/src/main/java/co/algorand/passkeyautofill/GetPasskeyActivity.kt +327 -0
- package/android/src/main/java/co/algorand/passkeyautofill/ReactNativePasskeyAutofillModule.kt +84 -0
- package/android/src/main/java/co/algorand/passkeyautofill/credentials/Credential.kt +11 -0
- package/android/src/main/java/co/algorand/passkeyautofill/credentials/CredentialRepository.kt +506 -0
- package/android/src/main/java/co/algorand/passkeyautofill/service/PasskeyAutofillCredentialProviderService.kt +248 -0
- package/android/src/main/java/co/algorand/passkeyautofill/utils/PasskeyUtils.kt +7 -0
- package/android/src/main/res/values/strings.xml +3 -0
- package/android/src/main/res/values/styles.xml +10 -0
- package/android/src/main/res/xml/provider.xml +5 -0
- package/app.plugin.js +249 -0
- package/assets/banner.png +0 -0
- package/build/ReactNativePasskeyAutofill.types.d.ts +9 -0
- package/build/ReactNativePasskeyAutofill.types.d.ts.map +1 -0
- package/build/ReactNativePasskeyAutofill.types.js +2 -0
- package/build/ReactNativePasskeyAutofill.types.js.map +1 -0
- package/build/ReactNativePasskeyAutofillModule.d.ts +12 -0
- package/build/ReactNativePasskeyAutofillModule.d.ts.map +1 -0
- package/build/ReactNativePasskeyAutofillModule.js +4 -0
- package/build/ReactNativePasskeyAutofillModule.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +10 -0
- package/ios/ReactNativePasskeyAutofill.podspec +29 -0
- package/ios/ReactNativePasskeyAutofillModule.swift +49 -0
- package/ios/ReactNativePasskeyAutofillView.swift +38 -0
- package/lefthook.yml +13 -0
- package/package.json +64 -8
- package/src/ReactNativePasskeyAutofill.types.ts +4 -0
- package/src/ReactNativePasskeyAutofillModule.ts +14 -0
- package/src/index.ts +4 -0
- package/tsconfig.json +9 -0
- package/index.js +0 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
package co.algorand.passkeyautofill.service
|
|
2
|
+
|
|
3
|
+
import android.app.PendingIntent
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.graphics.drawable.Icon
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.os.Bundle
|
|
8
|
+
import android.os.OutcomeReceiver
|
|
9
|
+
import android.os.CancellationSignal
|
|
10
|
+
import android.util.Log
|
|
11
|
+
import androidx.annotation.RequiresApi
|
|
12
|
+
import androidx.credentials.exceptions.ClearCredentialException
|
|
13
|
+
import androidx.credentials.exceptions.CreateCredentialException
|
|
14
|
+
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
|
15
|
+
import androidx.credentials.exceptions.GetCredentialException
|
|
16
|
+
import androidx.credentials.exceptions.GetCredentialUnknownException
|
|
17
|
+
import androidx.credentials.provider.BeginCreateCredentialRequest
|
|
18
|
+
import androidx.credentials.provider.BeginCreateCredentialResponse
|
|
19
|
+
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
|
20
|
+
import androidx.credentials.provider.BeginGetCredentialRequest
|
|
21
|
+
import androidx.credentials.provider.BeginGetCredentialResponse
|
|
22
|
+
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
|
23
|
+
import androidx.credentials.provider.CreateEntry
|
|
24
|
+
import androidx.credentials.provider.CredentialProviderService
|
|
25
|
+
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
|
26
|
+
import androidx.credentials.provider.PublicKeyCredentialEntry
|
|
27
|
+
import co.algorand.passkeyautofill.credentials.CredentialRepository
|
|
28
|
+
import co.algorand.passkeyautofill.credentials.Credential
|
|
29
|
+
import co.algorand.passkeyautofill.utils.PasskeyUtils
|
|
30
|
+
import com.tencent.mmkv.MMKV
|
|
31
|
+
import kotlinx.coroutines.*
|
|
32
|
+
import org.json.JSONArray
|
|
33
|
+
import org.json.JSONObject
|
|
34
|
+
|
|
35
|
+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
36
|
+
class PasskeyAutofillCredentialProviderService: CredentialProviderService() {
|
|
37
|
+
private val credentialRepository = CredentialRepository()
|
|
38
|
+
private val job = SupervisorJob()
|
|
39
|
+
private val scope = CoroutineScope(Dispatchers.IO + job)
|
|
40
|
+
|
|
41
|
+
companion object {
|
|
42
|
+
const val TAG = "PasskeyAutofillCredentialProviderService"
|
|
43
|
+
//TODO: App Lock Intents
|
|
44
|
+
const val GET_PASSKEY_INTENT = 1
|
|
45
|
+
const val CREATE_PASSKEY_INTENT = 2
|
|
46
|
+
const val DEFAULT_GET_PASSKEY_ACTION = "co.algorand.passkeyautofill.GET_PASSKEY"
|
|
47
|
+
const val DEFAULT_CREATE_PASSKEY_ACTION = "co.algorand.passkeyautofill.CREATE_PASSKEY"
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Handle Create Credential Requests
|
|
51
|
+
*/
|
|
52
|
+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
53
|
+
override fun onBeginCreateCredentialRequest(
|
|
54
|
+
request: BeginCreateCredentialRequest,
|
|
55
|
+
cancellationSignal: CancellationSignal,
|
|
56
|
+
callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>
|
|
57
|
+
) {
|
|
58
|
+
val response: BeginCreateCredentialResponse? = processCreateCredentialRequest(request)
|
|
59
|
+
if (response != null) {
|
|
60
|
+
callback.onResult(response)
|
|
61
|
+
} else {
|
|
62
|
+
callback.onError(CreateCredentialUnknownException())
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Process incoming Create Credential Requests
|
|
68
|
+
*/
|
|
69
|
+
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
|
|
70
|
+
when (request) {
|
|
71
|
+
is BeginCreatePublicKeyCredentialRequest -> {
|
|
72
|
+
return handleCreatePasskeyQuery(request)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a new PassKey Entry
|
|
80
|
+
*
|
|
81
|
+
* This returns an Entry list for the user to interact with.
|
|
82
|
+
* A PendingIntent must be configured to receive the data from the WebAuthn client
|
|
83
|
+
*/
|
|
84
|
+
private fun handleCreatePasskeyQuery(
|
|
85
|
+
request: BeginCreatePublicKeyCredentialRequest
|
|
86
|
+
): BeginCreateCredentialResponse {
|
|
87
|
+
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
|
88
|
+
|
|
89
|
+
// Ensure we have a master key available before offering to create a passkey
|
|
90
|
+
if (!credentialRepository.isMasterKeyAvailable(this)) {
|
|
91
|
+
return BeginCreateCredentialResponse(createEntries)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
val userJson = JSONObject(request.requestJson).optJSONObject("user")
|
|
95
|
+
val name = userJson?.optString("name") ?: "New Passkey"
|
|
96
|
+
|
|
97
|
+
val action = credentialRepository.getCreatePasskeyAction(this) ?: DEFAULT_CREATE_PASSKEY_ACTION
|
|
98
|
+
|
|
99
|
+
val data = Bundle()
|
|
100
|
+
data.putString("requestJson", request.requestJson)
|
|
101
|
+
|
|
102
|
+
val createEntry = CreateEntry.Builder(
|
|
103
|
+
name,
|
|
104
|
+
createNewPendingIntent(action, CREATE_PASSKEY_INTENT, data)
|
|
105
|
+
).setIcon(Icon.createWithResource(this, android.R.drawable.ic_menu_add))
|
|
106
|
+
.build()
|
|
107
|
+
|
|
108
|
+
createEntries.add(createEntry)
|
|
109
|
+
return BeginCreateCredentialResponse(createEntries)
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Handle Get Credential Requests
|
|
113
|
+
*/
|
|
114
|
+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
115
|
+
override fun onBeginGetCredentialRequest(
|
|
116
|
+
request: BeginGetCredentialRequest,
|
|
117
|
+
cancellationSignal: CancellationSignal,
|
|
118
|
+
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
|
|
119
|
+
) {
|
|
120
|
+
try {
|
|
121
|
+
val response = processGetCredentialRequest(request)
|
|
122
|
+
callback.onResult(response)
|
|
123
|
+
} catch (e: Exception) {
|
|
124
|
+
Log.e(TAG, "Error in onBeginGetCredentialRequest", e)
|
|
125
|
+
callback.onError(GetCredentialUnknownException())
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Fake a list of available PublicKeyCredential Entries
|
|
131
|
+
*/
|
|
132
|
+
private fun processGetCredentialRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse {
|
|
133
|
+
// Ensure we have a master key available. If not, we can't sign anything, so don't show entries.
|
|
134
|
+
if (!credentialRepository.isMasterKeyAvailable(this)) {
|
|
135
|
+
return BeginGetCredentialResponse(emptyList())
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
val credentials = runBlocking {
|
|
139
|
+
credentialRepository.getAllCredentials(this@PasskeyAutofillCredentialProviderService)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
val action = credentialRepository.getGetPasskeyAction(this) ?: DEFAULT_GET_PASSKEY_ACTION
|
|
143
|
+
val allEntries = mutableListOf<PublicKeyCredentialEntry>()
|
|
144
|
+
|
|
145
|
+
for (option in request.beginGetCredentialOptions) {
|
|
146
|
+
if (option !is BeginGetPublicKeyCredentialOption) continue
|
|
147
|
+
|
|
148
|
+
val requestJsonStr = option.requestJson
|
|
149
|
+
var allowCredentials: JSONArray? = null
|
|
150
|
+
try {
|
|
151
|
+
val json = JSONObject(requestJsonStr)
|
|
152
|
+
val pk = if (json.has("publicKey")) json.getJSONObject("publicKey") else json
|
|
153
|
+
allowCredentials = pk.optJSONArray("allowCredentials")
|
|
154
|
+
} catch (e: Exception) {
|
|
155
|
+
Log.e(TAG, "Error parsing requestJson", e)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
val allowedIds = if (allowCredentials != null && allowCredentials.length() > 0) {
|
|
159
|
+
val ids = mutableSetOf<String>()
|
|
160
|
+
for (i in 0 until allowCredentials.length()) {
|
|
161
|
+
ids.add(allowCredentials.getJSONObject(i).getString("id"))
|
|
162
|
+
}
|
|
163
|
+
ids
|
|
164
|
+
} else {
|
|
165
|
+
null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (credential in credentials) {
|
|
169
|
+
if (allowedIds != null) {
|
|
170
|
+
val isAllowed = allowedIds.any { allowedId ->
|
|
171
|
+
allowedId == credential.credentialId ||
|
|
172
|
+
PasskeyUtils.normalizeBase64(allowedId) == PasskeyUtils.normalizeBase64(credential.credentialId)
|
|
173
|
+
}
|
|
174
|
+
if (!isAllowed) continue
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
val data = Bundle()
|
|
179
|
+
data.putString("credentialId", credential.credentialId)
|
|
180
|
+
data.putString("userHandle", credential.userHandle)
|
|
181
|
+
data.putString("requestJson", option.requestJson)
|
|
182
|
+
|
|
183
|
+
// Use a unique requestCode for each PendingIntent to avoid data being overwritten
|
|
184
|
+
val uniqueRequestCode = (credential.credentialId + option.requestJson).hashCode()
|
|
185
|
+
|
|
186
|
+
val entry = PublicKeyCredentialEntry.Builder(
|
|
187
|
+
this,
|
|
188
|
+
credential.userHandle,
|
|
189
|
+
createNewPendingIntent(action, uniqueRequestCode, data),
|
|
190
|
+
option
|
|
191
|
+
)
|
|
192
|
+
.setIcon(Icon.createWithResource(this, android.R.drawable.ic_lock_lock))
|
|
193
|
+
.build()
|
|
194
|
+
allEntries.add(entry)
|
|
195
|
+
} catch (e: Exception) {
|
|
196
|
+
Log.e(TAG, "Error building PublicKeyCredentialEntry", e)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return BeginGetCredentialResponse(allEntries)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
override fun onClearCredentialStateRequest(
|
|
204
|
+
request: ProviderClearCredentialStateRequest,
|
|
205
|
+
cancellationSignal: CancellationSignal,
|
|
206
|
+
callback: OutcomeReceiver<Void?, ClearCredentialException>
|
|
207
|
+
) {
|
|
208
|
+
// TODO: Implement clear credential state if needed
|
|
209
|
+
callback.onResult(null)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private fun createNewPendingIntent(action: String, requestCode: Int, extra: Bundle?): PendingIntent{
|
|
213
|
+
MMKV.initialize(this)
|
|
214
|
+
val intent = Intent(action)
|
|
215
|
+
|
|
216
|
+
// Determine component based on action instead of requestCode
|
|
217
|
+
val getAction = credentialRepository.getGetPasskeyAction(this) ?: DEFAULT_GET_PASSKEY_ACTION
|
|
218
|
+
val createAction = credentialRepository.getCreatePasskeyAction(this) ?: DEFAULT_CREATE_PASSKEY_ACTION
|
|
219
|
+
|
|
220
|
+
val componentName = when(action) {
|
|
221
|
+
getAction -> android.content.ComponentName(packageName, "co.algorand.passkeyautofill.GetPasskeyActivity")
|
|
222
|
+
createAction -> android.content.ComponentName(packageName, "co.algorand.passkeyautofill.CreatePasskeyActivity")
|
|
223
|
+
else -> null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (componentName != null) {
|
|
227
|
+
intent.component = componentName
|
|
228
|
+
} else {
|
|
229
|
+
intent.setPackage(packageName)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (extra != null) {
|
|
233
|
+
intent.putExtra("CREDENTIAL_DATA", extra)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Add categories that might be required
|
|
237
|
+
intent.addCategory(android.content.Intent.CATEGORY_DEFAULT)
|
|
238
|
+
|
|
239
|
+
return PendingIntent.getActivity(
|
|
240
|
+
applicationContext, requestCode,
|
|
241
|
+
intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
override fun onDestroy() {
|
|
245
|
+
super.onDestroy()
|
|
246
|
+
job.cancel()
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<resources>
|
|
2
|
+
<style name="Theme.PasskeyAutofill.Dialog" parent="Theme.AppCompat.Light.Dialog.MinWidth">
|
|
3
|
+
<item name="android:windowIsTranslucent">true</item>
|
|
4
|
+
<item name="android:windowBackground">@android:color/transparent</item>
|
|
5
|
+
<item name="android:windowContentOverlay">@null</item>
|
|
6
|
+
<item name="android:windowNoTitle">true</item>
|
|
7
|
+
<item name="android:windowIsFloating">true</item>
|
|
8
|
+
<item name="android:backgroundDimEnabled">true</item>
|
|
9
|
+
</style>
|
|
10
|
+
</resources>
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const {
|
|
2
|
+
withAndroidManifest,
|
|
3
|
+
withStringsXml,
|
|
4
|
+
withProjectBuildGradle,
|
|
5
|
+
withMainApplication,
|
|
6
|
+
withDangerousMod,
|
|
7
|
+
AndroidConfig,
|
|
8
|
+
} = require("@expo/config-plugins");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
|
|
12
|
+
const withAndroidCookieModule = (config) => {
|
|
13
|
+
return withDangerousMod(config, [
|
|
14
|
+
"android",
|
|
15
|
+
async (config) => {
|
|
16
|
+
const projectRoot = config.modRequest.projectRoot;
|
|
17
|
+
const packageId = config.android?.package || "co.algorand.auth.example";
|
|
18
|
+
const packagePath = packageId.replace(/\./g, "/");
|
|
19
|
+
const targetDir = path.join(
|
|
20
|
+
projectRoot,
|
|
21
|
+
"android/app/src/main/java",
|
|
22
|
+
packagePath
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Ensure the directory exists
|
|
26
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
const cookieModuleContent = `package ${packageId}
|
|
29
|
+
|
|
30
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
31
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
32
|
+
import com.facebook.react.bridge.ReactMethod
|
|
33
|
+
import com.facebook.react.bridge.Promise
|
|
34
|
+
import android.webkit.CookieManager
|
|
35
|
+
|
|
36
|
+
class CookieModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
37
|
+
override fun getName(): String {
|
|
38
|
+
return "CookieModule"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@ReactMethod
|
|
42
|
+
fun getCookie(url: String, promise: Promise) {
|
|
43
|
+
try {
|
|
44
|
+
val cookieManager = CookieManager.getInstance()
|
|
45
|
+
val cookie = cookieManager.getCookie(url)
|
|
46
|
+
promise.resolve(cookie)
|
|
47
|
+
} catch (e: Exception) {
|
|
48
|
+
promise.reject("E_COOKIE_MANAGER", e.message)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@ReactMethod
|
|
53
|
+
fun setCookie(url: String, cookie: String, promise: Promise) {
|
|
54
|
+
try {
|
|
55
|
+
val cookieManager = CookieManager.getInstance()
|
|
56
|
+
cookieManager.setCookie(url, cookie)
|
|
57
|
+
promise.resolve(null)
|
|
58
|
+
} catch (e: Exception) {
|
|
59
|
+
promise.reject("E_COOKIE_MANAGER", e.message)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
const cookiePackageContent = `package ${packageId}
|
|
66
|
+
|
|
67
|
+
import com.facebook.react.ReactPackage
|
|
68
|
+
import com.facebook.react.bridge.NativeModule
|
|
69
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
70
|
+
import com.facebook.react.uimanager.ViewManager
|
|
71
|
+
|
|
72
|
+
class CookiePackage : ReactPackage {
|
|
73
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
74
|
+
return listOf(CookieModule(reactContext))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
78
|
+
return emptyList()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
fs.writeFileSync(
|
|
84
|
+
path.join(targetDir, "CookieModule.kt"),
|
|
85
|
+
cookieModuleContent
|
|
86
|
+
);
|
|
87
|
+
fs.writeFileSync(
|
|
88
|
+
path.join(targetDir, "CookiePackage.kt"),
|
|
89
|
+
cookiePackageContent
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return config;
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const withUserAgent = (config) => {
|
|
98
|
+
return withMainApplication(config, (config) => {
|
|
99
|
+
let content = config.modResults.contents;
|
|
100
|
+
|
|
101
|
+
// Add imports
|
|
102
|
+
const imports = [
|
|
103
|
+
"import android.webkit.CookieManager",
|
|
104
|
+
"import com.facebook.react.modules.network.OkHttpClientProvider",
|
|
105
|
+
"import com.facebook.react.modules.network.ForwardingCookieHandler",
|
|
106
|
+
"import com.facebook.react.modules.network.ReactCookieJarContainer",
|
|
107
|
+
"import okhttp3.Interceptor",
|
|
108
|
+
"import okhttp3.JavaNetCookieJar",
|
|
109
|
+
"import java.net.CookieHandler",
|
|
110
|
+
"import android.os.Build",
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
imports.forEach((imp) => {
|
|
114
|
+
if (!content.includes(imp)) {
|
|
115
|
+
content = content.replace(
|
|
116
|
+
/package .*\n/,
|
|
117
|
+
(match) => `${match}${imp}\n`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Add OkHttpClient customization in onCreate
|
|
123
|
+
const okHttpClientCode = `
|
|
124
|
+
CookieManager.getInstance().setAcceptCookie(true)
|
|
125
|
+
CookieManager.setAcceptFileSchemeCookies(true)
|
|
126
|
+
|
|
127
|
+
OkHttpClientProvider.setOkHttpClientFactory {
|
|
128
|
+
val userAgent = "\${BuildConfig.APPLICATION_ID}/\${BuildConfig.VERSION_NAME} " +
|
|
129
|
+
"(Android \${Build.VERSION.RELEASE}; \${Build.MODEL}; \${Build.BRAND})"
|
|
130
|
+
|
|
131
|
+
val cookieHandler = ForwardingCookieHandler()
|
|
132
|
+
CookieHandler.setDefault(cookieHandler)
|
|
133
|
+
|
|
134
|
+
val cookieJarContainer = ReactCookieJarContainer()
|
|
135
|
+
cookieJarContainer.setCookieJar(JavaNetCookieJar(cookieHandler))
|
|
136
|
+
|
|
137
|
+
OkHttpClientProvider.createClientBuilder()
|
|
138
|
+
.cookieJar(cookieJarContainer)
|
|
139
|
+
.addInterceptor(Interceptor { chain ->
|
|
140
|
+
val request = chain.request().newBuilder().header("User-Agent", userAgent).build()
|
|
141
|
+
chain.proceed(request)
|
|
142
|
+
})
|
|
143
|
+
.build()
|
|
144
|
+
}
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
if (!content.includes("OkHttpClientProvider.setOkHttpClientFactory")) {
|
|
148
|
+
content = content.replace(
|
|
149
|
+
/super\.onCreate\(\)/,
|
|
150
|
+
`super.onCreate()${okHttpClientCode}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Register CookiePackage if it exists
|
|
155
|
+
if (!content.includes("add(CookiePackage())")) {
|
|
156
|
+
content = content.replace(
|
|
157
|
+
/PackageList\(this\)\.packages\.apply \{/,
|
|
158
|
+
`PackageList(this).packages.apply {\n add(CookiePackage())`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
config.modResults.contents = content;
|
|
163
|
+
return config;
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const withPasskeyAutofill = (config, props = {}) => {
|
|
168
|
+
const site = props.site || "https://fido.shore-tech.net";
|
|
169
|
+
const label = props.label || "My Credential Provider";
|
|
170
|
+
|
|
171
|
+
config = withAndroidCookieModule(config);
|
|
172
|
+
config = withUserAgent(config);
|
|
173
|
+
|
|
174
|
+
// TODO: Move assetlinks to CLI
|
|
175
|
+
// 1. Add asset_statements meta-data to MainActivity
|
|
176
|
+
config = withAndroidManifest(config, async (config) => {
|
|
177
|
+
const mainActivity = config.modResults.manifest.application[0].activity.find(
|
|
178
|
+
(a) => a["$"]["android:name"] === ".MainActivity"
|
|
179
|
+
);
|
|
180
|
+
if (mainActivity) {
|
|
181
|
+
if (!mainActivity["meta-data"]) {
|
|
182
|
+
mainActivity["meta-data"] = [];
|
|
183
|
+
}
|
|
184
|
+
if (
|
|
185
|
+
!mainActivity["meta-data"].find(
|
|
186
|
+
(m) => m["$"]["android:name"] === "asset_statements"
|
|
187
|
+
)
|
|
188
|
+
) {
|
|
189
|
+
mainActivity["meta-data"].push({
|
|
190
|
+
$: {
|
|
191
|
+
"android:name": "asset_statements",
|
|
192
|
+
"android:resource": "@string/asset_statements",
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return config;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// 2. Add asset_statements string to strings.xml
|
|
201
|
+
config = withStringsXml(config, (config) => {
|
|
202
|
+
config.modResults = AndroidConfig.Strings.setStringItem(
|
|
203
|
+
[
|
|
204
|
+
{
|
|
205
|
+
$: { name: "asset_statements", translatable: "false" },
|
|
206
|
+
_: JSON.stringify([
|
|
207
|
+
{
|
|
208
|
+
relation: [
|
|
209
|
+
"delegate_permission/common.handle_all_urls",
|
|
210
|
+
"delegate_permission/common.get_login_creds",
|
|
211
|
+
],
|
|
212
|
+
target: {
|
|
213
|
+
namespace: "web",
|
|
214
|
+
site: site,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
]),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
$: { name: "passkey_autofill_label", translatable: "true" },
|
|
221
|
+
_: label,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
config.modResults
|
|
225
|
+
);
|
|
226
|
+
return config;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// 3. Add flatDir repository for local AAR
|
|
230
|
+
config = withProjectBuildGradle(config, (config) => {
|
|
231
|
+
if (config.modResults.contents.includes("dP256Android-release")) {
|
|
232
|
+
return config;
|
|
233
|
+
}
|
|
234
|
+
const libsDir = "../../android/libs";
|
|
235
|
+
config.modResults.contents = config.modResults.contents.replace(
|
|
236
|
+
/allprojects\s*{[\s\n]*repositories\s*{/,
|
|
237
|
+
`allprojects {
|
|
238
|
+
repositories {
|
|
239
|
+
flatDir {
|
|
240
|
+
dirs "$rootDir/${libsDir}"
|
|
241
|
+
}`
|
|
242
|
+
);
|
|
243
|
+
return config;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return config;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
module.exports = withPasskeyAutofill;
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactNativePasskeyAutofill.types.d.ts","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofill.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sCAAsC,GAAG;IACnD,cAAc,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IACtD,sBAAsB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;CAC/D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactNativePasskeyAutofill.types.js","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofill.types.ts"],"names":[],"mappings":"","sourcesContent":["export type ReactNativePasskeyAutofillModuleEvents = {\n onPasskeyAdded: (event: { success: boolean }) => void;\n onPasskeyAuthenticated: (event: { success: boolean }) => void;\n};\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NativeModule } from 'expo';
|
|
2
|
+
import { ReactNativePasskeyAutofillModuleEvents } from './ReactNativePasskeyAutofill.types';
|
|
3
|
+
declare class ReactNativePasskeyAutofillModule extends NativeModule<ReactNativePasskeyAutofillModuleEvents> {
|
|
4
|
+
setMasterKey(secret: string): Promise<void>;
|
|
5
|
+
setHdRootKeyId(id: string): Promise<void>;
|
|
6
|
+
getHdRootKeyId(): Promise<string | null>;
|
|
7
|
+
configureIntentActions(getPasskeyAction: string, createPasskeyAction: string): Promise<void>;
|
|
8
|
+
clearCredentials(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
declare const _default: ReactNativePasskeyAutofillModule;
|
|
11
|
+
export default _default;
|
|
12
|
+
//# sourceMappingURL=ReactNativePasskeyAutofillModule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactNativePasskeyAutofillModule.d.ts","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofillModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EAAE,sCAAsC,EAAE,MAAM,oCAAoC,CAAC;AAE5F,OAAO,OAAO,gCAAiC,SAAQ,YAAY,CAAC,sCAAsC,CAAC;IACzG,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3C,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IACzC,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IACxC,sBAAsB,CAAC,gBAAgB,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC5F,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;CAClC;;AAGD,wBAAmG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactNativePasskeyAutofillModule.js","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofillModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAYzD,yDAAyD;AACzD,eAAe,mBAAmB,CAAmC,4BAA4B,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\nimport { ReactNativePasskeyAutofillModuleEvents } from './ReactNativePasskeyAutofill.types';\n\ndeclare class ReactNativePasskeyAutofillModule extends NativeModule<ReactNativePasskeyAutofillModuleEvents> {\n setMasterKey(secret: string): Promise<void>;\n setHdRootKeyId(id: string): Promise<void>;\n getHdRootKeyId(): Promise<string | null>;\n configureIntentActions(getPasskeyAction: string, createPasskeyAction: string): Promise<void>;\n clearCredentials(): Promise<void>;\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<ReactNativePasskeyAutofillModule>('ReactNativePasskeyAutofill');\n"]}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAC7D,cAAe,oCAAoC,CAAC"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Reexport the native module. On web, it will be resolved to ReactNativePasskeyAutofillModule.web.ts
|
|
2
|
+
// and on native platforms to ReactNativePasskeyAutofillModule.ts
|
|
3
|
+
export { default } from './ReactNativePasskeyAutofillModule';
|
|
4
|
+
export * from './ReactNativePasskeyAutofill.types';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,iEAAiE;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAC7D,cAAe,oCAAoC,CAAC","sourcesContent":["// Reexport the native module. On web, it will be resolved to ReactNativePasskeyAutofillModule.web.ts\n// and on native platforms to ReactNativePasskeyAutofillModule.ts\nexport { default } from './ReactNativePasskeyAutofillModule';\nexport * from './ReactNativePasskeyAutofill.types';\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'ReactNativePasskeyAutofill'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platforms = {
|
|
14
|
+
:ios => '15.1',
|
|
15
|
+
:tvos => '15.1'
|
|
16
|
+
}
|
|
17
|
+
s.swift_version = '5.9'
|
|
18
|
+
s.source = { git: 'https://github.com/algorandfoundation/react-native-passkey-autofill' }
|
|
19
|
+
s.static_framework = true
|
|
20
|
+
|
|
21
|
+
s.dependency 'ExpoModulesCore'
|
|
22
|
+
|
|
23
|
+
# Swift/Objective-C compatibility
|
|
24
|
+
s.pod_target_xcconfig = {
|
|
25
|
+
'DEFINES_MODULE' => 'YES',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
29
|
+
end
|