├── app-demo
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── values
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ └── strings.xml
│ │ │ ├── values-ru
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ └── ic_launcher.xml
│ │ │ ├── xml
│ │ │ │ ├── data_extraction_rules.xml
│ │ │ │ └── network_security_config.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ │ └── layout
│ │ │ │ └── activity_partner.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── ru
│ │ │ └── tinkoff
│ │ │ └── core
│ │ │ └── app_demo_partner
│ │ │ ├── PartnerPresenter.kt
│ │ │ └── PartnerActivity.kt
│ └── androidTest
│ │ └── java
│ │ └── ru
│ │ └── tinkoff
│ │ └── core
│ │ └── app_demo_partner
│ │ └── TinkoffIdSignInButtonTest.kt
├── proguard-rules.pro
└── build.gradle
├── tinkoff-id
├── .gitignore
├── gradle.properties
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── font
│ │ │ │ └── neue_haas_unica_w1g.ttf
│ │ │ ├── drawable-hdpi
│ │ │ │ ├── tinkoff_id_tinkoff_logo.png
│ │ │ │ ├── tinkoff_id_tinkoff_small_logo.png
│ │ │ │ └── tinkoff_id_tinkoff_small_logo_border.png
│ │ │ ├── drawable-mdpi
│ │ │ │ ├── tinkoff_id_tinkoff_logo.png
│ │ │ │ ├── tinkoff_id_tinkoff_small_logo.png
│ │ │ │ └── tinkoff_id_tinkoff_small_logo_border.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ ├── tinkoff_id_tinkoff_logo.png
│ │ │ │ ├── tinkoff_id_tinkoff_small_logo.png
│ │ │ │ └── tinkoff_id_tinkoff_small_logo_border.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ ├── tinkoff_id_tinkoff_logo.png
│ │ │ │ ├── tinkoff_id_tinkoff_small_logo.png
│ │ │ │ └── tinkoff_id_tinkoff_small_logo_border.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ │ ├── tinkoff_id_tinkoff_logo.png
│ │ │ │ ├── tinkoff_id_tinkoff_small_logo.png
│ │ │ │ └── tinkoff_id_tinkoff_small_logo_border.png
│ │ │ ├── values-night
│ │ │ │ └── colors.xml
│ │ │ ├── layout
│ │ │ │ └── tinkoff_id_web_view_activity.xml
│ │ │ ├── values-en
│ │ │ │ └── strings.xml
│ │ │ ├── drawable
│ │ │ │ ├── tinkoff_id_badge_background_gray_style.xml
│ │ │ │ ├── tinkoff_id_badge_background_black_style.xml
│ │ │ │ ├── tinkoff_id_badge_background_yellow_style.xml
│ │ │ │ ├── tinkoff_id_compact_background_black_style.xml
│ │ │ │ ├── tinkoff_id_compact_background_gray_style.xml
│ │ │ │ ├── tinkoff_id_compact_background_yellow_style.xml
│ │ │ │ ├── tinkoff_id_close.xml
│ │ │ │ └── tinkoff_id_reload_icon.xml
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── styles.xml
│ │ │ │ ├── attr.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── dimens.xml
│ │ │ └── menu
│ │ │ │ └── tinkoff_id_web_view_auth_menu.xml
│ │ ├── java
│ │ │ └── ru
│ │ │ │ └── tinkoff
│ │ │ │ └── core
│ │ │ │ └── tinkoffId
│ │ │ │ ├── ui
│ │ │ │ └── webView
│ │ │ │ │ ├── TinkoffWebViewUiData.kt
│ │ │ │ │ ├── TinkoffWebViewListener.kt
│ │ │ │ │ ├── TinkoffWebViewAuthPresenter.kt
│ │ │ │ │ ├── TinkoffWebViewClient.kt
│ │ │ │ │ └── TinkoffWebViewAuthActivity.kt
│ │ │ │ ├── TinkoffIdStatusCode.kt
│ │ │ │ ├── error
│ │ │ │ ├── TinkoffRequestException.kt
│ │ │ │ ├── TinkoffErrorMessage.kt
│ │ │ │ ├── TokenSignOutErrorConstants.kt
│ │ │ │ └── TinkoffTokenErrorConstants.kt
│ │ │ │ ├── TinkoffTokenPayload.kt
│ │ │ │ ├── codeVerifier
│ │ │ │ ├── CodeVerifierStore.kt
│ │ │ │ └── CodeVerifierUtil.kt
│ │ │ │ ├── TinkoffCall.kt
│ │ │ │ ├── TinkoffPartnerApiService.kt
│ │ │ │ ├── AppLinkUtil.kt
│ │ │ │ ├── api
│ │ │ │ └── TinkoffIdApi.kt
│ │ │ │ └── TinkoffIdAuth.kt
│ │ └── AndroidManifest.xml
│ └── test
│ │ └── java
│ │ └── ru
│ │ └── tinkoff
│ │ └── core
│ │ └── tinkoffId
│ │ ├── TinkoffIdSignInButtonTest.kt
│ │ ├── TinkoffIdApiTest.kt
│ │ ├── AppLinkUtilTest.kt
│ │ ├── TinkoffIdAuthTest.kt
│ │ └── TinkoffPartnerApiServiceTest.kt
└── build.gradle
├── .gitattributes
├── settings.gradle
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── plugin
│ ├── kotlin-library.gradle
│ ├── detekt.gradle
│ ├── android.gradle
│ └── publish-lib.gradle
└── versions.gradle
├── imgs
└── tinkoff_id_sign_in_button
│ ├── compact.png
│ └── standard.png
├── .idea
└── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── .gitignore
├── .github
└── workflows
│ ├── merge_request.yml
│ └── publish.yml
├── CHANGELOG.md
├── MIGRATION.md
├── gradle.properties
├── gradlew.bat
├── gradlew
├── README.md
└── LICENSE
/app-demo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/tinkoff-id/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | changelog.md merge=union
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app-demo', ':tinkoff-id'
2 | rootProject.name = "TinkoffID"
--------------------------------------------------------------------------------
/tinkoff-id/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=tinkoff-id
2 | POM_ARTIFACT_ID=tinkoff-id
3 | POM_PACKAGING=aar
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/imgs/tinkoff_id_sign_in_button/compact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/imgs/tinkoff_id_sign_in_button/compact.png
--------------------------------------------------------------------------------
/imgs/tinkoff_id_sign_in_button/standard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/imgs/tinkoff_id_sign_in_button/standard.png
--------------------------------------------------------------------------------
/app-demo/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app-demo/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app-demo/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app-demo/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app-demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/font/neue_haas_unica_w1g.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/font/neue_haas_unica_w1g.ttf
--------------------------------------------------------------------------------
/app-demo/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #E8ADAB
4 |
--------------------------------------------------------------------------------
/app-demo/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app-demo/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app-demo/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app-demo/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app-demo/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/app-demo/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-hdpi/tinkoff_id_tinkoff_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-hdpi/tinkoff_id_tinkoff_logo.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-mdpi/tinkoff_id_tinkoff_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-mdpi/tinkoff_id_tinkoff_logo.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-xhdpi/tinkoff_id_tinkoff_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xhdpi/tinkoff_id_tinkoff_logo.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-xxhdpi/tinkoff_id_tinkoff_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xxhdpi/tinkoff_id_tinkoff_logo.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-xxxhdpi/tinkoff_id_tinkoff_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xxxhdpi/tinkoff_id_tinkoff_logo.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-hdpi/tinkoff_id_tinkoff_small_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-hdpi/tinkoff_id_tinkoff_small_logo.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-mdpi/tinkoff_id_tinkoff_small_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-mdpi/tinkoff_id_tinkoff_small_logo.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-xhdpi/tinkoff_id_tinkoff_small_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xhdpi/tinkoff_id_tinkoff_small_logo.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-xxhdpi/tinkoff_id_tinkoff_small_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xxhdpi/tinkoff_id_tinkoff_small_logo.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-xxxhdpi/tinkoff_id_tinkoff_small_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xxxhdpi/tinkoff_id_tinkoff_small_logo.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-hdpi/tinkoff_id_tinkoff_small_logo_border.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-hdpi/tinkoff_id_tinkoff_small_logo_border.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-mdpi/tinkoff_id_tinkoff_small_logo_border.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-mdpi/tinkoff_id_tinkoff_small_logo_border.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-xhdpi/tinkoff_id_tinkoff_small_logo_border.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xhdpi/tinkoff_id_tinkoff_small_logo_border.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-xxhdpi/tinkoff_id_tinkoff_small_logo_border.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xxhdpi/tinkoff_id_tinkoff_small_logo_border.png
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable-xxxhdpi/tinkoff_id_tinkoff_small_logo_border.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinkoff-mobile-tech/TinkoffID-Android/HEAD/tinkoff-id/src/main/res/drawable-xxxhdpi/tinkoff_id_tinkoff_small_logo_border.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle/
2 | .idea
3 | !.idea/codeStyles
4 | build/
5 | .DS_Store
6 |
7 | /local.properties
8 | /gradle.properties
9 | installlocal.bat
10 |
11 | *.iml
12 | *.eml
13 | *.iws
14 |
15 | gradle/plugin/custom.gradle
16 |
--------------------------------------------------------------------------------
/.github/workflows/merge_request.yml:
--------------------------------------------------------------------------------
1 | name: merge request
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - 'main'
7 |
8 | jobs:
9 | check:
10 | uses: tinkoff-mobile-tech/workflows/.github/workflows/android_lib.merge_request.yml@v1
11 |
--------------------------------------------------------------------------------
/gradle/plugin/kotlin-library.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin-android'
2 | apply from: rootProject.file('gradle/plugin/detekt.gradle')
3 | apply plugin: 'kotlin-kapt'
4 |
5 | dependencies {
6 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
7 | }
8 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed May 04 12:08:39 MSK 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/app-demo/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Partner Auth
4 | Обновить токен
5 | Отозвать токен
6 |
7 |
--------------------------------------------------------------------------------
/app-demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 |
7 | jobs:
8 | publish:
9 | uses: tinkoff-mobile-tech/workflows/.github/workflows/android_lib.publish.yml@v1
10 | secrets:
11 | gpg_key: ${{ secrets.GPG_KEY }}
12 | sign_ossrh_gradle_properties: ${{ secrets.SIGN_OSSRH_GRADLE_PROPERTIES }}
13 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/ui/webView/TinkoffWebViewUiData.kt:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.core.tinkoffId.ui.webView
2 |
3 | /**
4 | * @author k.voskrebentsev
5 | */
6 | internal class TinkoffWebViewUiData(
7 | val clientId: String,
8 | val codeChallenge: String,
9 | val codeChallengeMethod: String,
10 | val redirectUri: String,
11 | val callbackUrl: String,
12 | )
13 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/ui/webView/TinkoffWebViewListener.kt:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.core.tinkoffId.ui.webView
2 |
3 | /**
4 | * @author k.voskrebentsev
5 | */
6 | internal interface TinkoffWebViewListener {
7 |
8 | fun isUrlForAuthCompletion(url: String): Boolean
9 |
10 | fun completeAuthWithSuccess(url: String)
11 |
12 | fun completeAuthWithCancellation()
13 | }
14 |
--------------------------------------------------------------------------------
/app-demo/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/ui/webView/TinkoffWebViewAuthPresenter.kt:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.core.tinkoffId.ui.webView
2 |
3 | import ru.tinkoff.core.tinkoffId.api.TinkoffIdApi
4 |
5 | /**
6 | * @author k.voskrebentsev
7 | */
8 | internal class TinkoffWebViewAuthPresenter {
9 |
10 | fun buildWebViewAuthStartUrl(uiData: TinkoffWebViewUiData): String {
11 | return TinkoffIdApi.buildWebViewAuthStartUrl(
12 | clientId = uiData.clientId,
13 | codeChallenge = uiData.codeChallenge,
14 | codeChallengeMethod = uiData.codeChallengeMethod,
15 | redirectUri = uiData.redirectUri,
16 | )
17 | }
18 |
19 | fun parseCode(url: String): String {
20 | return TinkoffIdApi.parseCode(url)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app-demo/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app-demo/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 | #121212
21 |
22 |
23 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/layout/tinkoff_id_web_view_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/gradle/plugin/detekt.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'io.gitlab.arturbosch.detekt'
2 | apply from: rootProject.file('gradle/versions.gradle')
3 |
4 | dependencies {
5 | detekt "io.gitlab.arturbosch.detekt:detekt-formatting:${detektVersion}"
6 | detekt("io.gitlab.arturbosch.detekt:detekt-cli:${detektVersion}")
7 | }
8 |
9 | detekt {
10 | toolVersion = detektVersion
11 | source = files("src")
12 | config = rootProject.files('gradle/plugin/config/detekt.yml')
13 | }
14 |
15 | tasks.named("detekt").configure {
16 | reports {
17 | xml.required.set(false)
18 |
19 | sarif.required.set(false)
20 |
21 | txt.required.set(true)
22 | txt.outputLocation.set(file("$project.buildDir/reports/detekt-results-${project.name}.txt"))
23 |
24 | html.required.set(true)
25 | html.outputLocation.set(file("$project.buildDir/reports/detekt-results-${project.name}.html"))
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tinkoff-id/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: '../gradle/plugin/android.gradle'
2 | apply from: '../gradle/plugin/publish-lib.gradle'
3 |
4 | android {
5 | resourcePrefix 'tinkoff_id_'
6 |
7 | testOptions {
8 | unitTests.includeAndroidResources = true
9 | }
10 | }
11 |
12 | dependencies {
13 | implementation "ru.tinkoff.core.components.security:ssl-trusted-certs:$sslTrustedCertsVersion"
14 |
15 | implementation "androidx.appcompat:appcompat:$appCompatVersion"
16 | implementation "com.squareup.okhttp3:okhttp:$okHttpVersion"
17 |
18 | testImplementation "junit:junit:$junitVersion"
19 | testImplementation "com.google.truth:truth:$truthVersion"
20 | testImplementation "org.robolectric:robolectric:$robolectricVersion"
21 | testImplementation "androidx.test:core:$testCoreVersion"
22 | testImplementation "com.squareup.okhttp3:mockwebserver:$okHttpVersion"
23 | }
24 |
25 | repositories {
26 | mavenCentral()
27 | }
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/values-en/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 | Tinkoff
21 | Reload
22 | Close
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable/tinkoff_id_badge_background_gray_style.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 | Tinkoff
21 | Перезагрузить
22 | Закрыть
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable/tinkoff_id_badge_background_black_style.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable/tinkoff_id_badge_background_yellow_style.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/menu/tinkoff_id_web_view_auth_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
27 |
--------------------------------------------------------------------------------
/gradle/versions.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 | compileSdkVersion = 33
3 | minSdkVersion = 21
4 | targetSdkVersion = 33
5 |
6 | isRelease = project.hasProperty('release')
7 | authCode = VERSION_CODE.toInteger()
8 | authVersion = getVersionName(VERSION_NAME)
9 |
10 | // tinkoff libs
11 | sslTrustedCertsVersion = '1.12.2'
12 |
13 | // android libs
14 | kotlinVersion = '1.8.10'
15 | appCompatVersion = '1.6.1'
16 | materialVersion = '1.9.0'
17 | constraintLayoutVersion = '2.1.4'
18 | androidPluginVersion = '7.4.2'
19 |
20 | // 3-rd party libs
21 | rxJavaVersion = '2.2.21'
22 | rxAndroidVersion = '2.1.1'
23 | okHttpVersion = '4.9.3'
24 |
25 | // documentation & quality libs
26 | detektVersion = '1.20.0'
27 | dokkaVersion = '1.6.21'
28 |
29 | // testing automation libs
30 | junitVersion = '4.13.2'
31 | truthVersion = '1.1.3'
32 | robolectricVersion = '4.8'
33 | testCoreVersion = '1.5.0'
34 | espressoVersion = '3.5.1'
35 | testRuleVersion = '1.5.0'
36 | uiautomatorVersion = '2.2.0'
37 | }
38 |
39 | def getVersionName(version) {
40 | return isRelease ? version : "${version}-SNAPSHOT"
41 | }
42 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/TinkoffIdStatusCode.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2021 Tinkoff Bank
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ru.tinkoff.core.tinkoffId
18 |
19 | /**
20 | * Status when returning to the partner application after requesting partner authorization
21 | *
22 | * @author Stanislav Mukhametshin
23 | */
24 | public enum class TinkoffIdStatusCode {
25 |
26 | /** Success: authorization succeeded, you can retrieve the code for requesting a token */
27 | SUCCESS,
28 |
29 | /** Authorization has been canceled by the user */
30 | CANCELLED_BY_USER
31 | }
32 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/error/TinkoffRequestException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2021 Tinkoff Bank
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ru.tinkoff.core.tinkoffId.error
18 |
19 | /**
20 | * Any exception related to requests is wrapped to [TinkoffRequestException]
21 | *
22 | * @author Stanislav Mukhametshin
23 | */
24 | public class TinkoffRequestException(
25 | public val reason: Throwable,
26 | override val message: String? = null,
27 | /** Information about Tinkoff Api errors */
28 | public val errorMessage: TinkoffErrorMessage? = null
29 | ) : Exception(message, reason)
30 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable/tinkoff_id_compact_background_black_style.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
20 | -
21 |
22 |
-
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable/tinkoff_id_compact_background_gray_style.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
20 | -
21 |
22 |
-
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable/tinkoff_id_compact_background_yellow_style.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
20 | -
21 |
22 |
-
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/TinkoffTokenPayload.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2021 Tinkoff Bank
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ru.tinkoff.core.tinkoffId
18 |
19 | /**
20 | * Model that contains tinkoff session information
21 | *
22 | * @author Stanislav Mukhametshin
23 | */
24 | public data class TinkoffTokenPayload(
25 | /** Token to access Tinkoff Api */
26 | val accessToken: String,
27 | /** Time after which access token will expire */
28 | val expiresIn: Int,
29 | /** User id in jwt format */
30 | val idToken: String?,
31 | /** Token needed to get new [accessToken] */
32 | val refreshToken: String
33 | )
34 |
--------------------------------------------------------------------------------
/app-demo/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Partner Auth
3 | Refresh token
4 | Revoke token
5 | ClientId
6 | RedirectUrl
7 | Reset
8 | compact
9 | standard | small size | black style
10 | standard | medium size | gray style
11 | standard | large size | yellow style
12 | Сannot be empty
13 | Sign in with
14 | Cashback up to 5%
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/error/TinkoffErrorMessage.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2021 Tinkoff Bank
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ru.tinkoff.core.tinkoffId.error
18 |
19 | /**
20 | * Information about errors received in request responses
21 | *
22 | * @author Stanislav Mukhametshin
23 | */
24 | public class TinkoffErrorMessage(
25 |
26 | /** Human readable message of api error */
27 | public val message: String?,
28 |
29 | /**
30 | * Error type returned after sending request to endpoints, you can find all error types
31 | * in [TinkoffTokenErrorConstants], [TokenSignOutErrorConstants]
32 | */
33 | public val errorType: Int
34 | )
35 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 1.1.1
2 |
3 | #### Fixed
4 | #### Changes
5 | #### Additions
6 |
7 | ## 1.1.0
8 |
9 | #### Fixed
10 | #### Changes
11 | #### Additions
12 |
13 | - [app-demo], [tinkoff-id] Добавлен флоу авторизации через WebView MC-7723
14 |
15 | ## 1.0.5
16 |
17 | #### Fixed
18 |
19 | - [tinkoff-id] Внесены правки в документацию MC-6740
20 |
21 | #### Changes
22 |
23 | - [tinkoff-id] update versions of target and compile sdk, AGP and Gradle, dependencies MC-9132
24 |
25 | #### Additions
26 |
27 | - [tinkoff-id] В Intent для запуска авторизации добавлен параметр версии SDK MC-6740
28 |
29 | ## 1.0.4
30 |
31 | #### Fixed
32 | #### Changes
33 | #### Additions
34 | - [tinkoff-id] add self-signed SSL certificates MC-7689
35 |
36 | ## 1.0.3
37 |
38 | #### Fixed
39 | #### Changes
40 | - [app-demo], [tinkoff-id] redesign TinkoffIdSignInButton MC-6146
41 | #### Additions
42 |
43 | ## 1.0.2
44 |
45 | #### Fixed
46 | #### Changes
47 | - [app-demo], [tinkoff-id] bump up API versions (targetSdk and compileSdk to 31) and dependencies versions MC-5659
48 | #### Additions
49 |
50 | ## 1.0.1
51 |
52 | #### Fixed
53 | #### Changes
54 | #### Additions
55 | - Added redirect_uri configuration logic
56 |
57 | ## 1.0.0
58 |
59 | #### Fixed
60 | #### Changes
61 | #### Additions
62 |
63 | [app-demo]: app-demo
64 | [tinkoff-id]: tinkoff-id
65 |
--------------------------------------------------------------------------------
/MIGRATION.md:
--------------------------------------------------------------------------------
1 | # Документ по миграции
2 |
3 | ## 1.0.6 -> 1.1.0
4 |
5 | В связи с добавлением альтернативного способа авторизации через веб Тинькофф с помощью WebView,
6 | переработана логика метода `createTinkoffAuthIntent(callbackUrl: Uri): Intent`. Теперь внутри него, на основе значения
7 | `isTinkoffAppAuthAvailable(): Boolean`, происходит создание Intent для открытия или приложения Тинькофф,
8 | или `TinkoffWebViewAuthActivity` (для прохождения авторизации в вебе).
9 |
10 | Рекомендуется использовать `createTinkoffAuthIntent(callbackUrl: Uri): Intent`, чтобы пользователю в любом случае была
11 | доступна авторизации через Тинькофф:
12 |
13 | **Before**:
14 |
15 | ```kotlin
16 | if (tinkoffPartnerAuth.isTinkoffAuthAvailable()) {
17 | val intent = tinkoffPartnerAuth.createTinkoffAuthIntent(callbackUrl)
18 | startActivity(intent)
19 | } else {
20 | // The logic of disabling authorization via Tinkoff
21 | }
22 | ```
23 |
24 | **After**:
25 |
26 | ```kotlin
27 | val intent = tinkoffPartnerAuth.createTinkoffAuthIntent(callbackUrl)
28 | startActivity(intent)
29 | ```
30 |
31 | Изменения в методах класса `TinkoffIdAuth`:
32 | - `createTinkoffAuthIntent(callbackUrl: Uri): Intent` -> `createTinkoffAppAuthIntent(callbackUrl: Uri): Intent`
33 | - `isTinkoffAuthAvailable(): Boolean` -> `isTinkoffAppAuthAvailable(): Boolean`
34 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/error/TokenSignOutErrorConstants.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2021 Tinkoff Bank
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ru.tinkoff.core.tinkoffId.error
18 |
19 | /**
20 | * Endpoint /auth/revoke.
21 | * Error types when revoking token
22 | *
23 | * @author Stanislav Mukhametshin
24 | */
25 | public object TokenSignOutErrorConstants {
26 |
27 | /**
28 | * User is not contains in audience
29 | */
30 | public const val INVALID_GRANT: Int = 1
31 |
32 | /**
33 | * There is no token in request
34 | */
35 | public const val INVALID_REQUEST: Int = 2
36 |
37 | /**
38 | * Unknown type of error
39 | */
40 | public const val UNKNOWN_ERROR: Int = 3
41 | }
42 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | android.useAndroidX=true
11 | android.enableJetifier=true
12 | kotlin.code.style=official
13 |
14 | POM_DESCRIPTION=Android library for Tinkoff authorization
15 | POM_LICENCE_NAME=The Apache Software License, Version 2.0
16 | POM_LICENCE_URL=https://github.com/tinkoff-mobile-tech/TinkoffID-Android/blob/master/LICENSE
17 | POM_LICENCE_DIST=repo
18 | POM_DEVELOPER_ID=tcs
19 | POM_DEVELOPER_NAME=Tinkoff Credit Systems
20 | POM_DEVELOPER_EMAIL=Tinkoff_id@tinkoff.ru
21 | POM_URL=https://github.com/tinkoff-mobile-tech/TinkoffID-Android
22 | POM_SCM_URL=https://github.com/tinkoff-mobile-tech/TinkoffID-Android/tree/main
23 | POM_SCM_CONNECTION=scm:git:git://github.com/tinkoff-mobile-tech/TinkoffID-Android.git
24 | POM_SCM_DEV_CONNECTION=scm:git:ssh://github.com:tinkoff-mobile-tech/TinkoffID-Android.git
25 |
26 | VERSION_NAME=1.1.1
27 | VERSION_CODE=8
28 | GROUP=ru.tinkoff.core.tinkoffauth
29 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable/tinkoff_id_close.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
22 |
25 |
26 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/codeVerifier/CodeVerifierStore.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2021 Tinkoff Bank
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ru.tinkoff.core.tinkoffId.codeVerifier
18 |
19 | import android.content.Context
20 | import android.content.SharedPreferences
21 |
22 | /**
23 | * @author Stanislav Mukhametshin
24 | */
25 | internal class CodeVerifierStore(context: Context) {
26 |
27 | private val preference: SharedPreferences = context.getSharedPreferences(PREFS_FILENAME, Context.MODE_PRIVATE)
28 |
29 | internal var codeVerifier: String
30 | get() = requireNotNull(preference.getString(CODE_VERIFIER, ""))
31 | set(value) = preference.edit().putString(CODE_VERIFIER, value).apply()
32 |
33 | companion object {
34 |
35 | private const val PREFS_FILENAME = "prefs_tinkoff_partner"
36 | private const val CODE_VERIFIER = "code_verifier"
37 | }
38 | }
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/drawable/tinkoff_id_reload_icon.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
22 |
26 |
27 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/values/attr.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/TinkoffCall.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2021 Tinkoff Bank
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ru.tinkoff.core.tinkoffId
18 |
19 | import androidx.annotation.WorkerThread
20 | import ru.tinkoff.core.tinkoffId.error.TinkoffRequestException
21 |
22 | /**
23 | * @author Stanislav Mukhametshin
24 | *
25 | * Main class to perform requests from Tinkoff Api
26 | */
27 | public interface TinkoffCall {
28 |
29 | /**
30 | * Function for synchronous operations
31 | *
32 | * NOTE should not be called from the main thread
33 | *
34 | * @return T
35 | *
36 | * @throws TinkoffRequestException if something goes wrong.
37 | * It can contain message [TinkoffErrorMessage][ru.tinkoff.core.tinkoffId.error.TinkoffErrorMessage]
38 | * with problem description
39 | */
40 | @WorkerThread
41 | @Throws(TinkoffRequestException::class)
42 | public fun getResponse(): T
43 |
44 | /**
45 | * Function to cancel [TinkoffCall]
46 | */
47 | public fun cancel()
48 | }
49 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 | #333333
21 | #FFDD2D
22 | #FAB619
23 | #FFEE95
24 |
25 | #333333
26 | #F5F5F6
27 | #CECECF
28 | #FFFFFF
29 |
30 | #FFFFFF
31 | #000000
32 | #333333
33 | #3D3D3D
34 |
35 | #428BF9
36 | #FFFFFF
37 |
38 |
39 |
--------------------------------------------------------------------------------
/gradle/plugin/android.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply from: '../gradle/plugin/kotlin-library.gradle'
3 | apply plugin: 'org.jetbrains.dokka'
4 |
5 | android {
6 | compileSdkVersion rootProject.compileSdkVersion
7 |
8 | defaultConfig {
9 | minSdkVersion rootProject.minSdkVersion
10 | targetSdkVersion rootProject.compileSdkVersion
11 | multiDexEnabled = true
12 | versionCode rootProject.authCode
13 | versionName rootProject.authVersion
14 | buildConfigField 'String', 'VERSION_NAME', "\"${rootProject.authVersion}\""
15 | vectorDrawables.useSupportLibrary = true
16 | }
17 |
18 | compileOptions {
19 | sourceCompatibility JavaVersion.VERSION_1_8
20 | targetCompatibility JavaVersion.VERSION_1_8
21 | }
22 |
23 | lintOptions {
24 | htmlReport true
25 | abortOnError true
26 | warningsAsErrors true
27 | disable 'GradleDependency'
28 | }
29 |
30 | packagingOptions {
31 | exclude 'LICENSE.txt'
32 | exclude 'META-INF/LICENSE.txt'
33 | exclude 'META-INF/NOTICE.txt'
34 | exclude 'META-INF/androidx.exifinterface_exifinterface.version'
35 | }
36 |
37 | testOptions {
38 | unitTests {
39 | returnDefaultValues = true
40 | includeAndroidResources = true
41 | }
42 | }
43 |
44 | kotlinOptions {
45 | freeCompilerArgs += '-Xexplicit-api=strict'
46 | }
47 |
48 | dokkaHtml.configure {
49 | dokkaSourceSets {
50 | configureEach {
51 | includeNonPublic.set(false)
52 | noAndroidSdkLink.set(false)
53 | suppressInheritedMembers.set(true)
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app-demo/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply from: rootProject.file('gradle/versions.gradle')
4 | apply from: rootProject.file('gradle/plugin/detekt.gradle')
5 |
6 | android {
7 | compileSdkVersion rootProject.ext.compileSdkVersion
8 |
9 | defaultConfig {
10 | multiDexEnabled = true
11 | minSdkVersion rootProject.minSdkVersion
12 | targetSdkVersion rootProject.targetSdkVersion
13 | versionCode rootProject.authCode
14 | versionName rootProject.authVersion
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 |
29 | }
30 |
31 | dependencies {
32 | api project(':tinkoff-id')
33 | api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
34 | api "androidx.appcompat:appcompat:${appCompatVersion}"
35 | api "androidx.constraintlayout:constraintlayout:${constraintLayoutVersion}"
36 | api "com.google.android.material:material:${materialVersion}"
37 | //rx
38 | api "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
39 | api "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
40 | //tests
41 | androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
42 | androidTestImplementation "androidx.test:rules:$testRuleVersion"
43 | androidTestImplementation "androidx.test.uiautomator:uiautomator:$uiautomatorVersion"
44 | }
45 |
--------------------------------------------------------------------------------
/app-demo/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
18 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/app-demo/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/error/TinkoffTokenErrorConstants.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2021 Tinkoff Bank
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ru.tinkoff.core.tinkoffId.error
18 |
19 | /**
20 | * Endpoint /auth/token.
21 | * Error types when refreshing or obtaining token
22 | *
23 | * @author Stanislav Mukhametshin
24 | */
25 | public object TinkoffTokenErrorConstants {
26 |
27 | /**
28 | * There is not required parameters, cookies, headers and so on
29 | */
30 | public const val INVALID_REQUEST: Int = 1
31 |
32 | /**
33 | * redirect_uri does not match the client
34 | */
35 | public const val INVALID_CLIENT: Int = 2
36 |
37 | /**
38 | * Invalid/expired refresh_token or code passed
39 | */
40 | public const val INVALID_GRANT: Int = 3
41 |
42 | /**
43 | * No required headers
44 | */
45 | public const val UNAUTHORIZED_CLIENT: Int = 4
46 |
47 | /**
48 | * Unknown grant_type passed
49 | */
50 | public const val UNSUPPORTED_GRANT_TYPE: Int = 5
51 |
52 | /**
53 | * Something went wrong. You can try to repeat request again
54 | */
55 | public const val SERVER_ERROR: Int = 6
56 |
57 | /**
58 | * The app is asking for tokens too often (current limit is 50 per hour), it might be worth looking for a bug in the app
59 | */
60 | public const val LIMIT_EXCEEDED: Int = 7
61 |
62 | /**
63 | * Unknown error type returned
64 | */
65 | public const val UNKNOWN_ERROR: Int = 8
66 | }
67 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/ui/webView/TinkoffWebViewClient.kt:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.core.tinkoffId.ui.webView
2 |
3 | import android.webkit.CookieManager
4 | import android.webkit.RenderProcessGoneDetail
5 | import android.webkit.WebResourceRequest
6 | import android.webkit.WebView
7 | import android.webkit.WebViewClient
8 |
9 | /**
10 | * @author k.voskrebentsev
11 | */
12 | internal class TinkoffWebViewClient(
13 | private val listener: TinkoffWebViewListener,
14 | ) : WebViewClient() {
15 |
16 | private var lastUrl: String? = null
17 |
18 | override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
19 | return processUrl(request?.url.toString())
20 | }
21 |
22 | @Deprecated("Deprecated in Java")
23 | override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
24 | return processUrl(url)
25 | }
26 |
27 | private fun processUrl(url: String?): Boolean {
28 | if (url == null || !listener.isUrlForAuthCompletion(url)) {
29 | lastUrl = url
30 | return false
31 | }
32 |
33 | lastUrl?.let { clearCookies(it) }
34 | listener.completeAuthWithSuccess(url)
35 | return true
36 | }
37 |
38 | private fun clearCookies(url: String) {
39 | val cookieManager = CookieManager.getInstance()
40 | val cookiesNames = CookieManager.getInstance().getCookie(url)
41 | .split(COOKIES_SEPARATOR)
42 | .map { cookie ->
43 | val endIndexOfCookieName = cookie.indexOf(COOKIE_NAME_AND_VALUE_SEPARATOR)
44 | cookie.substring(0, endIndexOfCookieName)
45 | }
46 | cookiesNames.forEach { cookieName ->
47 | cookieManager.setCookie(url, "$cookieName$COOKIE_NAME_AND_VALUE_SEPARATOR")
48 | }
49 | cookieManager.flush()
50 | }
51 |
52 | override fun onRenderProcessGone(view: WebView?, detail: RenderProcessGoneDetail?): Boolean {
53 | listener.completeAuthWithCancellation()
54 | return true
55 | }
56 |
57 | private companion object {
58 | const val COOKIES_SEPARATOR = "; "
59 | const val COOKIE_NAME_AND_VALUE_SEPARATOR = "="
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tinkoff-id/src/test/java/ru/tinkoff/core/tinkoffId/TinkoffIdSignInButtonTest.kt:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.core.tinkoffId
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import android.text.SpannedString
6 | import androidx.core.content.res.ResourcesCompat
7 | import androidx.test.core.app.ApplicationProvider
8 | import org.junit.Assert.assertEquals
9 | import org.junit.Before
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 | import org.robolectric.RobolectricTestRunner
13 | import org.robolectric.annotation.Config
14 | import ru.tinkoff.core.tinkoffId.ui.TinkoffDimen
15 | import ru.tinkoff.core.tinkoffId.ui.TinkoffIdSignInButton
16 |
17 | /**
18 | * @author k.voskrebentsev
19 | */
20 | @Config(sdk = [Build.VERSION_CODES.P])
21 | @RunWith(RobolectricTestRunner::class)
22 | internal class TinkoffIdSignInButtonTest {
23 |
24 | private lateinit var button: TinkoffIdSignInButton
25 |
26 | @Before
27 | fun setUp() {
28 | button = TinkoffIdSignInButton(context = context)
29 | }
30 |
31 | @Test
32 | fun testSetEmptyTitle() {
33 | button.title = ""
34 |
35 | val result = button.title as SpannedString
36 |
37 | assertEquals(PERMANENT_TITLE_PART, result.toString())
38 | }
39 |
40 | @Test
41 | fun testSetTitle() {
42 | button.title = CUSTOM_TITLE_PART
43 |
44 | val result = button.title as SpannedString
45 |
46 | assertEquals("$CUSTOM_TITLE_PART $PERMANENT_TITLE_PART", result.toString())
47 | }
48 |
49 | @Test
50 | fun testDefaultBadgeText() {
51 | assertEquals("", button.badgeText)
52 | }
53 |
54 | @Test
55 | fun testDefaultCompact() {
56 | assertEquals(false, button.isCompact)
57 | }
58 |
59 | @Test
60 | fun testDefaultStyle() {
61 | assertEquals(TinkoffIdSignInButton.ButtonStyle.YELLOW, button.style)
62 | }
63 |
64 | @Test
65 | fun testDefaultCornerSize() {
66 | assertEquals(context.resources.getDimension(TinkoffDimen.tinkoff_id_default_corner_radius).toInt(), button.cornerRadius)
67 | }
68 |
69 | @Test
70 | fun testDefaultFont() {
71 | assertEquals(ResourcesCompat.getFont(context, R.font.neue_haas_unica_w1g), button.textFont)
72 | }
73 |
74 | companion object {
75 | val context: Context = ApplicationProvider.getApplicationContext()
76 |
77 | const val CUSTOM_TITLE_PART = "title"
78 | val PERMANENT_TITLE_PART = context.getString(R.string.tinkoff_id_tinkoff_text)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/tinkoff-id/src/test/java/ru/tinkoff/core/tinkoffId/TinkoffIdApiTest.kt:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.core.tinkoffId
2 |
3 | import android.net.Uri
4 | import android.os.Build
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 | import org.robolectric.RobolectricTestRunner
9 | import org.robolectric.annotation.Config
10 | import ru.tinkoff.core.tinkoffId.api.TinkoffIdApi
11 |
12 | /**
13 | * @author k.voskrebentsev
14 | */
15 | @Config(sdk = [Build.VERSION_CODES.P])
16 | @RunWith(RobolectricTestRunner::class)
17 | internal class TinkoffIdApiTest {
18 |
19 | @Test
20 | fun testWebViewUrlCreation() {
21 | val result = TinkoffIdApi.buildWebViewAuthStartUrl(
22 | clientId = VALUE_CLIENT_ID,
23 | codeChallenge = VALUE_CODE_CHALLENGE,
24 | codeChallengeMethod = VALUE_CODE_CHALLENGE_METHOD,
25 | redirectUri = VALUE_REDIRECT_URI,
26 | )
27 |
28 | val uri = Uri.parse(result)
29 | assertEquals(HOST, "${uri.scheme}://${uri.host}")
30 | assertEquals("/$PATH_AUTHORIZE", uri.path)
31 | uri.assertQueryParam(FIELD_CLIENT_ID, VALUE_CLIENT_ID)
32 | uri.assertQueryParam(FIELD_CODE_CHALLENGE, VALUE_CODE_CHALLENGE)
33 | uri.assertQueryParam(FIELD_CODE_CHALLENGE_METHOD, VALUE_CODE_CHALLENGE_METHOD)
34 | uri.assertQueryParam(FIELD_REDIRECT_URI, VALUE_REDIRECT_URI)
35 | uri.assertQueryParam(FIELD_RESPONSE_TYPE, VALUE_RESPONSE_TYPE)
36 | uri.assertQueryParam(FIELD_RESPONSE_MODE, VALUE_RESPONSE_MODE)
37 | }
38 |
39 | @Test
40 | fun testCodeParsing() {
41 | val url = Uri.parse(HOST).buildUpon()
42 | .appendQueryParameter(FIELD_CODE, VALUE_CODE)
43 | .build()
44 | .toString()
45 |
46 | val result = TinkoffIdApi.parseCode(url)
47 |
48 | assertEquals(VALUE_CODE, result)
49 | }
50 |
51 | @Test(expected = IllegalArgumentException::class)
52 | fun testCodeAbsence() {
53 | TinkoffIdApi.parseCode("")
54 | }
55 |
56 | private fun Uri.assertQueryParam(paramName: String, expectedValue: String) {
57 | val actualValue = getQueryParameter(paramName)
58 | assertEquals(expectedValue, actualValue)
59 | }
60 |
61 | companion object {
62 | private const val HOST = "https://id.tinkoff.ru"
63 | private const val PATH_AUTHORIZE = "auth/authorize"
64 |
65 | private const val FIELD_CLIENT_ID = "client_id"
66 | private const val FIELD_REDIRECT_URI = "redirect_uri"
67 | private const val FIELD_CODE_CHALLENGE = "code_challenge"
68 | private const val FIELD_CODE_CHALLENGE_METHOD = "code_challenge_method"
69 | private const val FIELD_RESPONSE_TYPE = "response_type"
70 | private const val FIELD_RESPONSE_MODE = "response_mode"
71 | private const val FIELD_CODE = "code"
72 |
73 | private const val VALUE_CLIENT_ID = "client_id_value"
74 | private const val VALUE_CODE_CHALLENGE = "code_challenge_value"
75 | private const val VALUE_CODE_CHALLENGE_METHOD = "code_challenge_method_value"
76 | private const val VALUE_REDIRECT_URI = "redirect_uri_value"
77 | private const val VALUE_CODE = "code_value"
78 | private const val VALUE_RESPONSE_TYPE = "code"
79 | private const val VALUE_RESPONSE_MODE = "query"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 | 8dp
21 |
22 | 30dp
23 | 40dp
24 | 60dp
25 |
26 | 5dp
27 | 10dp
28 | 16dp
29 | 10dp
30 | 13dp
31 | 20dp
32 |
33 | 12sp
34 | 15sp
35 | 19sp
36 |
37 | 4dp
38 | 8dp
39 | 8dp
40 |
41 | 15dp
42 | 30dp
43 | 19dp
44 | 37dp
45 | 22dp
46 | 45dp
47 | 2dp
48 |
49 | 20dp
50 | 26dp
51 | 40dp
52 |
53 | 11sp
54 | 11sp
55 | 13sp
56 | 2.5dp
57 | 4dp
58 | 4.5dp
59 | 6dp
60 | 5.5dp
61 | 8dp
62 | 14dp
63 |
64 | 12dp
65 | 10dp
66 |
67 |
68 |
--------------------------------------------------------------------------------
/gradle/plugin/publish-lib.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2016 Tinkoff Bank
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | apply plugin: 'maven-publish'
18 | apply plugin: 'signing'
19 |
20 | def isReleaseBuild() {
21 | return !VERSION_NAME.contains("SNAPSHOT")
22 | }
23 |
24 | def getReleaseRepositoryUrl() {
25 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
26 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
27 | }
28 |
29 | def getRepositoryUsername() {
30 | return hasProperty('ossrhUsername') ? ossrhUsername : ''
31 | }
32 |
33 | def getRepositoryPassword() {
34 | return hasProperty('ossrhPassword') ? ossrhPassword : ''
35 | }
36 |
37 | afterEvaluate {
38 | publishing {
39 | publications {
40 | release(MavenPublication) {
41 | groupId GROUP
42 | artifactId POM_ARTIFACT_ID
43 | version VERSION_NAME
44 |
45 | from components.release
46 |
47 | afterEvaluate {
48 | artifact androidSourcesJar
49 | artifact androidJavadocsJar
50 | }
51 |
52 | pom {
53 | name = POM_NAME
54 | packaging = POM_PACKAGING
55 | description = POM_DESCRIPTION
56 | url = POM_URL
57 | scm {
58 | url = POM_SCM_URL
59 | connection = POM_SCM_CONNECTION
60 | developerConnection = POM_SCM_DEV_CONNECTION
61 | }
62 | licenses {
63 | license {
64 | name = POM_LICENCE_NAME
65 | url = POM_LICENCE_URL
66 | distribution = POM_LICENCE_DIST
67 | }
68 | }
69 | developers {
70 | developer {
71 | id = POM_DEVELOPER_ID
72 | name = POM_DEVELOPER_NAME
73 | email = POM_DEVELOPER_EMAIL
74 | }
75 | }
76 | }
77 | }
78 | }
79 | repositories {
80 | maven {
81 | url = getReleaseRepositoryUrl()
82 | credentials {
83 | username = getRepositoryUsername()
84 | password = getRepositoryPassword()
85 | }
86 | }
87 | }
88 | }
89 |
90 | task androidSourcesJar(type: Jar) {
91 | archiveClassifier.set("sources")
92 | from android.sourceSets.main.java.srcDirs
93 | }
94 |
95 | task androidJavadocsJar(type: Jar, dependsOn: dokkaJavadoc) {
96 | archiveClassifier.set("javadoc")
97 | from dokkaJavadoc.outputDirectory
98 | }
99 |
100 | signing {
101 | required { isReleaseBuild() && gradle.taskGraph.hasTask("publish") }
102 | sign publishing.publications.release
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/tinkoff-id/src/test/java/ru/tinkoff/core/tinkoffId/AppLinkUtilTest.kt:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.core.tinkoffId
2 |
3 | import android.content.Intent
4 | import android.os.Build
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 | import org.robolectric.RobolectricTestRunner
9 | import org.robolectric.annotation.Config
10 |
11 | /**
12 | * @author k.voskrebentsev
13 | */
14 | @Config(sdk = [Build.VERSION_CODES.P])
15 | @RunWith(RobolectricTestRunner::class)
16 | internal class AppLinkUtilTest {
17 |
18 | @Test
19 | fun testWebViewIntentParsing() {
20 | val intent = Intent()
21 | .putExtra(QUERY_PARAMETER_CLIENT_ID, VALUE_PARAMETER_CLIENT_ID)
22 | .putExtra(QUERY_PARAMETER_CODE_CHALLENGE, VALUE_PARAMETER_CODE_CHALLENGE)
23 | .putExtra(QUERY_PARAMETER_CODE_CHALLENGE_METHOD, VALUE_PARAMETER_CODE_CHALLENGE_METHOD)
24 | .putExtra(QUERY_PARAMETER_REDIRECT_URI, VALUE_PARAMETER_REDIRECT_URI)
25 | .putExtra(QUERY_PARAMETER_CALLBACK_URL, VALUE_PARAMETER_CALLBACK_URL)
26 |
27 | val result = AppLinkUtil.parseTinkoffWebViewUiData(intent)
28 |
29 | assertEquals(VALUE_PARAMETER_CLIENT_ID, result.clientId)
30 | assertEquals(VALUE_PARAMETER_CODE_CHALLENGE, result.codeChallenge)
31 | assertEquals(VALUE_PARAMETER_CODE_CHALLENGE_METHOD, result.codeChallengeMethod)
32 | assertEquals(VALUE_PARAMETER_REDIRECT_URI, result.redirectUri)
33 | assertEquals(VALUE_PARAMETER_CALLBACK_URL, result.callbackUrl)
34 | }
35 |
36 | @Test
37 | fun testCodeIntentCreation() {
38 | val code = "code_value"
39 |
40 | val result = AppLinkUtil.createBackAppCodeIntent(
41 | callbackUrl = VALUE_PARAMETER_CALLBACK_URL,
42 | code = code,
43 | )
44 |
45 | assertEquals(Intent.ACTION_VIEW, result.action)
46 | assertEquals(VALUE_PARAMETER_CALLBACK_URL, result.data?.path)
47 | assertEquals(code, result.data?.getQueryParameter(QUERY_PARAMETER_CODE))
48 | assertEquals(AUTH_STATUS_CODE_SUCCESS, result.data?.getQueryParameter(QUERY_PARAMETER_AUTH_STATUS_CODE))
49 | }
50 |
51 | @Test
52 | fun testCancelIntentCreation() {
53 | val result = AppLinkUtil.createBackAppCancelIntent(
54 | callbackUrl = VALUE_PARAMETER_CALLBACK_URL,
55 | )
56 |
57 | assertEquals(Intent.ACTION_VIEW, result.action)
58 | assertEquals(VALUE_PARAMETER_CALLBACK_URL, result.data?.path)
59 | assertEquals(AUTH_STATUS_CODE_CANCELLED_BY_USER, result.data?.getQueryParameter(QUERY_PARAMETER_AUTH_STATUS_CODE))
60 | }
61 |
62 | companion object {
63 | private const val QUERY_PARAMETER_CLIENT_ID = "clientId"
64 | private const val QUERY_PARAMETER_CODE_CHALLENGE = "code_challenge"
65 | private const val QUERY_PARAMETER_CODE_CHALLENGE_METHOD = "code_challenge_method"
66 | private const val QUERY_PARAMETER_CALLBACK_URL = "callback_url"
67 | private const val QUERY_PARAMETER_REDIRECT_URI = "redirect_uri"
68 |
69 | private const val VALUE_PARAMETER_CLIENT_ID = "clientId_value"
70 | private const val VALUE_PARAMETER_CODE_CHALLENGE = "code_challenge_value"
71 | private const val VALUE_PARAMETER_CODE_CHALLENGE_METHOD = "code_challenge_method_value"
72 | private const val VALUE_PARAMETER_CALLBACK_URL = "callback_url_value"
73 | private const val VALUE_PARAMETER_REDIRECT_URI = "redirect_uri_value"
74 |
75 | private const val QUERY_PARAMETER_CODE = "code"
76 | private const val QUERY_PARAMETER_AUTH_STATUS_CODE = "auth_status_code"
77 |
78 | private const val AUTH_STATUS_CODE_SUCCESS = "success"
79 | private const val AUTH_STATUS_CODE_CANCELLED_BY_USER = "cancelled_by_user"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app-demo/src/androidTest/java/ru/tinkoff/core/app_demo_partner/TinkoffIdSignInButtonTest.kt:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.core.app_demo_partner
2 |
3 | import androidx.test.espresso.Espresso.onView
4 | import androidx.test.espresso.assertion.ViewAssertions.matches
5 | import androidx.test.espresso.matcher.ViewMatchers
6 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
7 | import androidx.test.espresso.matcher.ViewMatchers.withChild
8 | import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
9 | import androidx.test.espresso.matcher.ViewMatchers.withId
10 | import androidx.test.espresso.matcher.ViewMatchers.withText
11 | import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
12 | import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
13 | import androidx.test.rule.ActivityTestRule
14 | import androidx.test.runner.AndroidJUnitRunner
15 | import org.hamcrest.CoreMatchers.allOf
16 | import org.junit.Before
17 | import org.junit.Rule
18 | import org.junit.Test
19 | import org.junit.runner.RunWith
20 | import ru.tinkoff.core.tinkoffId.ui.TinkoffIdSignInButton
21 |
22 | /**
23 | * @author k.voskrebentsev
24 | */
25 | @RunWith(AndroidJUnit4ClassRunner::class)
26 | class TinkoffIdSignInButtonTest : AndroidJUnitRunner() {
27 |
28 | @get:Rule
29 | val activityRule = ActivityTestRule(PartnerActivity::class.java, false, true)
30 |
31 | private lateinit var standardButton: TinkoffIdSignInButton
32 |
33 | @Before
34 | fun setUp() {
35 | runOnMainThread {
36 | standardButton = activityRule.activity.findViewById(standardButtonId)
37 | }
38 | }
39 |
40 | @Test
41 | fun testTitleChanging() {
42 | runOnMainThread {
43 | standardButton.title = SOME_TEXT
44 | }
45 |
46 | checkTextOnView(createFinalTitle(SOME_TEXT))
47 | }
48 |
49 | @Test
50 | fun testBadgeTextChanging() {
51 | runOnMainThread {
52 | standardButton.badgeText = SOME_TEXT
53 | }
54 |
55 | checkTextOnView(SOME_TEXT)
56 | }
57 |
58 | @Test
59 | fun testTextElementsHidingInCompactMode() {
60 | runOnMainThread {
61 | standardButton.title = SOME_TEXT
62 | standardButton.badgeText = SOME_TEXT
63 | standardButton.isCompact = true
64 | }
65 |
66 | onView(
67 | allOf(
68 | withId(standardButtonId),
69 | // title element
70 | withChild(
71 | allOf(
72 | withText(createFinalTitle(SOME_TEXT)),
73 | withEffectiveVisibility(ViewMatchers.Visibility.GONE)
74 | )
75 | ),
76 | // badge element
77 | withChild(
78 | allOf(
79 | withText(SOME_TEXT),
80 | withEffectiveVisibility(ViewMatchers.Visibility.GONE)
81 | )
82 | ),
83 | )
84 | ).check(matches(isDisplayed()))
85 | }
86 |
87 | private fun checkTextOnView(text: String) {
88 | onView(
89 | allOf(
90 | withId(standardButtonId),
91 | withChild(withText(text))
92 | )
93 | ).check(matches(isDisplayed()))
94 | }
95 |
96 | private fun createFinalTitle(titlePart: String) = "$titlePart $permanentTitlePart"
97 | private fun runOnMainThread(block: () -> Unit) {
98 | activityRule.runOnUiThread(block)
99 | }
100 |
101 | private companion object {
102 | private const val SOME_TEXT = "test"
103 |
104 | private const val standardButtonId = R.id.standardSmallBlackButtonTinkoffAuth
105 |
106 | private val permanentTitlePart = getInstrumentation().targetContext.resources.getString(ru.tinkoff.core.tinkoffId.R.string.tinkoff_id_tinkoff_text)
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/tinkoff-id/src/test/java/ru/tinkoff/core/tinkoffId/TinkoffIdAuthTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2021 Tinkoff Bank
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ru.tinkoff.core.tinkoffId
18 |
19 | import android.content.Context
20 | import android.net.Uri
21 | import android.os.Build
22 | import androidx.test.core.app.ApplicationProvider
23 | import com.google.common.truth.Truth.assertThat
24 | import org.junit.Test
25 | import org.junit.runner.RunWith
26 | import org.robolectric.RobolectricTestRunner
27 | import org.robolectric.annotation.Config
28 |
29 | /**
30 | * @author Stanislav Mukhametshin
31 | */
32 | @Config(sdk = [Build.VERSION_CODES.P])
33 | @RunWith(RobolectricTestRunner::class)
34 | public class TinkoffIdAuthTest {
35 |
36 | private var context: Context = ApplicationProvider.getApplicationContext()
37 | private val tinkoffAuth: TinkoffIdAuth = TinkoffIdAuth(context, CLIENT_ID, REDIRECT_URL)
38 | private val testUri: Uri = Uri.Builder()
39 | .scheme("https")
40 | .authority("www.partner.com")
41 | .appendPath("partner")
42 | .build()
43 |
44 | @Test
45 | public fun testIntentCreation() {
46 | val intent = tinkoffAuth.createTinkoffAppAuthIntent(testUri)
47 | assertThat(intent.data).isNotNull()
48 | val queryParam = { paramName: String -> requireNotNull(intent.data).getQueryParameter(paramName) }
49 | assertThat(queryParam("clientId")).isEqualTo(CLIENT_ID)
50 | assertThat(queryParam("code_challenge")).isNotEmpty()
51 | assertThat(queryParam("code_challenge_method")).isNotEmpty()
52 | assertThat(queryParam("redirect_uri")).isEqualTo(REDIRECT_URL)
53 | assertThat(queryParam("callback_url")).isEqualTo(testUri.toString())
54 | assertThat(queryParam("package_name")).isNotEmpty()
55 | assertThat(queryParam("partner_sdk_version")).isEqualTo(BuildConfig.VERSION_NAME)
56 | }
57 |
58 | @Test
59 | public fun testWebViewIntentCreation() {
60 | val intent = tinkoffAuth.createTinkoffWebViewAuthIntent(testUri)
61 | assertThat(intent.extras).isNotNull()
62 | val queryParam = { paramName: String -> requireNotNull(intent.getStringExtra(paramName)) }
63 | assertThat(queryParam("clientId")).isEqualTo(CLIENT_ID)
64 | assertThat(queryParam("code_challenge")).isNotEmpty()
65 | assertThat(queryParam("code_challenge_method")).isNotEmpty()
66 | assertThat(queryParam("redirect_uri")).isEqualTo(REDIRECT_URL)
67 | assertThat(queryParam("callback_url")).isEqualTo(testUri.toString())
68 | }
69 |
70 | @Test
71 | public fun testStatusWhenReturningBack() {
72 | listOf(AUTH_STATUS_CODE_SUCCESS to TinkoffIdStatusCode.SUCCESS, AUTH_STATUS_CODE_CANCELLED_BY_USER to TinkoffIdStatusCode.CANCELLED_BY_USER)
73 | .forEach { (param, status) ->
74 | val uri = testUri.buildUpon().appendQueryParameter(QUERY_PARAMETER_AUTH_STATUS_CODE, param).build()
75 | assertThat(tinkoffAuth.getStatusCode(uri)).isEqualTo(status)
76 | }
77 | }
78 |
79 | private companion object {
80 |
81 | private const val CLIENT_ID = "testClientId"
82 | private const val REDIRECT_URL = "mobile://redirectUrl"
83 | private const val QUERY_PARAMETER_AUTH_STATUS_CODE = "auth_status_code"
84 |
85 | private const val AUTH_STATUS_CODE_SUCCESS = "success"
86 | private const val AUTH_STATUS_CODE_CANCELLED_BY_USER = "cancelled_by_user"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/ui/webView/TinkoffWebViewAuthActivity.kt:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.core.tinkoffId.ui.webView
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.webkit.WebSettings.LOAD_NO_CACHE
7 | import android.webkit.WebView
8 | import androidx.activity.OnBackPressedCallback
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.appcompat.widget.Toolbar
11 | import ru.tinkoff.core.tinkoffId.AppLinkUtil
12 | import ru.tinkoff.core.tinkoffId.R
13 |
14 | /**
15 | * @author k.voskrebentsev
16 | */
17 | internal class TinkoffWebViewAuthActivity : AppCompatActivity() {
18 |
19 | private val presenter: TinkoffWebViewAuthPresenter by lazy { TinkoffWebViewAuthPresenter() }
20 |
21 | private lateinit var webView: WebView
22 |
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | setContentView(R.layout.tinkoff_id_web_view_activity)
26 |
27 | val uiData = AppLinkUtil.parseTinkoffWebViewUiData(intent)
28 |
29 | initWebView(uiData)
30 | initToolbar(uiData)
31 | initBackPress(uiData)
32 | }
33 |
34 | private fun initToolbar(uiData: TinkoffWebViewUiData) {
35 | val toolbar = findViewById(R.id.toolbar)
36 |
37 | toolbar.inflateMenu(R.menu.tinkoff_id_web_view_auth_menu)
38 | toolbar.setOnMenuItemClickListener { menuItem ->
39 | if (menuItem.itemId == R.id.reloadMenuItem) {
40 | webView.reload()
41 | true
42 | } else {
43 | false
44 | }
45 | }
46 |
47 | toolbar.setNavigationOnClickListener {
48 | finishWithCancellation(uiData.callbackUrl)
49 | }
50 | }
51 |
52 | private fun initBackPress(uiData: TinkoffWebViewUiData) {
53 | val callback = object : OnBackPressedCallback(true) {
54 | override fun handleOnBackPressed() {
55 | finishWithCancellation(uiData.callbackUrl)
56 | }
57 | }
58 | onBackPressedDispatcher.addCallback(callback)
59 | }
60 |
61 | @SuppressLint("SetJavaScriptEnabled")
62 | private fun initWebView(uiData: TinkoffWebViewUiData) {
63 | webView = findViewById(R.id.webView)
64 | val url = presenter.buildWebViewAuthStartUrl(uiData)
65 | webView.run {
66 | webViewClient = TinkoffWebViewClient(createTinkoffWebViewCallback(uiData))
67 | with(settings) {
68 | javaScriptEnabled = true
69 | setGeolocationEnabled(false)
70 | cacheMode = LOAD_NO_CACHE
71 | allowFileAccess = false
72 | allowContentAccess = false
73 | }
74 | loadUrl(url)
75 | }
76 | }
77 |
78 | private fun createTinkoffWebViewCallback(uiData: TinkoffWebViewUiData): TinkoffWebViewListener {
79 | return object : TinkoffWebViewListener {
80 |
81 | override fun isUrlForAuthCompletion(url: String): Boolean {
82 | return url.startsWith(uiData.redirectUri)
83 | }
84 |
85 | override fun completeAuthWithSuccess(url: String) {
86 | finish(
87 | intent = AppLinkUtil.createBackAppCodeIntent(
88 | callbackUrl = uiData.callbackUrl,
89 | code = presenter.parseCode(url),
90 | )
91 | )
92 | }
93 |
94 | override fun completeAuthWithCancellation() {
95 | finishWithCancellation(uiData.callbackUrl)
96 | }
97 | }
98 | }
99 |
100 | private fun finishWithCancellation(callbackUrl: String) {
101 | finish(
102 | intent = AppLinkUtil.createBackAppCancelIntent(callbackUrl)
103 | )
104 | }
105 |
106 | private fun finish(intent: Intent) {
107 | intent.setPackage(packageName)
108 | startActivity(intent)
109 | finish()
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/app-demo/src/main/java/ru/tinkoff/core/app_demo_partner/PartnerPresenter.kt:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.core.app_demo_partner
2 |
3 | import android.net.Uri
4 | import android.util.Log
5 | import androidx.lifecycle.DefaultLifecycleObserver
6 | import androidx.lifecycle.LifecycleOwner
7 | import io.reactivex.Single
8 | import io.reactivex.android.schedulers.AndroidSchedulers
9 | import io.reactivex.disposables.CompositeDisposable
10 | import io.reactivex.schedulers.Schedulers
11 | import ru.tinkoff.core.tinkoffId.TinkoffCall
12 | import ru.tinkoff.core.tinkoffId.TinkoffIdAuth
13 | import ru.tinkoff.core.tinkoffId.TinkoffIdStatusCode
14 | import ru.tinkoff.core.tinkoffId.TinkoffTokenPayload
15 |
16 | /**
17 | * @author Stanislav Mukhametshin
18 | */
19 | class PartnerPresenter(
20 | private val tinkoffPartnerAuth: TinkoffIdAuth,
21 | private val partnerActivity: PartnerActivity
22 | ) : DefaultLifecycleObserver {
23 |
24 | private val compositeDisposable = CompositeDisposable()
25 | private var tokenPayload: TinkoffTokenPayload? = null
26 |
27 | init {
28 | partnerActivity.lifecycle.addObserver(this)
29 | }
30 |
31 | fun getToken(uri: Uri) {
32 | when (tinkoffPartnerAuth.getStatusCode(uri)) {
33 | TinkoffIdStatusCode.SUCCESS -> {
34 | tinkoffPartnerAuth.getTinkoffTokenPayload(uri)
35 | .toSingle()
36 | .subscribeOn(Schedulers.io())
37 | .observeOn(AndroidSchedulers.mainThread())
38 | .subscribe(
39 | {
40 | Log.d(LOG_TAG, it.accessToken)
41 | tokenPayload = it
42 | partnerActivity.onTokenAvailable()
43 | },
44 | {
45 | Log.e(LOG_TAG, "GetToken Error", it)
46 | partnerActivity.onAuthError()
47 | }
48 | ).apply {
49 | compositeDisposable.add(this)
50 | }
51 | }
52 | TinkoffIdStatusCode.CANCELLED_BY_USER -> {
53 | partnerActivity.onCancelledByUser()
54 | }
55 | else -> {
56 | partnerActivity.onAuthError()
57 | }
58 | }
59 | }
60 |
61 | fun refreshToken() {
62 | val refreshToken = tokenPayload?.refreshToken ?: return
63 | tinkoffPartnerAuth.obtainTokenPayload(refreshToken)
64 | .toSingle()
65 | .subscribeOn(Schedulers.io())
66 | .observeOn(AndroidSchedulers.mainThread())
67 | .subscribe(
68 | {
69 | Log.d(LOG_TAG, it.accessToken)
70 | partnerActivity.onTokenRefresh()
71 | tokenPayload = it
72 | },
73 | {
74 | Log.e(LOG_TAG, "RefreshToken Error", it)
75 | }
76 | ).apply {
77 | compositeDisposable.add(this)
78 | }
79 | }
80 |
81 | fun revokeToken() {
82 | val refreshToken = tokenPayload?.refreshToken ?: return
83 | tinkoffPartnerAuth.signOutByRefreshToken(refreshToken)
84 | .toSingle()
85 | .subscribeOn(Schedulers.io())
86 | .observeOn(AndroidSchedulers.mainThread())
87 | .subscribe(
88 | {
89 | Log.d(LOG_TAG, "Token Revoked")
90 | partnerActivity.onTokenRevoke()
91 | },
92 | {
93 | Log.e(LOG_TAG, "RevokeToken Error", it)
94 | }
95 | ).apply {
96 | compositeDisposable.add(this)
97 | }
98 | }
99 |
100 | override fun onDestroy(owner: LifecycleOwner) {
101 | super.onDestroy(owner)
102 | compositeDisposable.clear()
103 | }
104 |
105 | private fun TinkoffCall.toSingle(): Single {
106 | return Single.fromCallable { getResponse() }
107 | .doOnDispose { cancel() }
108 | }
109 |
110 | private companion object {
111 |
112 | private const val LOG_TAG = "TokenResponse"
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/tinkoff-id/src/main/java/ru/tinkoff/core/tinkoffId/codeVerifier/CodeVerifierUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2021 Tinkoff Bank
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ru.tinkoff.core.tinkoffId.codeVerifier
18 |
19 | import android.annotation.SuppressLint
20 | import android.os.Build
21 | import android.util.Base64
22 | import androidx.annotation.RequiresApi
23 | import java.io.UnsupportedEncodingException
24 | import java.security.MessageDigest
25 | import java.security.NoSuchAlgorithmException
26 | import java.security.SecureRandom
27 |
28 | /**
29 | * Generates code verifiers and challenges for PKCE exchange.
30 | *
31 | * @see "Proof Key for Code Exchange by OAuth Public Clients"
32 | * @author Stanislav Mukhametshin
33 | */
34 | internal object CodeVerifierUtil {
35 |
36 | /**
37 | * SHA-256 based code verifier challenge method.
38 | *
39 | * @see "Proof Key for Code Exchange by OAuth Public Clients"
40 | */
41 | const val CODE_CHALLENGE_METHOD_S256 = "S256"
42 |
43 | /**
44 | * Plain-text code verifier challenge method. This is only used by AppAuth for Android if
45 | * SHA-256 is not supported on this platform.
46 | *
47 | * @see "Proof Key for Code Exchange by OAuth Public Clients"
48 | */
49 | const val CODE_CHALLENGE_METHOD_PLAIN = "plain"
50 |
51 | /**
52 | * The default entropy (in bytes) used for the code verifier.
53 | */
54 | private const val DEFAULT_CODE_VERIFIER_ENTROPY = 64
55 |
56 | /**
57 | * Base64 encoding settings used for generated code verifiers.
58 | */
59 | private const val PKCE_BASE64_ENCODE_SETTINGS =
60 | Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE
61 |
62 | /**
63 | * Generates a random code verifier string using the provided entropy source and the specified
64 | * number of bytes of entropy.
65 | */
66 | @SuppressLint("TrulyRandom")
67 | @RequiresApi(Build.VERSION_CODES.M)
68 | fun generateRandomCodeVerifier(): String {
69 | val entropySource = SecureRandom()
70 | val randomBytes = ByteArray(DEFAULT_CODE_VERIFIER_ENTROPY)
71 | entropySource.nextBytes(randomBytes)
72 | return Base64.encodeToString(randomBytes, PKCE_BASE64_ENCODE_SETTINGS)
73 | }
74 |
75 | /**
76 | * Produces a challenge from a code verifier, using SHA-256 as the challenge method if the
77 | * system supports it (all Android devices _should_ support SHA-256), and falls back
78 | * to the ["plain" challenge type][CODE_CHALLENGE_METHOD_PLAIN] if
79 | * unavailable.
80 | */
81 | fun deriveCodeVerifierChallenge(codeVerifier: String): String {
82 | return try {
83 | val sha256Digester = MessageDigest.getInstance("SHA-256")
84 | sha256Digester.update(codeVerifier.toByteArray(charset("ISO_8859_1")))
85 | val digestBytes = sha256Digester.digest()
86 | Base64.encodeToString(digestBytes, PKCE_BASE64_ENCODE_SETTINGS)
87 | } catch (e: NoSuchAlgorithmException) {
88 | codeVerifier
89 | } catch (e: UnsupportedEncodingException) {
90 | throw IllegalStateException("ISO-8859-1 encoding not supported", e)
91 | }
92 | }
93 |
94 | /**
95 | * Returns the challenge method utilized on this system: typically
96 | * [SHA-256][CODE_CHALLENGE_METHOD_S256] if supported by
97 | * the system, [plain][CODE_CHALLENGE_METHOD_PLAIN] otherwise.
98 | */
99 | // no exception, so SHA-256 is supported
100 | fun getCodeVerifierChallengeMethod(): String {
101 | return try {
102 | MessageDigest.getInstance("SHA-256")
103 | CODE_CHALLENGE_METHOD_S256
104 | } catch (e: NoSuchAlgorithmException) {
105 | CODE_CHALLENGE_METHOD_PLAIN
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | xmlns:android
37 |
38 | ^$
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | xmlns:.*
48 |
49 | ^$
50 |
51 |
52 | BY_NAME
53 |
54 |
55 |
56 |
57 |
58 |
59 | .*:id
60 |
61 | http://schemas.android.com/apk/res/android
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | .*:name
71 |
72 | http://schemas.android.com/apk/res/android
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | name
82 |
83 | ^$
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | style
93 |
94 | ^$
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | .*
104 |
105 | ^$
106 |
107 |
108 | BY_NAME
109 |
110 |
111 |
112 |
113 |
114 |
115 | .*
116 |
117 | http://schemas.android.com/apk/res/android
118 |
119 |
120 | ANDROID_ATTRIBUTE_ORDER
121 |
122 |
123 |
124 |
125 |
126 |
127 | .*
128 |
129 | .*
130 |
131 |
132 | BY_NAME
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/tinkoff-id/src/test/java/ru/tinkoff/core/tinkoffId/TinkoffPartnerApiServiceTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2021 Tinkoff Bank
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ru.tinkoff.core.tinkoffId
18 |
19 | import android.os.Build
20 | import com.google.common.truth.Truth.assertThat
21 | import okhttp3.OkHttpClient
22 | import okhttp3.mockwebserver.MockResponse
23 | import okhttp3.mockwebserver.MockWebServer
24 | import org.junit.After
25 | import org.junit.Assert.assertThrows
26 | import org.junit.Before
27 | import org.junit.Test
28 | import org.junit.runner.RunWith
29 | import org.robolectric.RobolectricTestRunner
30 | import org.robolectric.annotation.Config
31 | import ru.tinkoff.core.tinkoffId.api.TinkoffIdApi
32 | import ru.tinkoff.core.tinkoffId.error.TinkoffRequestException
33 | import ru.tinkoff.core.tinkoffId.error.TinkoffTokenErrorConstants
34 | import java.net.HttpURLConnection
35 |
36 | /**
37 | * @author Stanislav Mukhametshin
38 | */
39 | @Config(sdk = [Build.VERSION_CODES.P])
40 | @RunWith(RobolectricTestRunner::class)
41 | public class TinkoffPartnerApiServiceTest {
42 |
43 | private lateinit var mockWebServer: MockWebServer
44 | private lateinit var tinkoffPartnerApiService: TinkoffPartnerApiService
45 |
46 | @Before
47 | public fun setUp() {
48 | mockWebServer = MockWebServer()
49 | mockWebServer.start()
50 | val tinkoffIdApi = TinkoffIdApi(OkHttpClient(), mockWebServer.url("/"))
51 | tinkoffPartnerApiService = TinkoffPartnerApiService(tinkoffIdApi)
52 | }
53 |
54 | @Test
55 | public fun testValidTokenGet() {
56 | val call = tinkoffPartnerApiService.getToken("test", "test", "test", "test")
57 | testTokenRequestResponsesValidation(call)
58 | }
59 |
60 | @Test
61 | public fun testInvalidTokenGet() {
62 | val response = MockResponse()
63 | .setResponseCode(HttpURLConnection.HTTP_BAD_REQUEST)
64 | .setBody(
65 | """{
66 | "error": "invalid_request",
67 | "error_message": "Some message"
68 | }""".trimIndent()
69 | )
70 | mockWebServer.enqueue(response)
71 | val call = tinkoffPartnerApiService.getToken("test", "test", "test", "test")
72 | val exception = assertThrows(TinkoffRequestException::class.java) { call.getResponse() }
73 | assertThat(exception.errorMessage).isNotNull()
74 | val message = requireNotNull(exception.errorMessage)
75 | assertThat(message.errorType).isEqualTo(TinkoffTokenErrorConstants.INVALID_REQUEST)
76 | assertThat(message.message).isEqualTo("Some message")
77 | }
78 |
79 | @Test
80 | public fun testValidTokenUpdate() {
81 | val call = tinkoffPartnerApiService.refreshToken("refresh_token", "test")
82 | testTokenRequestResponsesValidation(call)
83 | }
84 |
85 | @Test
86 | public fun testRevokeAccessToken() {
87 | testTokenRevokeValidation(tinkoffPartnerApiService.revokeAccessToken("test", "test"))
88 | }
89 |
90 | @Test
91 | public fun revokeRefreshToken() {
92 | testTokenRevokeValidation(tinkoffPartnerApiService.revokeRefreshToken("test", "test"))
93 | }
94 |
95 | private fun testTokenRequestResponsesValidation(call: TinkoffCall) {
96 | val response = MockResponse()
97 | .setResponseCode(HttpURLConnection.HTTP_OK)
98 | .setBody(TOKEN_JSON)
99 | mockWebServer.enqueue(response)
100 | val payload = call.getResponse()
101 | assertThat(payload.accessToken).isEqualTo(ACCESS_TOKEN)
102 | assertThat(payload.expiresIn).isEqualTo(EXPIRES_IN)
103 | assertThat(payload.idToken).isEqualTo(ID_TOKEN)
104 | assertThat(payload.refreshToken).isEqualTo(REFRESH_TOKEN)
105 | }
106 |
107 | private fun testTokenRevokeValidation(call: TinkoffCall) {
108 | val response = MockResponse()
109 | .setResponseCode(HttpURLConnection.HTTP_OK)
110 | .setBody("OK")
111 | mockWebServer.enqueue(response)
112 | assertThat(call.getResponse()).isEqualTo(Unit)
113 | }
114 |
115 | @After
116 | public fun tearDown() {
117 | mockWebServer.shutdown()
118 | }
119 |
120 | private companion object {
121 |
122 | private const val ACCESS_TOKEN = "DR_Y7iifsfsdfRKGuXtMovTocYD4MnA7RxhwAMX3ydRKeDOYFls4a1S4IC1Daq7poz2k2AoJIOICsgA"
123 | private const val EXPIRES_IN = 1834
124 | private const val ID_TOKEN = "yJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ"
125 | private const val REFRESH_TOKEN = "OuyFBEMG2yhjh0KHNIbeS8sb0VmBmdqd08rY52ZniOyDmCnn"
126 |
127 | private val TOKEN_JSON = """{
128 | "access_token": "$ACCESS_TOKEN",
129 | "token_type": "Bearer",
130 | "expires_in": $EXPIRES_IN,
131 | "id_token": "$ID_TOKEN",
132 | "refresh_token": "$REFRESH_TOKEN"
133 | }""".trimIndent()
134 | }
135 | }
--------------------------------------------------------------------------------
/app-demo/src/main/java/ru/tinkoff/core/app_demo_partner/PartnerActivity.kt:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.core.app_demo_partner
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.view.View
7 | import android.widget.Button
8 | import android.widget.EditText
9 | import android.widget.Toast
10 | import androidx.appcompat.app.AppCompatActivity
11 | import ru.tinkoff.core.tinkoffId.TinkoffIdAuth
12 | import ru.tinkoff.core.tinkoffId.ui.TinkoffIdSignInButton
13 | import kotlin.LazyThreadSafetyMode.NONE
14 |
15 | class PartnerActivity : AppCompatActivity() {
16 |
17 | private val callbackUrl: Uri = Uri.Builder()
18 | .scheme("https")
19 | .authority("www.partner.com")
20 | .appendPath("partner")
21 | .build()
22 |
23 | private val redirectUri = "mobile://"
24 | private val clientId = "test-partner-mobile"
25 |
26 | private lateinit var tinkoffPartnerAuth: TinkoffIdAuth
27 |
28 | private val partnerPresenter by lazy(NONE) { PartnerPresenter(tinkoffPartnerAuth, this) }
29 | private val clientIdEditText by lazy(NONE) { findViewById(R.id.etClientId) }
30 | private val redirectUriEditText by lazy(NONE) { findViewById(R.id.etRedirectUri) }
31 |
32 | private val reset by lazy(NONE) { findViewById