@pyrocancode/react-native-vk-auth 1.0.2 → 1.1.0
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/README.md +22 -0
- package/android/src/main/java/com/vkauth/vkid/AuthDelegate.kt +129 -3
- package/android/src/main/java/com/vkauth/vkid/InitDelegate.kt +7 -2
- package/android/src/main/java/com/vkauth/vkid/Pkce.kt +21 -0
- package/android/src/main/java/com/vkauth/vkid/VkAuthConfig.kt +10 -0
- package/android/src/main/java/com/vkauth/vkid/VkAuthPayload.kt +17 -0
- package/android/src/main/java/com/vkauth/vkid/jsinput/App.kt +33 -2
- package/android/src/main/java/com/vkauth/vkid/onetapbutton/OneTapButtonManager.kt +9 -2
- package/ios/VkAuth.swift +76 -13
- package/package.json +1 -1
- package/src/index.tsx +42 -2
package/README.md
CHANGED
|
@@ -181,6 +181,28 @@ VK.initialize(
|
|
|
181
181
|
);
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
#### Режим `authFlow`
|
|
185
|
+
|
|
186
|
+
По умолчанию после входа на устройстве доступен **access token** (`payload.accessToken`, `payload.userId`).
|
|
187
|
+
|
|
188
|
+
Если нужно обменять код на своём бэкенде (PKCE + `code` / `device_id` / `state` / `code_verifier`), передайте при инициализации:
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
VK.initialize(
|
|
192
|
+
{
|
|
193
|
+
credentials: {
|
|
194
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
195
|
+
clientSecret: 'YOUR_CLIENT_SECRET',
|
|
196
|
+
},
|
|
197
|
+
mode: VK.Mode.DEBUG,
|
|
198
|
+
authFlow: 'authorizationCode',
|
|
199
|
+
},
|
|
200
|
+
vkid
|
|
201
|
+
);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
В `onAuthorized` тогда придёт `payload.authorizationCode` с полями `code`, `codeVerifier`, `state`, `deviceId`, `isCompletion` (без `accessToken` в этом потоке). Поддерживается на **iOS и Android**.
|
|
205
|
+
|
|
184
206
|
### 2. Подписка на авторизацию и выход
|
|
185
207
|
|
|
186
208
|
```tsx
|
|
@@ -12,8 +12,10 @@ import com.vk.id.VKIDAuthFail
|
|
|
12
12
|
import com.vk.id.auth.AuthCodeData
|
|
13
13
|
import com.vk.id.auth.VKIDAuthCallback
|
|
14
14
|
import com.vk.id.auth.VKIDAuthParams
|
|
15
|
+
import com.vk.id.auth.VKIDAuthUiParams
|
|
15
16
|
import com.vk.id.logout.VKIDLogoutCallback
|
|
16
17
|
import com.vk.id.logout.VKIDLogoutFail
|
|
18
|
+
import java.util.UUID
|
|
17
19
|
|
|
18
20
|
class AuthDelegate(private val reactContext: ReactApplicationContext) {
|
|
19
21
|
|
|
@@ -21,6 +23,11 @@ class AuthDelegate(private val reactContext: ReactApplicationContext) {
|
|
|
21
23
|
private const val TAG = "VkAuth"
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
/** Для потока authorization code: переданы в authorize() и нужны бэкенду вместе с [AuthCodeData]. */
|
|
27
|
+
private var pendingCodeVerifier: String? = null
|
|
28
|
+
private var pendingState: String? = null
|
|
29
|
+
private var authorizationCodeEmitted: Boolean = false
|
|
30
|
+
|
|
24
31
|
init {
|
|
25
32
|
VKID.logsEnabled = true
|
|
26
33
|
}
|
|
@@ -32,6 +39,27 @@ class AuthDelegate(private val reactContext: ReactApplicationContext) {
|
|
|
32
39
|
emitAuthFail("No activity")
|
|
33
40
|
return
|
|
34
41
|
}
|
|
42
|
+
|
|
43
|
+
authorizationCodeEmitted = false
|
|
44
|
+
pendingCodeVerifier = null
|
|
45
|
+
pendingState = null
|
|
46
|
+
|
|
47
|
+
val params =
|
|
48
|
+
VKIDAuthParams.Builder().apply {
|
|
49
|
+
if (VkAuthConfig.useAuthorizationCodeFlow) {
|
|
50
|
+
val verifier = Pkce.generateVerifier()
|
|
51
|
+
val challenge = Pkce.challengeS256(verifier)
|
|
52
|
+
val state = UUID.randomUUID().toString()
|
|
53
|
+
pendingCodeVerifier = verifier
|
|
54
|
+
pendingState = state
|
|
55
|
+
codeChallenge = challenge
|
|
56
|
+
this.state = state
|
|
57
|
+
}
|
|
58
|
+
if (VkAuthConfig.scopes.isNotEmpty()) {
|
|
59
|
+
scopes = VkAuthConfig.scopes
|
|
60
|
+
}
|
|
61
|
+
}.build()
|
|
62
|
+
|
|
35
63
|
VKID.instance.authorize(
|
|
36
64
|
activity,
|
|
37
65
|
object : VKIDAuthCallback {
|
|
@@ -44,18 +72,96 @@ class AuthDelegate(private val reactContext: ReactApplicationContext) {
|
|
|
44
72
|
}
|
|
45
73
|
|
|
46
74
|
override fun onAuthCode(data: AuthCodeData, isCompletion: Boolean) {
|
|
47
|
-
|
|
75
|
+
dispatchAuthCode(data, isCompletion)
|
|
48
76
|
}
|
|
49
77
|
|
|
50
78
|
override fun onFail(fail: VKIDAuthFail) {
|
|
51
79
|
Log.e(TAG, "VKID onFail: $fail")
|
|
80
|
+
pendingCodeVerifier = null
|
|
81
|
+
pendingState = null
|
|
82
|
+
authorizationCodeEmitted = false
|
|
52
83
|
emitAuthFail(fail.toString())
|
|
53
84
|
}
|
|
54
85
|
},
|
|
55
|
-
|
|
86
|
+
params,
|
|
56
87
|
)
|
|
57
88
|
}
|
|
58
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Параметры One Tap / UI при потоке authorization code: PKCE (code_challenge) и state,
|
|
92
|
+
* как в [startAuth] с [VKIDAuthParams].
|
|
93
|
+
*/
|
|
94
|
+
fun buildAuthorizationCodeUiParams(): VKIDAuthUiParams? {
|
|
95
|
+
if (!VkAuthConfig.useAuthorizationCodeFlow) {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
authorizationCodeEmitted = false
|
|
99
|
+
pendingCodeVerifier = null
|
|
100
|
+
pendingState = null
|
|
101
|
+
val verifier = Pkce.generateVerifier()
|
|
102
|
+
val challenge = Pkce.challengeS256(verifier)
|
|
103
|
+
val state = UUID.randomUUID().toString()
|
|
104
|
+
pendingCodeVerifier = verifier
|
|
105
|
+
pendingState = state
|
|
106
|
+
return VKIDAuthUiParams.Builder().apply {
|
|
107
|
+
codeChallenge = challenge
|
|
108
|
+
this.state = state
|
|
109
|
+
scopes = VkAuthConfig.scopes
|
|
110
|
+
}.build()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* [VKIDAuthUiParams] для One Tap: PKCE при authorization code, иначе только [scopes] (как в
|
|
115
|
+
* [документации One Tap](https://id.vk.com/about/business/go/docs/ru/vkid/latest/vk-id/connection/elements/onetap-button/onetap-android)).
|
|
116
|
+
*/
|
|
117
|
+
fun buildOneTapAuthUiParams(): VKIDAuthUiParams? {
|
|
118
|
+
if (VkAuthConfig.useAuthorizationCodeFlow) {
|
|
119
|
+
return buildAuthorizationCodeUiParams()
|
|
120
|
+
}
|
|
121
|
+
if (VkAuthConfig.scopes.isEmpty()) {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
return VKIDAuthUiParams.Builder().apply {
|
|
125
|
+
scopes = VkAuthConfig.scopes
|
|
126
|
+
}.build()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Общая обработка [AuthCodeData] для [startAuth] и One Tap. */
|
|
130
|
+
fun dispatchAuthCode(data: AuthCodeData, isCompletion: Boolean) {
|
|
131
|
+
Log.d(
|
|
132
|
+
TAG,
|
|
133
|
+
"VKID onAuthCode: isCompletion=$isCompletion code=${data.code} deviceId=${data.deviceId}",
|
|
134
|
+
)
|
|
135
|
+
if (!VkAuthConfig.useAuthorizationCodeFlow) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
// SDK может сначала вызвать с isCompletion=false (промежуточный код), затем с true — обмен на бэкенде нужен только по финальному.
|
|
139
|
+
if (!isCompletion) {
|
|
140
|
+
Log.d(TAG, "onAuthCode: skip non-final code, wait for isCompletion=true")
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
if (authorizationCodeEmitted) {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
val verifier = pendingCodeVerifier
|
|
147
|
+
val state = pendingState
|
|
148
|
+
if (verifier == null || state == null) {
|
|
149
|
+
Log.e(TAG, "onAuthCode: missing PKCE state")
|
|
150
|
+
emitAuthFail("PKCE state lost")
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
emitAuthAuthorizationCode(
|
|
154
|
+
code = data.code,
|
|
155
|
+
deviceId = data.deviceId,
|
|
156
|
+
state = state,
|
|
157
|
+
codeVerifier = verifier,
|
|
158
|
+
isCompletion = isCompletion,
|
|
159
|
+
)
|
|
160
|
+
authorizationCodeEmitted = true
|
|
161
|
+
pendingCodeVerifier = null
|
|
162
|
+
pendingState = null
|
|
163
|
+
}
|
|
164
|
+
|
|
59
165
|
fun closeAuth() {
|
|
60
166
|
// VK ID SDK не требует явного закрытия веб-view из нативного модуля.
|
|
61
167
|
}
|
|
@@ -82,7 +188,6 @@ class AuthDelegate(private val reactContext: ReactApplicationContext) {
|
|
|
82
188
|
}
|
|
83
189
|
|
|
84
190
|
fun accessTokenChangedSuccess(token: String, userId: Int) {
|
|
85
|
-
// Старый поток silent token; оставлено для совместимости с JS до миграции приложения.
|
|
86
191
|
Log.d(TAG, "accessTokenChangedSuccess (legacy): userId=$userId")
|
|
87
192
|
}
|
|
88
193
|
|
|
@@ -110,7 +215,28 @@ class AuthDelegate(private val reactContext: ReactApplicationContext) {
|
|
|
110
215
|
promise.resolve(VkAuthPayload.profileForJs(token.userID, token.userData))
|
|
111
216
|
}
|
|
112
217
|
|
|
218
|
+
private fun emitAuthAuthorizationCode(
|
|
219
|
+
code: String,
|
|
220
|
+
deviceId: String,
|
|
221
|
+
state: String,
|
|
222
|
+
codeVerifier: String,
|
|
223
|
+
isCompletion: Boolean,
|
|
224
|
+
) {
|
|
225
|
+
val map = VkAuthPayload.fromAuthorizationCode(
|
|
226
|
+
code = code,
|
|
227
|
+
deviceId = deviceId,
|
|
228
|
+
state = state,
|
|
229
|
+
codeVerifier = codeVerifier,
|
|
230
|
+
isCompletion = isCompletion,
|
|
231
|
+
)
|
|
232
|
+
sendEvent("onAuth", map)
|
|
233
|
+
}
|
|
234
|
+
|
|
113
235
|
fun emitAuthSuccess(accessToken: com.vk.id.AccessToken) {
|
|
236
|
+
if (VkAuthConfig.useAuthorizationCodeFlow && authorizationCodeEmitted) {
|
|
237
|
+
Log.d(TAG, "emitAuthSuccess: skipped, authorization code already emitted")
|
|
238
|
+
return
|
|
239
|
+
}
|
|
114
240
|
val map = VkAuthPayload.fromAccessToken(accessToken)
|
|
115
241
|
map.putMap("profile", VkAuthPayload.profileForJs(accessToken.userID, accessToken.userData))
|
|
116
242
|
sendEvent("onAuth", map)
|
|
@@ -3,14 +3,19 @@ package com.vkauth.vkid
|
|
|
3
3
|
import android.util.Log
|
|
4
4
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
5
|
import com.vkauth.vkid.jsinput.App
|
|
6
|
+
import com.vkauth.vkid.jsinput.AuthFlow
|
|
6
7
|
import com.vkauth.vkid.jsinput.VKID
|
|
7
8
|
|
|
8
9
|
class InitDelegate(
|
|
9
10
|
@Suppress("unused") private val context: ReactApplicationContext,
|
|
10
11
|
) {
|
|
11
12
|
fun initialize(app: App, vkid: VKID) {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
VkAuthConfig.useAuthorizationCodeFlow = app.authFlow == AuthFlow.AUTHORIZATION_CODE
|
|
14
|
+
VkAuthConfig.scopes = app.scopes
|
|
15
|
+
Log.d(
|
|
16
|
+
TAG,
|
|
17
|
+
"initialize mode=${app.mode} appName=${vkid.appName} authFlow=${app.authFlow} scopes=${app.scopes.size}",
|
|
18
|
+
)
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
private companion object {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
package com.vkauth.vkid
|
|
2
|
+
|
|
3
|
+
import android.util.Base64
|
|
4
|
+
import java.security.MessageDigest
|
|
5
|
+
import java.security.SecureRandom
|
|
6
|
+
|
|
7
|
+
/** RFC 7636 PKCE для обмена кода на бэкенде (см. VKIDAuthParams.codeChallenge). */
|
|
8
|
+
internal object Pkce {
|
|
9
|
+
fun generateVerifier(): String {
|
|
10
|
+
val random = SecureRandom()
|
|
11
|
+
val bytes = ByteArray(32)
|
|
12
|
+
random.nextBytes(bytes)
|
|
13
|
+
return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fun challengeS256(verifier: String): String {
|
|
17
|
+
val digest =
|
|
18
|
+
MessageDigest.getInstance("SHA-256").digest(verifier.toByteArray(Charsets.US_ASCII))
|
|
19
|
+
return Base64.encodeToString(digest, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -11,6 +11,23 @@ import com.vk.id.VKIDUser
|
|
|
11
11
|
*/
|
|
12
12
|
internal object VkAuthPayload {
|
|
13
13
|
|
|
14
|
+
fun fromAuthorizationCode(
|
|
15
|
+
code: String,
|
|
16
|
+
deviceId: String,
|
|
17
|
+
state: String,
|
|
18
|
+
codeVerifier: String,
|
|
19
|
+
isCompletion: Boolean,
|
|
20
|
+
): WritableMap {
|
|
21
|
+
val map = Arguments.createMap()
|
|
22
|
+
map.putString("type", "authorization_code")
|
|
23
|
+
map.putString("code", code)
|
|
24
|
+
map.putString("deviceId", deviceId)
|
|
25
|
+
map.putString("state", state)
|
|
26
|
+
map.putString("codeVerifier", codeVerifier)
|
|
27
|
+
map.putBoolean("isCompletion", isCompletion)
|
|
28
|
+
return map
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
fun fromAccessToken(token: AccessToken): WritableMap {
|
|
15
32
|
val map = Arguments.createMap()
|
|
16
33
|
map.putString("type", "authorized")
|
|
@@ -1,17 +1,48 @@
|
|
|
1
1
|
package com.vkauth.vkid.jsinput
|
|
2
2
|
|
|
3
|
+
import com.facebook.react.bridge.ReadableArray
|
|
3
4
|
import com.facebook.react.bridge.ReadableMap
|
|
4
5
|
|
|
6
|
+
enum class AuthFlow {
|
|
7
|
+
/** Токен на устройстве (по умолчанию). */
|
|
8
|
+
ACCESS_TOKEN,
|
|
9
|
+
|
|
10
|
+
/** PKCE + код → обмен на бэкенде; см. VKIDAuthParams.codeChallenge. */
|
|
11
|
+
AUTHORIZATION_CODE,
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
data class App(
|
|
6
15
|
val credentials: Credentials,
|
|
7
|
-
val mode: Mode
|
|
16
|
+
val mode: Mode,
|
|
17
|
+
val authFlow: AuthFlow = AuthFlow.ACCESS_TOKEN,
|
|
18
|
+
val scopes: Set<String> = emptySet(),
|
|
8
19
|
) {
|
|
9
20
|
companion object {
|
|
10
21
|
fun fromMap(map: ReadableMap): App {
|
|
22
|
+
val flowStr = if (map.hasKey("authFlow")) map.getString("authFlow") else null
|
|
23
|
+
val authFlow =
|
|
24
|
+
when (flowStr) {
|
|
25
|
+
"authorizationCode" -> AuthFlow.AUTHORIZATION_CODE
|
|
26
|
+
else -> AuthFlow.ACCESS_TOKEN
|
|
27
|
+
}
|
|
11
28
|
return App(
|
|
12
29
|
credentials = Credentials.fromMap(map.getMap("credentials")!!),
|
|
13
|
-
mode = Mode.fromString(map.getString("mode")!!)
|
|
30
|
+
mode = Mode.fromString(map.getString("mode")!!),
|
|
31
|
+
authFlow = authFlow,
|
|
32
|
+
scopes = parseScopes(map),
|
|
14
33
|
)
|
|
15
34
|
}
|
|
35
|
+
|
|
36
|
+
private fun parseScopes(map: ReadableMap): Set<String> {
|
|
37
|
+
if (!map.hasKey("scopes")) {
|
|
38
|
+
return emptySet()
|
|
39
|
+
}
|
|
40
|
+
val arr: ReadableArray = map.getArray("scopes") ?: return emptySet()
|
|
41
|
+
val out = LinkedHashSet<String>()
|
|
42
|
+
for (i in 0 until arr.size()) {
|
|
43
|
+
arr.getString(i)?.trim()?.takeIf { it.isNotEmpty() }?.let { out.add(it) }
|
|
44
|
+
}
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
16
47
|
}
|
|
17
48
|
}
|
|
@@ -7,6 +7,7 @@ import com.facebook.react.uimanager.SimpleViewManager
|
|
|
7
7
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
8
8
|
import com.facebook.react.uimanager.annotations.ReactProp
|
|
9
9
|
import com.vk.id.onetap.xml.OneTap
|
|
10
|
+
import com.vkauth.vkid.VkAuthConfig
|
|
10
11
|
import com.vkauth.vkid.VkAuthServiceHolder
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -18,13 +19,19 @@ class OneTabButtonManager : SimpleViewManager<OneTap>() {
|
|
|
18
19
|
|
|
19
20
|
override fun createViewInstance(context: ThemedReactContext): OneTap {
|
|
20
21
|
val view = OneTap(context)
|
|
22
|
+
val delegate = VkAuthServiceHolder.authDelegate
|
|
23
|
+
delegate?.buildOneTapAuthUiParams()?.let { view.authParams = it }
|
|
21
24
|
view.setCallbacks(
|
|
22
25
|
onAuth = { _, accessToken ->
|
|
23
|
-
|
|
26
|
+
delegate?.emitAuthSuccess(accessToken)
|
|
24
27
|
},
|
|
25
28
|
onFail = { _, fail ->
|
|
26
29
|
Log.e(TAG, "OneTap onFail: $fail")
|
|
27
|
-
|
|
30
|
+
delegate?.emitAuthFail(fail.toString())
|
|
31
|
+
delegate?.buildOneTapAuthUiParams()?.let { view.authParams = it }
|
|
32
|
+
},
|
|
33
|
+
onAuthCode = { data, isCompletion ->
|
|
34
|
+
delegate?.dispatchAuthCode(data, isCompletion)
|
|
28
35
|
},
|
|
29
36
|
)
|
|
30
37
|
return view
|
package/ios/VkAuth.swift
CHANGED
|
@@ -23,12 +23,14 @@ fileprivate func vkAuthTopViewController() -> UIViewController? {
|
|
|
23
23
|
return top
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
///
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
/// Обмен кода на бэкенде: параметры уходят в JS; `finishFlow` завершает UI SDK (см. `AuthCodeHandler`).
|
|
27
|
+
private final class RnAuthorizationCodeHandler: NSObject, AuthCodeHandler {
|
|
28
|
+
weak var emitter: VkAuth?
|
|
29
|
+
|
|
30
|
+
func exchange(_ code: AuthorizationCode, finishFlow: @escaping () -> Void) {
|
|
31
|
+
emitter?.sendAuthorizationCodePayload(code)
|
|
32
|
+
finishFlow()
|
|
33
|
+
}
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/// React Native мост для VK ID SDK. См. https://id.vk.com/about/business/go/docs/ru/vkid/latest/vk-id/connection/migration/ios/migration-ios
|
|
@@ -36,6 +38,10 @@ fileprivate func vkAuthMakeConfiguration() -> AuthConfiguration {
|
|
|
36
38
|
final class VkAuth: RCTEventEmitter {
|
|
37
39
|
fileprivate static weak var _sharedEmitter: VkAuth?
|
|
38
40
|
|
|
41
|
+
private let authorizationCodeHandler = RnAuthorizationCodeHandler()
|
|
42
|
+
private var useAuthorizationCodeFlow = false
|
|
43
|
+
private var requestedScopes: [String] = []
|
|
44
|
+
|
|
39
45
|
@objc(initialize:vkid:)
|
|
40
46
|
func initialize(_ app: NSDictionary, vkid: NSDictionary) {
|
|
41
47
|
guard
|
|
@@ -47,6 +53,20 @@ final class VkAuth: RCTEventEmitter {
|
|
|
47
53
|
return
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
if let flow = app["authFlow"] as? String, flow == "authorizationCode" {
|
|
57
|
+
useAuthorizationCodeFlow = true
|
|
58
|
+
} else {
|
|
59
|
+
useAuthorizationCodeFlow = false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if let scopes = app["scopes"] as? [String] {
|
|
63
|
+
requestedScopes = scopes
|
|
64
|
+
} else if let scopes = app["scopes"] as? [Any] {
|
|
65
|
+
requestedScopes = scopes.compactMap { $0 as? String }
|
|
66
|
+
} else {
|
|
67
|
+
requestedScopes = []
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
do {
|
|
51
71
|
try VKID.shared.set(
|
|
52
72
|
config: Configuration(
|
|
@@ -57,6 +77,7 @@ final class VkAuth: RCTEventEmitter {
|
|
|
57
77
|
)
|
|
58
78
|
)
|
|
59
79
|
VkAuth._sharedEmitter = self
|
|
80
|
+
authorizationCodeHandler.emitter = self
|
|
60
81
|
VKID.shared.add(observer: self)
|
|
61
82
|
} catch {
|
|
62
83
|
os_log("VKID initialization failed: %{public}@", type: .error, error.localizedDescription)
|
|
@@ -69,13 +90,48 @@ final class VkAuth: RCTEventEmitter {
|
|
|
69
90
|
_ = VKID.shared.open(url: url)
|
|
70
91
|
}
|
|
71
92
|
|
|
93
|
+
fileprivate func makeScope() -> Scope {
|
|
94
|
+
guard !requestedScopes.isEmpty else {
|
|
95
|
+
return Scope([])
|
|
96
|
+
}
|
|
97
|
+
return Scope(requestedScopes)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fileprivate func makeAuthConfiguration() -> AuthConfiguration {
|
|
101
|
+
if useAuthorizationCodeFlow {
|
|
102
|
+
return AuthConfiguration(
|
|
103
|
+
flow: .confidentialClientFlow(
|
|
104
|
+
codeExchanger: authorizationCodeHandler,
|
|
105
|
+
pkce: nil
|
|
106
|
+
),
|
|
107
|
+
scope: makeScope()
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
return AuthConfiguration(
|
|
111
|
+
flow: .publicClientFlow(),
|
|
112
|
+
scope: makeScope()
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fileprivate func sendAuthorizationCodePayload(_ code: AuthorizationCode) {
|
|
117
|
+
let body: [String: Any] = [
|
|
118
|
+
"type": "authorization_code",
|
|
119
|
+
"code": code.code,
|
|
120
|
+
"deviceId": code.deviceId,
|
|
121
|
+
"state": code.state,
|
|
122
|
+
"codeVerifier": code.codeVerifier ?? "",
|
|
123
|
+
"isCompletion": true,
|
|
124
|
+
]
|
|
125
|
+
sendEvent(withName: "onAuth", body: body)
|
|
126
|
+
}
|
|
127
|
+
|
|
72
128
|
@objc func startAuth() {
|
|
73
129
|
guard let presenter = vkAuthTopViewController() else {
|
|
74
130
|
os_log("No presenter for VKID authorize", type: .error)
|
|
75
131
|
return
|
|
76
132
|
}
|
|
77
133
|
VKID.shared.authorize(
|
|
78
|
-
with:
|
|
134
|
+
with: makeAuthConfiguration(),
|
|
79
135
|
using: .uiViewController(presenter)
|
|
80
136
|
) { _ in }
|
|
81
137
|
}
|
|
@@ -143,13 +199,16 @@ final class VkAuth: RCTEventEmitter {
|
|
|
143
199
|
|
|
144
200
|
extension VkAuth: VKIDObserver {
|
|
145
201
|
func vkid(_ vkid: VKID, didCompleteAuthWith result: AuthResult, in oAuth: OAuthProvider) {
|
|
146
|
-
|
|
147
|
-
|
|
202
|
+
switch result {
|
|
203
|
+
case .success(let session):
|
|
148
204
|
send(event: .onAuth(userSession: session))
|
|
149
|
-
|
|
205
|
+
case .failure(AuthError.authCodeExchangedOnYourBackend):
|
|
206
|
+
// Код уже отправлен в JS из RnAuthorizationCodeHandler.exchange
|
|
207
|
+
break
|
|
208
|
+
case .failure(AuthError.cancelled):
|
|
150
209
|
os_log("VKID auth cancelled", type: .info)
|
|
151
|
-
|
|
152
|
-
os_log("VKID auth failed: %{public}@", type: .error, error
|
|
210
|
+
case .failure(let error):
|
|
211
|
+
os_log("VKID auth failed: %{public}@", type: .error, String(describing: error))
|
|
153
212
|
}
|
|
154
213
|
}
|
|
155
214
|
|
|
@@ -177,13 +236,17 @@ final class OneTapButtonManager: RCTViewManager {
|
|
|
177
236
|
return UIView()
|
|
178
237
|
}
|
|
179
238
|
|
|
239
|
+
guard let emitter = VkAuth._sharedEmitter else {
|
|
240
|
+
return UIView()
|
|
241
|
+
}
|
|
242
|
+
|
|
180
243
|
let button = OneTapButton(
|
|
181
244
|
layout: .regular(
|
|
182
245
|
height: .medium(.h44),
|
|
183
246
|
cornerRadius: 8
|
|
184
247
|
),
|
|
185
248
|
presenter: .uiViewController(root),
|
|
186
|
-
authConfiguration:
|
|
249
|
+
authConfiguration: emitter.makeAuthConfiguration(),
|
|
187
250
|
onCompleteAuth: nil
|
|
188
251
|
)
|
|
189
252
|
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -63,9 +63,19 @@ export class VK {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export namespace VK {
|
|
66
|
+
/**
|
|
67
|
+
* `authorizationCode` — PKCE на клиенте, в JS придёт `code` + `codeVerifier` + `state` + `deviceId`
|
|
68
|
+
* (Android: VKIDAuthParams.codeChallenge; iOS: confidential client flow + AuthCodeHandler).
|
|
69
|
+
* По умолчанию — готовый access token на устройстве.
|
|
70
|
+
*/
|
|
71
|
+
export type AuthFlowMode = 'accessToken' | 'authorizationCode';
|
|
72
|
+
|
|
66
73
|
export interface App {
|
|
67
74
|
mode: Mode;
|
|
68
75
|
credentials: Credentials;
|
|
76
|
+
authFlow?: AuthFlowMode;
|
|
77
|
+
/** Запрашиваемые доступы, например `phone`, `email` (включите их в кабинете VK ID для приложения). */
|
|
78
|
+
scopes?: string[];
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
export enum Mode {
|
|
@@ -90,6 +100,28 @@ function parseOnAuthPayload(
|
|
|
90
100
|
};
|
|
91
101
|
}
|
|
92
102
|
|
|
103
|
+
if (raw.type === 'authorization_code') {
|
|
104
|
+
if (
|
|
105
|
+
typeof raw.code === 'string' &&
|
|
106
|
+
typeof raw.codeVerifier === 'string' &&
|
|
107
|
+
typeof raw.state === 'string'
|
|
108
|
+
) {
|
|
109
|
+
return {
|
|
110
|
+
kind: 'ok',
|
|
111
|
+
payload: {
|
|
112
|
+
authorizationCode: {
|
|
113
|
+
code: raw.code,
|
|
114
|
+
codeVerifier: raw.codeVerifier,
|
|
115
|
+
state: raw.state,
|
|
116
|
+
deviceId: typeof raw.deviceId === 'string' ? raw.deviceId : '',
|
|
117
|
+
isCompletion: Boolean(raw.isCompletion),
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return { kind: 'error', message: 'Invalid authorization_code payload' };
|
|
123
|
+
}
|
|
124
|
+
|
|
93
125
|
if (raw.type === 'authorized') {
|
|
94
126
|
const vkid = raw.vkid as Record<string, unknown> | undefined;
|
|
95
127
|
if (vkid && typeof vkid.accessToken === 'string') {
|
|
@@ -240,11 +272,19 @@ export namespace VKID {
|
|
|
240
272
|
|
|
241
273
|
/** Успешная авторизация VK ID (OAuth 2.1): access token и id пользователя VK. */
|
|
242
274
|
export interface AuthSuccessPayload {
|
|
243
|
-
accessToken
|
|
244
|
-
userId
|
|
275
|
+
accessToken?: string;
|
|
276
|
+
userId?: string;
|
|
245
277
|
profile?: UserProfile;
|
|
246
278
|
/** Android: полный ответ VK ID SDK (отладка). */
|
|
247
279
|
vkidNative?: Record<string, unknown>;
|
|
280
|
+
/** Поток обмена кода на бэкенде (`authFlow: 'authorizationCode'`). */
|
|
281
|
+
authorizationCode?: {
|
|
282
|
+
code: string;
|
|
283
|
+
codeVerifier: string;
|
|
284
|
+
state: string;
|
|
285
|
+
deviceId: string;
|
|
286
|
+
isCompletion: boolean;
|
|
287
|
+
};
|
|
248
288
|
}
|
|
249
289
|
|
|
250
290
|
export interface AuthChangedCallback {
|