yandex_login_sdk 1.0.1
yandex_login_sdk: ^1.0.1 copied to clipboard
Native Yandex LoginSDK wrapper for Flutter — SSO via installed Yandex apps with browser fallback (iOS + Android).
yandex_login_sdk #
A Flutter plugin for Yandex sign-in on Android, iOS and Web. On
mobile it wraps the official Yandex LoginSDK — native single sign-on
through installed Yandex apps (Browser, Mail, Старт, …) with automatic
fallback to a Chrome Custom Tab, WebView, or ASWebAuthenticationSession. On
web it runs the OAuth 2.0 Authorization Code + PKCE flow in a popup.
- ✅ Native SSO via installed Yandex apps (Android / iOS)
- ✅ Automatic browser fallback when no Yandex app is present
- ✅ Web support — code + PKCE popup flow, token never appears in a URL
- ✅ Optional
webOnlystrategy to skip the installed apps entirely - ✅ Cancellation surfaced as a typed exception
- ✅ Returns the OAuth
access_token(and JWT on iOS,expires_inon Android/Web) - ✅ Fetch the user profile (
getUserInfo) and a cross-platform JWT (getJwt) — pure Dart - ✅
signOut()to clear local sign-in state - ✅ 100% Dart test coverage, every commit verified by CI
Status: v1.0.1. Android tested in production; iOS code complete but not yet field-tested by the maintainer (no Apple Developer account at the time of release). Reports/PRs welcome.
Setup #
1. Register an OAuth app in Yandex #
Create or open your app at oauth.yandex.ru, enable the mobile platform, and provide both bundle identifiers:
- iOS Bundle ID
- Android Package name
You'll get a client_id (32-char hex string) — used in every step below.
2. Add the dependency #
dependencies:
yandex_login_sdk: ^1.0.0
3. Android setup #
android/app/build.gradle.kts — add manifest placeholders (required for
the native SDK to initialize; the clientId you pass to signIn overrides
the placeholder value at runtime):
android {
defaultConfig {
manifestPlaceholders["YANDEX_CLIENT_ID"] = "<your_client_id>"
manifestPlaceholders["YANDEX_OAUTH_HOST"] = "oauth.yandex.ru"
}
}
android/app/src/main/kotlin/.../MainActivity.kt — switch to
FlutterFragmentActivity (required for ActivityResultLauncher):
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterFragmentActivity()
That's it on Android — the plugin's manifest contributes the package visibility queries for known Yandex apps.
4. iOS setup #
ios/Runner/Info.plist — add the URL scheme returned by the SDK and
declare the schemes it queries:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key><string>Editor</string>
<key>CFBundleURLName</key><string>YandexLoginSDK</string>
<key>CFBundleURLSchemes</key>
<array>
<string>yx0123456789abcdef0123456789abcdef</string>
<!-- "yx" + your client_id, no angle brackets -->
</array>
</dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>primaryyandexloginsdk</string>
<string>secondaryyandexloginsdk</string>
</array>
ios/Runner/SceneDelegate.swift — forward URL callbacks (only required
for projects using the modern scene-based lifecycle, which is the default
for Flutter 3+):
import Flutter
import UIKit
import yandex_login_sdk
class SceneDelegate: FlutterSceneDelegate {
override func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
var handled = false
for ctx in URLContexts {
if YandexLoginSdkPlugin.handle(openURL: ctx.url) { handled = true }
}
if !handled {
super.scene(scene, openURLContexts: URLContexts)
}
}
}
AppDelegate.swift needs no changes — the plugin registers itself as a
FlutterApplicationLifeCycleDelegate and intercepts AppDelegate callbacks
automatically (used as a backup for non-scene-based apps).
Minimum iOS deployment target: 13.0.
5. Web setup #
The web flow is plain OAuth (Authorization Code + PKCE) — no Yandex JS SDK, no client secret.
-
At oauth.yandex.ru enable the Web services platform for your app and register the Redirect URI:
https://your.app/yandex_auth_callback.html(plushttp://localhost:5000/yandex_auth_callback.htmlfor development — run with a fixed port:flutter run -d chrome --web-port 5000). -
Add
web/yandex_auth_callback.htmlto your app:
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Yandex auth callback</title></head>
<body>
<script>
(function () {
if (window.opener) {
window.opener.postMessage(
'yandex_login_sdk:' + window.location.search + window.location.hash,
window.location.origin
);
}
window.close();
})();
</script>
</body>
</html>
That's it — signIn() opens the Yandex consent popup and resolves with the
token; getUserInfo / getJwt work in the browser out of the box
(login.yandex.ru serves CORS headers). Call signIn() from a user gesture
(button tap), otherwise the browser blocks the popup (code POPUP_BLOCKED).
If the app is deployed under a sub-path, set
YandexLoginSdkWeb.redirectUriOverride before signing in.
Usage #
import 'package:yandex_login_sdk/yandex_login_sdk.dart';
Future<void> signIn() async {
try {
final result = await YandexLoginSdk.signIn(
clientId: 'your_yandex_oauth_client_id',
// strategy: YandexLoginStrategy.webOnly, // skip installed Yandex apps
);
print('Access token: ${result.token}');
print('JWT (iOS only): ${result.jwt}');
print('Expires at (Android only): ${result.expiresAt}');
} on YandexAuthCancelledException {
// User dismissed the sheet — no need to show an error.
} on YandexAuthInProgressException {
// A sign-in is already running — ignore the extra tap.
} on YandexAuthUnsupportedException {
// Web/desktop or unsupported — fall back to your own WebView.
} on YandexAuthException catch (e) {
print('Yandex SDK error: $e');
}
}
Login strategy #
signIn accepts an optional strategy:
| Value | Android | iOS | Web |
|---|---|---|---|
YandexLoginStrategy.auto (default) |
NATIVE → CHROME_TAB → WEBVIEW |
Yandex apps → web session | popup (ignored) |
YandexLoginStrategy.webOnly |
CHROME_TAB → WEBVIEW |
ASWebAuthenticationSession only |
popup (ignored) |
There is deliberately no nativeOnly: neither native SDK can require the
app-only flow — both fall back to the browser when no Yandex app is
installed. On web there is only the popup flow, so the strategy is ignored.
Fetching the user profile #
getUserInfo is a pure-Dart call to login.yandex.ru/info — it behaves the
same on every platform and needs no native support. Which fields are populated
depends on the permissions your OAuth app was granted (see
Scopes).
final result = await YandexLoginSdk.signIn(clientId: clientId);
final user = await YandexLoginSdk.getUserInfo(token: result.token);
print(user.displayName); // e.g. "Vasya"
print(user.defaultEmail); // requires login:email
print(user.avatarUrl()); // requires login:avatar; null when no avatar
Cross-platform JWT #
YandexLoginResult.jwt is only populated natively on iOS. For a JWT that is
identical on both platforms, call getJwt — it fetches the signed JWT from
login.yandex.ru/info?format=jwt (the same endpoint the Android SDK uses
internally):
final jwt = await YandexLoginSdk.getJwt(token: result.token);
Both getUserInfo and getJwt throw YandexAuthInvalidTokenException on an
expired or revoked token (HTTP 401), and both accept an optional
timeout: Duration(...) that bounds the whole request (code TIMEOUT on
expiry).
Signing out #
await YandexLoginSdk.signOut();
- iOS — calls the native
YandexLoginSDK.logout(), clearing the cached token/JWT, PKCE verifier and CSRF state from the Keychain. This forces the nextsignInto present interactive UI (and lets the user switch accounts). - Android — the
authsdkis stateless and has no logout, so this is a documented no-op. - Web — the plugin holds no session state, so this is a documented no-op too (the Yandex cookie session in the browser is not touched).
On both platforms it is local only: it does not revoke the token on Yandex's servers, nor clear the Yandex-app / browser cookie session. Drop your own copy of the token afterwards.
Scopes (permissions) #
Yandex fixes OAuth permissions when you register your app at
oauth.yandex.ru. The native Yandex SDKs (3.x) do
not support requesting scopes at runtime, so this plugin has no scopes
argument. The YandexScope constants are provided for reference — they
document which YandexUserInfo fields each permission unlocks:
| Constant | Scope | Unlocks |
|---|---|---|
YandexScope.loginInfo |
login:info |
displayName, realName, firstName, lastName, sex |
YandexScope.loginEmail |
login:email |
defaultEmail, emails |
YandexScope.loginAvatar |
login:avatar |
defaultAvatarId / avatarUrl() |
YandexScope.loginBirthday |
login:birthday |
birthday |
YandexScope.loginDefaultPhone |
login:default_phone |
defaultPhone |
Logging #
The plugin emits diagnostic events through an opt-in callback — disabled by
default, no print calls in release builds. While a handler is installed the
native Kotlin/Swift layers mirror their own events into the same hook
(prefixed with [native]), and on Android the underlying authsdk's logcat
output is enabled too. Wire it up to your logger of choice:
import 'package:yandex_login_sdk/yandex_login_sdk.dart';
YandexLoginSdk.onLog = (level, message, {error, stackTrace}) {
switch (level) {
case YandexLogLevel.error:
// forward to Sentry, Crashlytics, etc.
mySentry.captureException(error, stackTrace: stackTrace, hint: message);
case YandexLogLevel.warning:
case YandexLogLevel.info:
case YandexLogLevel.debug:
myLogger.log(level.name, message);
}
};
What you'll see during a normal flow:
| Level | Message |
|---|---|
info |
signIn() invoked |
debug |
Invoking native signIn (clientId length=N) |
debug |
Native signIn returned token (length=N) |
info |
signIn() succeeded |
On cancel: info: signIn() cancelled by user. On unsupported platform:
warning: signIn() unsupported on this platform. On any other error:
error: signIn() failed: <code> <message> with error and stackTrace
populated.
API #
YandexLoginSdk.signIn({required String clientId, YandexLoginStrategy strategy = .auto}) → Future<YandexLoginResult> #
Triggers the authorization flow. clientId is used on every platform: on
Android it overrides the manifest placeholder at runtime (authsdk 3.2+), on
iOS it (re-)activates the SDK, on web it goes into the OAuth URL. See
Login strategy for strategy.
Web-specific error codes: POPUP_BLOCKED (call from a user gesture) and
STATE_MISMATCH (CSRF-mismatched callback discarded).
YandexLoginResult #
| Field | Type | Notes |
|---|---|---|
token |
String |
OAuth 2.0 access token |
jwt |
String? |
Native JWT — iOS only. For every platform use getJwt. |
expiresIn |
int? |
Relative TTL in seconds from issuance — Android and Web (null on iOS) |
issuedAt |
DateTime? |
When the sign-in response arrived on the Dart side |
expiresAt |
DateTime? (getter) |
issuedAt + expiresIn; null on iOS |
YandexLoginSdk.getUserInfo({required String token, http.Client? httpClient, Duration? timeout}) → Future<YandexUserInfo> #
Pure-Dart GET login.yandex.ru/info. Throws YandexAuthInvalidTokenException
on HTTP 401 and YandexAuthException (codes HTTP_<status>, BAD_RESPONSE,
TIMEOUT, CONNECTION_ERROR, BAD_ARGS) otherwise.
YandexLoginSdk.getJwt({required String token, String? jwtSecret, http.Client? httpClient, Duration? timeout}) → Future<String> #
Pure-Dart GET login.yandex.ru/info?format=jwt. Returns the raw signed JWT.
jwtSecret only changes the HMAC signing key — avoid shipping a real
client_secret in the app.
YandexLoginSdk.signOut() → Future<void> #
Clears local sign-in state. Real logout() on iOS; documented no-op on
Android. Local-only — no server-side revocation. See
Signing out.
YandexUserInfo #
| Field | Type | Notes |
|---|---|---|
id / login / clientId |
String |
always present |
displayName / realName / firstName / lastName / sex |
String? |
login:info |
defaultEmail |
String? |
login:email |
emails |
List<String> |
login:email |
defaultAvatarId + avatarUrl([size]) |
String? |
login:avatar |
birthday |
String? |
login:birthday, raw YYYY-MM-DD |
defaultPhone |
YandexPhone? |
login:default_phone |
psuid / oldSocialLogin |
String? |
|
raw |
Map<String, dynamic> |
full decoded body (forward-compat) |
YandexLoginSdk.onLog #
| Type | Notes |
|---|---|
YandexLogHandler? |
Optional callback (level, message, {error, stackTrace}). null = silent. See Logging above. |
Exceptions #
| Exception | When |
|---|---|
YandexAuthCancelledException |
User dismissed the auth sheet |
YandexAuthInProgressException |
signIn called while another sign-in is running |
YandexAuthUnsupportedException |
Plugin not available on this platform |
YandexAuthInvalidTokenException |
getUserInfo / getJwt got HTTP 401 (expired/revoked token) |
YandexAuthException |
Any other SDK / configuration / network error |
Testing #
The Dart layer is covered by 113 unit tests with 100 % line coverage —
every error branch in the method-channel implementation, the getUserInfo /
getJwt HTTP paths (success, 401, server error, timeout, transport failure,
malformed body), every exception type and YandexUserInfo.fromJson edge case
is exercised. Coverage is reported to
Coveralls on every
push and pull request.
Running tests locally #
flutter test # 113 tests, ~1 s
flutter test --coverage # writes coverage/lcov.info
genhtml coverage/lcov.info -o coverage/html && open coverage/html/index.html
CI #
Every push and every pull request runs:
dart format --set-exit-if-changed .— code style gateflutter analyze— static analysis must passflutter test --coverage— all tests must passflutter pub publish --dry-run— packaging must be valid- Build the example app for Android, iOS and Web to catch native regressions
- Kotlin unit tests of the Android plugin layer
What's not covered #
The native Kotlin and Swift layers are intentionally not measured — they do little more than forward calls to the official Yandex SDKs and require a real device + Yandex account to test meaningfully. Treat the example app as the manual smoke test for the native side.
Limitations / known issues #
expiresIn/expiresAtarenullon iOS. The iOSYandexLoginSDKdiscards the OAuthexpires_in; Android and Web populate both. (JWT parity is solved — usegetJwtfor an identical JWT everywhere.)- Desktop is not supported —
signInthrowsYandexAuthUnsupportedExceptionon macOS/Windows/Linux.getUserInfoandgetJwtstill work there with a token obtained elsewhere. - No runtime scopes. The native Yandex SDKs 3.x fix permissions at OAuth-app registration time; there is no per-login scope selection. See Scopes.
signOut()is local-only. It clears on-device state (iOS) or is a no-op (Android); it never revokes the token server-side or clears the cookie session.- No
nativeOnlystrategy. Neither native SDK can require the app-only flow — both fall back to the browser when no Yandex app is installed. See Login strategy.
License #
BSD-3-Clause. See LICENSE.
This plugin is a community wrapper. The bundled native code (Android and iOS) ships under Yandex's own license terms — see the Yandex LoginSDK iOS and Yandex LoginSDK Android repositories.