@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 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
- Log.d(TAG, "VKID onAuthCode: isCompletion=$isCompletion code=${data.code} deviceId=${data.deviceId}")
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
- VKIDAuthParams.Builder().build(),
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
- // Конфигурация клиента — через manifest placeholders (VKIDClientID и т.д.) и VKID.init в Application.
13
- Log.d(TAG, "initialize mode=${app.mode} appName=${vkid.appName}")
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
+ }
@@ -0,0 +1,10 @@
1
+ package com.vkauth.vkid
2
+
3
+ /** Задаётся в [InitDelegate] из JS (`authFlow`, `scopes`). */
4
+ object VkAuthConfig {
5
+ @JvmField
6
+ var useAuthorizationCodeFlow: Boolean = false
7
+
8
+ @JvmField
9
+ var scopes: Set<String> = emptySet()
10
+ }
@@ -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
- VkAuthServiceHolder.authDelegate?.emitAuthSuccess(accessToken)
26
+ delegate?.emitAuthSuccess(accessToken)
24
27
  },
25
28
  onFail = { _, fail ->
26
29
  Log.e(TAG, "OneTap onFail: $fail")
27
- VkAuthServiceHolder.authDelegate?.emitAuthFail(fail.toString())
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
- /// Публичный клиент + PKCE внутри SDK. Для обмена кода на бэкенде позже можно сменить на `confidentialClientFlow`.
27
- fileprivate func vkAuthMakeConfiguration() -> AuthConfiguration {
28
- AuthConfiguration(
29
- flow: .publicClientFlow(),
30
- scope: Scope([])
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: vkAuthMakeConfiguration(),
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
- do {
147
- let session = try result.get()
202
+ switch result {
203
+ case .success(let session):
148
204
  send(event: .onAuth(userSession: session))
149
- } catch AuthError.cancelled {
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
- } catch {
152
- os_log("VKID auth failed: %{public}@", type: .error, error.localizedDescription)
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: vkAuthMakeConfiguration(),
249
+ authConfiguration: emitter.makeAuthConfiguration(),
187
250
  onCompleteAuth: nil
188
251
  )
189
252
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyrocancode/react-native-vk-auth",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Тонкая обёртка над VK ID SDK для React Native: OAuth 2.1, нативные модули iOS и Android.",
5
5
  "main": "src/index.tsx",
6
6
  "module": "src/index.tsx",
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: string;
244
- userId: string;
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 {