❗ Important! Before you proceed, please read the EUDI Wallet Reference Implementation project description
- Overview
- Requirements
- Installation
- How to use
- Theming and rebranding
- Error handling
- Sample app
- How to contribute
- License
This module provides core and UI functionality for the EUDI Wallet, focused on the Remote Qualified Electronic Signature (RQES) service. RQES allows users to sign documents using a qualified certificate held remotely by a Qualified Trust Service Provider (QTSP).
The SDK supports two flows:
- Local file flow — the host app already has a local document (PDF) selected and asks the SDK to sign it.
- Remote URL flow — the host app receives a remote URL (typically via a deep link or QR code) which the SDK resolves to a document and then signs.
It ships ready-made Jetpack Compose screens for QTSP selection, certificate selection, document preview, and a success/sharing screen, plus the orchestration around OAuth authorization, credential authorization, signing, and sharing the signed document.
You configure the SDK at compile time via the EudiRQESUiConfig interface and drive
it at runtime through the EudiRQESUi object.
- Options selection screen — pick the QTSP and signing certificate.
- View document screen — preview the original or signed PDF.
- Success screen — confirm the signed document and share it.
sequenceDiagram
participant Host as Host app
participant SDK as EudiRQESUi
participant Browser as System browser
participant QTSP
Host->>SDK: setup(application, config)
Host->>SDK: initiate(documentUri | remoteUri)
SDK-->>Host: launches signing UI
SDK->>Browser: opens OAuth authorization URL
Browser->>QTSP: user authenticates
QTSP-->>Host: deep link with authorization code (via browser redirect)
Host->>SDK: resume(authorizationCode)
SDK->>QTSP: sign document
QTSP-->>SDK: signed document
SDK-->>Host: Success screen
- Android 10 (API level 29) or higher
- Java 17 (the SDK is compiled with
JvmTarget.JVM_17) - A Kotlin host app is recommended — the public API uses Kotlin
value classtypes (DocumentUri,RemoteUri). The SDK ships its own Jetpack Compose UI, so your app does not need to use Compose itself.
The library is published on Maven Central. Add the dependency to your app's
build.gradle.kts:
dependencies {
implementation("eu.europa.ec.eudi:eudi-lib-android-rqes-ui:$version")
}Replace $version with the latest released version from
Maven Central
or the GitHub releases page.
Implement EudiRQESUiConfig to supply runtime configuration to the SDK.
| Property | Required | Default | Purpose |
|---|---|---|---|
qtsps |
Yes | — | List of Qualified Trust Service Providers the user can pick from. |
documentRetrievalConfig |
Yes | — | Certificate-trust policy for resolving remote documents (required even for local-only flows). |
translations |
No | English defaults built into the SDK | Override or extend localized strings. |
themeManager |
No | Built-in light/dark theme | Customize colors and typography (see Theming). |
printLogs |
No | false |
Enable Timber debug logging. |
Example (mirrors the :test-app reference implementation):
class RQESConfigImpl(val context: Context) : EudiRQESUiConfig {
override val qtsps: List<QtspData>
get() = listOf(
QtspData(
// Human-readable name shown in the QTSP picker.
name = "Wallet-Centric",
// QTSP CSC v2 endpoint.
endpoint = "https://walletcentric.signer.eudiw.dev/csc/v2".toUriOrEmpty(),
// Optional Timestamp Authority URL.
tsaUrl = "https://timestamp.sectigo.com/qualified",
// OAuth2 client credentials issued by the QTSP.
clientId = "wallet-client-tester",
clientSecret = "somesecrettester2",
// Deep link the QTSP redirects to after authorization.
// Must match the host-app intent filter (see section 3).
authFlowRedirectionURI = URI.create("rqes://oauth/callback"),
hashAlgorithm = HashAlgorithmOID.SHA_256,
)
)
override val documentRetrievalConfig: DocumentRetrievalConfig
get() = DocumentRetrievalConfig.X509Certificates(
context = context,
certificates = listOf(R.raw.my_certificate),
shouldLog = BuildConfig.DEBUG,
)
// Optional — overrides for individual localized strings.
override val translations: Map<String, Map<LocalizableKey, String>>
get() = mapOf(
"en" to mapOf(LocalizableKey.View to "VIEW")
)
override val printLogs: Boolean
get() = BuildConfig.DEBUG
}You can supply multiple QtspData entries; the user selects one in the options screen.
documentRetrievalConfig is required even if you only use the local-file flow, although
it only takes effect when resolving remote documents. Besides
DocumentRetrievalConfig.X509Certificates (shown above — trusts certificates bundled as
raw resources), you can pass DocumentRetrievalConfig.X509CertificateImpl with your own
X509CertificateTrust, or DocumentRetrievalConfig.NoValidation to skip certificate
validation (useful for local-only integrations or testing).
Call setup() once, typically from your Application.onCreate(). This wires the
SDK's Koin modules and applies your config.
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
EudiRQESUi.setup(
application = this,
config = RQESConfigImpl(this),
// Optional: if your host app already uses Koin, pass it here so the
// SDK adds its modules to your application instead of starting its own.
koinApplication = null,
)
}
}The SDK relies on the system browser for OAuth and (optionally) a deep link for same-device document retrieval. Register the corresponding intent filters on the Activity that should receive them.
This must match the authFlowRedirectionURI set in QtspData. For
URI.create("rqes://oauth/callback"):
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- scheme is the literal scheme without "://" -->
<data
android:scheme="rqes"
android:host="oauth"
android:path="/callback" />
</intent-filter>Alternatively, you can use Android App Links.
Only needed if you support the remote-URL flow. The host app receives the remote
URL via this deep link and passes it to EudiRQESUi.initiate(remoteUri = ...).
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="your_scheme"
android:host="your_host" />
</intent-filter>The Activity that hosts the deep-link filters needs two things, otherwise the return from OAuth will not work:
android:launchMode="singleTask"in the manifest so the existing instance receives the deep link instead of launching a new one.- An
onNewIntentoverride that extracts thecodequery parameter from the OAuth callback and forwards it toEudiRQESUi.resume(...).
class HostActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkIntent(intent)
// ... your normal setContent { } here
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
checkIntent(intent)
}
private fun checkIntent(intent: Intent) {
val code = intent.data?.getQueryParameter("code")
if (code != null) {
EudiRQESUi.resume(
context = this,
authorizationCode = code,
)
}
}
}Start the signing process with the URI of a local PDF (for example, one returned
by ActivityResultContracts.OpenDocument).
EudiRQESUi.initiate(
context = activity,
documentUri = DocumentUri(fileUri),
)Start the signing process with a remote URL (for example, extracted from a QR code or the document-retrieval deep link above).
EudiRQESUi.initiate(
context = activity,
remoteUri = RemoteUri(remoteUri),
)In both cases, the SDK opens its own Activity (EudiRQESContainer) and drives the
flow until either the success screen completes or the user cancels.
The SDK renders its screens with its own Material 3 Compose theme — your host app's theme is not applied to them. It automatically follows the system light/dark setting and shows the matching color set (Material You dynamic color is not used).
You can rebrand the SDK along three independent axes, all wired through your
EudiRQESUiConfig:
| What you change | How | Types / resources |
|---|---|---|
| Colors & typography | Override themeManager |
ThemeManager.Builder, ThemeColorsTemplate, ThemeTypographyTemplate |
| Header logo | Shadow the SDK drawable in your app module | ic_logo_icon_and_text |
| Text & labels | Override translations |
LocalizableKey (see Configuration) |
The public theming types live under
eu.europa.ec.eudi.rqesui.infrastructure.theme(and itstemplates/templates.structuressub-packages). The SDK's built-in palette and typography areinternal, so you build your own templates rather than tweaking the defaults.
Override the themeManager property and assemble a ThemeManager with the builder.
Colors are passed as a ThemeColorsTemplate (one for light, optionally one for dark);
fonts and text styles as a ThemeTypographyTemplate.
override val themeManager: ThemeManager
get() = ThemeManager.Builder()
.withLightColors(brandLightColors)
.withDarkColors(brandDarkColors) // optional — light colors are reused if omitted
.withTypography(brandTypography)
.build()Colors. Every Material 3 role is required (there are no per-field defaults). Values
are ARGB Long literals, e.g. 0xFF2A5FD9:
private val brandLightColors = ThemeColorsTemplate(
primary = 0xFF2A5FD9,
onPrimary = 0xFFFFFFFF,
primaryContainer = 0xFFEADDFF,
onPrimaryContainer = 0xFF21005D,
secondary = 0xFFD6D9F9,
// … provide every remaining role: the secondary/tertiary/error families,
// background, surface and the surface*/surfaceContainer* slots, outline, scrim,
// the inverse* roles and the *Fixed accent roles.
)
// private val brandDarkColors = ThemeColorsTemplate( … )See ThemeColorsTemplate for the full list of roles to fill in.
Typography. All 15 text styles must be supplied. Each ThemeTextStyle field is
optional and falls back to Compose defaults when omitted. Bundle your font files under
res/font and reference them via ThemeFont:
private val brandFont = ThemeFont(
res = R.font.my_brand_font,
weight = ThemeFontWeight.W400,
style = ThemeFontStyle.Normal,
)
private val brandTypography = ThemeTypographyTemplate(
headlineSmall = ThemeTextStyle(
fontFamily = listOf(brandFont),
fontSize = 24,
lineHeight = 32,
letterSpacing = 0f,
textAlign = ThemeTextAlign.Start,
),
// … provide the remaining styles: display{Large,Medium,Small},
// headline{Large,Medium}, title{Large,Medium,Small},
// body{Large,Medium,Small} and label{Large,Medium,Small}.
)A few internal accent colors (success, warning, divider) are not part of
ThemeColorsTemplateand cannot currently be themed.
The header/success area shows a single logo drawable, ic_logo_icon_and_text, which
combines the mark and the wordmark. It is not exposed through config — rebrand it by
shadowing the resource: declare a drawable with the same name in your app module and
it overrides the SDK's at resource-merge time.
your-app/src/main/res/drawable/ic_logo_icon_and_text.xml
Within the drawable, the wordmark paths use ?colorOnSurface, so the text adapts to
light/dark automatically, while the mark carries its own brand colors. Optionally
override the content_description_logo_icon_and_text string for accessibility.
All user-facing copy is overridden through translations (shown in
Configuration): map any LocalizableKey to your own wording. For
keys that take runtime arguments, keep the @arg placeholder (the ARGUMENTS_SEPARATOR
token) where the value should be inserted — for example
LocalizableKey.Certificate to "Cert @arg".
The SDK exposes errors via EudiRQESUiError, a subclass of Exception. Methods
that can throw it are annotated with @Throws(EudiRQESUiError::class):
EudiRQESUi.initiate(...)— thrown if the document URI's filename cannot be extracted or if the remote URI is malformed.EudiRQESUi.resume(...)— thrown if the SDK was not initiated first (no active signing session — i.e.initiate(...)was never called), or if the suppliedcontextis not an Activity.
Failures that happen inside the SDK's UI (network errors during OAuth, certificate listing, signing, etc.) are surfaced on the in-SDK error screen and do not propagate out to the host app.
The :test-app module is a complete, runnable host application that
demonstrates every integration point: a configured EudiRQESUiConfig, deep-link
registration, Activity wiring, and both local-file and remote-URL flows. It is
the canonical reference if anything in this README is ambiguous.
We welcome contributions to this project. To ensure that the process is smooth for everyone involved, follow the guidelines found in CONTRIBUTING.md.
Copyright (c) 2026 European Commission
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.