Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
eb83095
tests: fix env var name
Oran-Dan Aug 17, 2026
b7f9b54
build(i18n): sync `.typesafe-i18n.json` schema with installed 5.27.1
coplat Aug 20, 2026
43adbab
feat: add ShieldCheckRegularIcon
coplat Aug 20, 2026
d030b16
feat: add mock fixtures to test accept-connection prompt
coplat Aug 20, 2026
5147850
feat: show if previously interacted, and verified domain
coplat Aug 20, 2026
f8f7415
Merge branch 'dev' into feat/connection-page
coplat Aug 21, 2026
c8359cb
nit: redirectPath accidentally removed
coplat Aug 21, 2026
e3ec487
feat: add certifications section with list sub-route
coplat Aug 21, 2026
c858a83
feat: render individual certifications
coplat Aug 24, 2026
8857c9f
fix: remove unneeded logo_uri from linked_vps.
coplat Aug 24, 2026
0630f78
refactor: realign data model
coplat Aug 24, 2026
986f87d
feat: interactions counter and summarized certifications tile
coplat Aug 24, 2026
fc78173
fix: cert overview
coplat Aug 25, 2026
acaaf9f
fix: tolerate a prompt without linked_vps or ecosystems
coplat Aug 25, 2026
f74e513
feat: add accept connection screen before every interaction
Oran-Dan Aug 25, 2026
0aa9acf
chore: update structs
Oran-Dan Aug 25, 2026
6352727
fix: optionality
coplat Aug 25, 2026
0fab87e
Merge remote-tracking branch 'origin/feat/add_connection_before_conse…
coplat Aug 25, 2026
74724e9
feat: regenerate ts types
coplat Aug 25, 2026
7368a0e
feat: update linked VPs
nanderstabel Aug 25, 2026
2a64f5a
Merge remote-tracking branch 'origin/feat/linked-vps' into feat/conne…
coplat Aug 25, 2026
8f53f53
chore: add comment
Oran-Dan Aug 25, 2026
51ff503
feat: align with real LinkedVerfiableCredentialData types
coplat Aug 25, 2026
5b3874c
fix: rename `Connected` to `Known Connection`
coplat Aug 25, 2026
16de4cf
chore: check against host not full connection url
Oran-Dan Aug 25, 2026
3e98a4c
feat: prettier dark mode
coplat Aug 25, 2026
6c37d0a
Merge remote-tracking branch 'origin/feat/add_connection_before_conse…
coplat Aug 25, 2026
b48ade9
fix: use relative date times for interaction fields
coplat Aug 25, 2026
9a5f014
fix: format relative datetime
coplat Aug 25, 2026
fedfa93
chore: improve connection url parsing
Oran-Dan Aug 25, 2026
adfa3b6
Merge branch 'fix/add-vc-sd-jwt' into tmp/jelle-build
nanderstabel Aug 25, 2026
7e33b02
Merge remote-tracking branch 'origin/fix/add-vc-sd-jwt' into feat/con…
coplat Aug 26, 2026
071c2b9
feat: better logos without padding
coplat Aug 26, 2026
0f8c0b0
fix: no error when logo download fails
Oran-Dan Aug 26, 2026
f13ab79
chore: bun fmt prettier
coplat Aug 26, 2026
28d0f92
chore: fix client_id parsing
Oran-Dan Aug 26, 2026
3d56806
Merge branch 'feat/connection-page' into backend/connection-page
Oran-Dan Aug 26, 2026
9d5fb75
Merge branch 'feat/connection-page' into tmp/jelle-build
nanderstabel Aug 26, 2026
78fe73f
feat: display credential name and logo
nanderstabel Aug 27, 2026
f1c1542
chore: fix did matching for connections
Oran-Dan Aug 27, 2026
670f588
Merge branch 'feat/connection-page' into backend/connection-page
Oran-Dan Aug 27, 2026
612233c
chore: clean up
nanderstabel Aug 27, 2026
ff2930f
fix: remove unused component, minor comment addition
coplat Aug 28, 2026
339bbbf
Merge branch 'backend/connection-page' of https://github.com/impierce…
coplat Aug 28, 2026
0f3632c
chore: clean code
nanderstabel Aug 28, 2026
93ea03a
fix: derive mock mode from the selected fixture
coplat Aug 28, 2026
8212bae
Merge remote-tracking branch 'origin/feat/connection-page' into feat/…
coplat Aug 28, 2026
2d717b7
cargo fmt
coplat Aug 31, 2026
7c6a761
bun format
coplat Aug 31, 2026
81702f8
Merge branch 'feat/connection-page' into backend/connection-page
Oran-Dan Sep 1, 2026
16eb5c9
chore: update docs, naming, PR comments
Oran-Dan Sep 2, 2026
55b4748
chore: fix arguments
Oran-Dan Sep 2, 2026
6143fd4
fix: new contract
coplat Sep 3, 2026
2d81b6c
chore: fix client_metadata fetching
Oran-Dan Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion identity-wallet/bindings/connections/Connection.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export interface Connection { id: string, name: string, url: string, did?: string, verified: boolean, first_interacted: string, last_interacted: string, }
export interface Connection { id: string, name: string, url: string, did: string, verified: boolean, first_interacted: string, last_interacted: string, }
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import type { CredentialMetadata } from "./CredentialMetadata";
import type { CredentialStatus } from "./CredentialStatus";
import type { DisplayClaim } from "./DisplayClaim";

export interface DisplayCredential { id: string, format: { format: string }, issuer_name: string, data: any, display_claims: Array<DisplayClaim>, metadata: CredentialMetadata, connection_id?: string, display_name: string, credential_status?: CredentialStatus, public_link?: string, }
export interface DisplayCredential { id: string, format: { format: string }, issuer_name: string, issuer_logo_uri: string | null, data: any, display_claims: Array<DisplayClaim>, metadata: CredentialMetadata, connection_id?: string, display_name: string, credential_status?: CredentialStatus, public_link?: string, }
3 changes: 3 additions & 0 deletions identity-wallet/bindings/user_prompt/ClientMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export interface ClientMetadata { client_name: string, logo_uri: string | null, connection_url: string, redirect_uri: string | null, client_id: string, }
4 changes: 4 additions & 0 deletions identity-wallet/bindings/user_prompt/ConnectionData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HistoryEvent } from "../history/HistoryEvent";

export interface ConnectionData { first_interacted_at: string, last_interacted_at: string, interactions: Array<HistoryEvent>, }
5 changes: 4 additions & 1 deletion identity-wallet/bindings/user_prompt/CurrentUserPrompt.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ClientMetadata } from "./ClientMetadata";
import type { ConnectionData } from "./ConnectionData";
import type { EcosystemProfile } from "./EcosystemProfile";
import type { LinkedVerifiableCredentialData } from "./LinkedVerifiableCredentialData";
import type { ValidationResult } from "./ValidationResult";

export type CurrentUserPrompt = { "type": "redirect", target: string, } | { "type": "password-required" } | { "type": "accept-connection", client_name: string, logo_uri?: string, redirect_uri: string, previously_connected: boolean, domain_validation: ValidationResult, linked_verifiable_presentations: Array<LinkedVerifiableCredentialData>, } | { "type": "credential-offer", issuer_name: string, logo_uri?: string, credential_configurations: Record<string, any>, tx_code?: { input_mode?: 'numeric' | 'text'; length?: number }, } | { "type": "share-credentials", client_name: string, logo_uri?: string, options: Array<string>, is_interactive: boolean, };
export type CurrentUserPrompt = { "type": "redirect", target: string, } | { "type": "password-required" } | { "type": "accept-connection", client_metadata: ClientMetadata, connection_data?: ConnectionData, domain_validation: ValidationResult, linked_verifiable_presentations?: Array<LinkedVerifiableCredentialData>, ecosystems?: Array<EcosystemProfile>, } | { "type": "credential-offer", issuer_name: string, logo_uri?: string, credential_configurations: Record<string, any>, tx_code?: { input_mode?: 'numeric' | 'text'; length?: number }, } | { "type": "share-credentials", client_name: string, logo_uri?: string, options: Array<string>, is_interactive: boolean, };
4 changes: 4 additions & 0 deletions identity-wallet/bindings/user_prompt/EcosystemProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Member } from "./Member";

export interface EcosystemProfile { logo_uri: string | null, name: string, description: string | null, ecosystem_leader: Member, member_count: number, members: Array<Member>, }
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DisplayCredential } from "../credentials/DisplayCredential";
import type { ValidationResult } from "./ValidationResult";

export interface LinkedVerifiableCredentialData { name: string | null, logo_uri: string | null, issuance_date: string, }
export interface LinkedVerifiableCredentialData { credential: DisplayCredential, issuer_domain_validations: Array<ValidationResult>, }
3 changes: 3 additions & 0 deletions identity-wallet/bindings/user_prompt/Member.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export interface Member { logo_uri: string | null, name: string, description: string | null, domain: string, }
2 changes: 1 addition & 1 deletion identity-wallet/bindings/user_prompt/ValidationResult.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ValidationStatus } from "./ValidationStatus";

export interface ValidationResult { status: ValidationStatus, name?: string, logo_uri?: string, issuance_date?: string, message?: string, }
export interface ValidationResult { status: ValidationStatus, url: string, name?: string, logo_uri?: string, issuance_date?: string, message?: string, }
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use crate::{
state::{
actions::ActionTrait,
connections::reducers::handle_siopv2_authorization_request::handle_siopv2_authorization_request,
profile_settings::reducers::update_sorting_preference::sort_connections, Reducer,
profile_settings::reducers::update_sorting_preference::sort_connections,
qr_code::reducers::read_authorization_request::read_oid4vp_authorization_request,
qr_code::reducers::read_credential_offer::read_credential_offer, Reducer,
},
};

Expand All @@ -13,11 +15,14 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ConnectionAccepted;

// The first 3 reducers are executed in an OR/OR/OR manner, matching against the active flow, which is set after the QrCodeScanned action.
#[typetag::serde(name = "[Authenticate] Connection accepted")]
impl ActionTrait for ConnectionAccepted {
fn reducers<'a>(&self) -> Vec<Reducer<'a>> {
vec![
reducer!(handle_siopv2_authorization_request),
reducer!(read_oid4vp_authorization_request),
reducer!(read_credential_offer),
reducer!(sort_connections),
]
}
Expand Down
66 changes: 31 additions & 35 deletions identity-wallet/src/state/connections/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pub mod reducers;

use super::{core_utils::DateUtils, FeatTrait};

use identity_iota::did::CoreDID;
use identity_iota::did::{CoreDID, DID};
use log::info;
use serde::{Deserialize, Serialize};
use std::ops::Not;
Expand All @@ -18,16 +18,14 @@ impl Connections {
Self(Vec::new())
}

pub fn contains(&self, url: &str, name: &str) -> bool {
self.0
.iter()
.any(|connection| connection.url == url && connection.name == name)
pub fn contains(&self, did: &str) -> bool {
self.0.iter().any(|connection| connection.did == did)
}

/// Inserts a new connection into the list of connections.
/// Modelled after the `std::collections::HashMap::insert` method.
fn insert(&mut self, connection: Connection) -> Option<&Connection> {
self.contains(&connection.url, &connection.name)
self.contains(&connection.did)
.not()
.then(|| {
self.0.push(connection);
Expand All @@ -38,31 +36,23 @@ impl Connections {

/// Returns a mutable reference to the connection with the given `url` and `name`.
/// Modelled after the `std::collections::HashMap::get_mut` method.
fn get_mut(&mut self, url: &str, name: &str) -> Option<&mut Connection> {
self.0
.iter_mut()
.find(|connection| connection.url == url && connection.name == name)
fn get_mut(&mut self, did: &str) -> Option<&mut Connection> {
self.0.iter_mut().find(|connection| connection.did == did)
}

/// Inserts a new connection into the list of connections if it does not already exist. If it does exist, updates
/// the last interaction time and returns a reference to the connection.
pub fn update_or_insert(&mut self, url: &str, name: &str, did: Option<CoreDID>) -> &Connection {
if self.contains(url, name) {
pub fn update_or_insert(&mut self, url: &str, name: &str, did: CoreDID) -> &Connection {
if self.contains(did.as_str()) {
info!("Updating existing connection: {name} {url}");
self.get_mut(url, name).map(|connection| {
if let Some(core_did) = did {
connection.did = Some(core_did.to_string());
}
self.get_mut(did.as_str()).map(|connection| {
connection.did = did.to_string();
connection.update_last_interaction_time();
&*connection
})
} else {
info!("Inserting new connection: {name} {url}");
self.insert(Connection::new(
name.to_string(),
url.to_string(),
did.map(|d| d.to_string()),
))
self.insert(Connection::new(name.to_string(), url.to_string(), did.to_string()))
}
.expect("Failed to update or insert connection")
}
Expand All @@ -84,15 +74,14 @@ pub struct Connection {
pub id: String,
pub name: String,
pub url: String,
#[ts(optional)]
pub did: Option<String>,
pub did: String,
pub verified: bool,
pub first_interacted: String,
pub last_interacted: String,
}

impl Connection {
pub fn new(name: String, url: String, did: Option<String>) -> Self {
pub fn new(name: String, url: String, did: String) -> Self {
// TODO(ngdil): Temporary solution to support NGDIL demo, replace with different unique identifier to distinguish connection
let id = sha256::digest([name.as_bytes(), url.as_bytes()].concat()).to_string();
let current_datetime = DateUtils::new_date_string();
Expand Down Expand Up @@ -122,21 +111,26 @@ impl PartialEq for Connection {

#[cfg(test)]
mod tests {
use std::str::FromStr;

use identity_iota::did::DID;

use super::*;

#[test]
fn test_update_or_insert() {
let mut connections = Connections::new();
let url = "https://example.com";
let name = "Example";
let connection = connections.update_or_insert(url, name, None);
let did = CoreDID::from_str("did:example:123").unwrap();
let connection = connections.update_or_insert(url, name, did.clone());
assert_eq!(connection.url, url);
assert_eq!(connection.name, name);
assert_eq!(connection.first_interacted, connection.last_interacted);
assert_eq!(connections.0.len(), 1);
assert!(connections.contains(url, name));
assert!(connections.contains(did.as_str()));

let connection = connections.update_or_insert(url, name, None);
let connection = connections.update_or_insert(url, name, did.clone());
assert_eq!(connection.url, url);
assert_eq!(connection.name, name);
// The last interaction time should have been updated.
Expand All @@ -147,44 +141,46 @@ mod tests {
#[test]
fn test_update_or_insert_with_duplicate_names() {
let mut connections = Connections::new();
let did = CoreDID::from_str("did:example:123").unwrap();
let url = "https://example.com";
let name = "Example";
let connection = connections.update_or_insert(url, name, None);
let connection = connections.update_or_insert(url, name, did.clone());
assert_eq!(connection.url, url);
assert_eq!(connection.name, name);
assert_eq!(connection.first_interacted, connection.last_interacted);
assert_eq!(connections.0.len(), 1);
assert!(connections.contains(url, name));
assert!(connections.contains(did.as_str()));

// A different server with the same name is treated as a different connection.
let url = "https://example2.com";
let connection = connections.update_or_insert(url, name, None);
let connection = connections.update_or_insert(url, name, did.clone());
assert_eq!(connection.url, url);
assert_eq!(connection.name, name);
assert_eq!(connection.first_interacted, connection.last_interacted);
assert_eq!(connections.0.len(), 2);
assert!(connections.contains(url, name));
assert!(connections.contains(did.as_str()));
}

#[test]
fn test_update_or_insert_with_duplicate_urls() {
let mut connections = Connections::new();
let did = CoreDID::from_str("did:example:123").unwrap();
let url = "https://example.com";
let name = "Example";
let connection = connections.update_or_insert(url, name, None);
let connection = connections.update_or_insert(url, name, did.clone());
assert_eq!(connection.url, url);
assert_eq!(connection.name, name);
assert_eq!(connection.first_interacted, connection.last_interacted);
assert_eq!(connections.0.len(), 1);
assert!(connections.contains(url, name));
assert!(connections.contains(did.as_str()));

// The same server is used with a different name.
let name = "Example2";
let connection = connections.update_or_insert(url, name, None);
let connection = connections.update_or_insert(url, name, did.clone());
assert_eq!(connection.url, url);
assert_eq!(connection.name, name);
assert_eq!(connection.first_interacted, connection.last_interacted);
assert_eq!(connections.0.len(), 2);
assert!(connections.contains(url, name));
assert!(connections.contains(did.as_str()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,26 @@ use crate::{
},
};

use identity_iota::did::CoreDID;
use log::{debug, info, warn};
use oid4vc::oid4vc_core::{
authorization_request::{AuthorizationRequest, Object},
client_metadata::ClientMetadataResource,
};
use oid4vc::siopv2::siopv2::SIOPv2;
use log::{debug, info};

// Sends the authorization response.
/// Handles the `ConnectionAccepted` action for the SIOPv2 active flow, triggered by accepting `AcceptConnection` prompt and persists the connection.
/// Sends the SIOPv2 authorization response.
#[tracing::instrument(skip_all, err)]
pub async fn handle_siopv2_authorization_request(state: AppState, _action: Action) -> Result<AppState, AppError> {
let siopv2_authorization_request = match state.core_utils.active_flow.clone() {
Some(ActiveFlow::Siopv2 { authorization_request }) => authorization_request,
// Not a SIOPv2 flow, let other reducers handle this action.
_ => return Ok(state),
};

let client_metadata = match &state.current_user_prompt {
Some(CurrentUserPrompt::AcceptConnection { client_metadata, .. }) => client_metadata.clone(),
_ => return Err(Error(
"Unexpected state: No CurrentUserPrompt::AcceptConnection found when reading SIOPv2 authorization request"
.to_string(),
)),
};

let state_guard = state.core_utils.managers.lock().await;

let provider_manager = &state_guard
Expand All @@ -31,15 +40,7 @@ pub async fn handle_siopv2_authorization_request(state: AppState, _action: Actio
.ok_or(MissingManagerError("identity"))?
.provider_manager;

let siopv2_authorization_request = match state.core_utils.active_flow.clone() {
Some(ActiveFlow::Siopv2 { authorization_request }) => authorization_request,
_ => return Err(AppError::Error("Expected SIOPv2 authorization request".to_string())),
};

info!(
"Generating SIOPv2 authorization response for client: {}",
siopv2_authorization_request.body.client_id
);
info!("generating response");

let response = provider_manager
.generate_response(&*siopv2_authorization_request, Default::default())
Expand All @@ -49,28 +50,21 @@ pub async fn handle_siopv2_authorization_request(state: AppState, _action: Actio

#[cfg(not(feature = "test_utils"))]
if provider_manager.send_response(&response).await.is_err() {
warn!("Failed to send SIOPv2 authorization response to redirect_uri");
log::warn!("Failed to send SIOPv2 authorization response to redirect_uri");
return Err(SendAuthorizationResponseError);
}
info!("SIOPv2 response successfully sent");

let (client_name, logo_uri, connection_url, client_id) =
get_siopv2_client_name_and_logo_uri(&siopv2_authorization_request);

if logo_uri.is_some() {
warn!("Skipping download of client logo as it should have already been downloaded in `read_authorization_request()` and be present in /assets/tmp folder");
}

let did = CoreDID::parse(client_id).ok();

let mut connections = state.connections;
let connection = connections.update_or_insert(&connection_url, &client_name, did);
let connection = connections.update_or_insert(
&client_metadata.connection_url,
&client_metadata.client_name,
client_metadata.client_id,
);

let file_name = match logo_uri {
Some(logo_uri) => hash(logo_uri.as_str()),
None => "_".to_string(),
};
persist_asset(&file_name, &connection.id).ok();
if let Some(logo_uri) = client_metadata.logo_uri {
persist_asset(&hash(logo_uri.as_str()), &connection.id).ok();
}

// History
let mut history = state.history;
Expand All @@ -92,33 +86,3 @@ pub async fn handle_siopv2_authorization_request(state: AppState, _action: Actio
..state
})
}

// Helper

// TODO: move this functionality to the oid4vc-manager crate.
/// Returns (client_name, logo_uri, connection_url, client_id)
pub fn get_siopv2_client_name_and_logo_uri(
siopv2_authorization_request: &AuthorizationRequest<Object<SIOPv2>>,
) -> (String, Option<String>, String, String) {
// Get the connection url from the redirect url host (or use the redirect url if it does not
// contain a host).
let redirect_uri = siopv2_authorization_request.body.uri.uri().clone();
let connection_url = redirect_uri.host_str().unwrap_or(redirect_uri.as_str());

let client_id = siopv2_authorization_request.body.client_id.clone();

// Get the client_name and logo_uri from the client_metadata if it exists.
match &siopv2_authorization_request.body.extension.client_metadata {
ClientMetadataResource::ClientMetadata {
client_name, logo_uri, ..
} => {
let client_name = client_name.as_ref().cloned().unwrap_or(connection_url.to_string());
let logo_uri = logo_uri.as_ref().map(|logo_uri| logo_uri.to_string());
Some((client_name, logo_uri, connection_url.to_string(), client_id.clone()))
}
// TODO: support `client_metadata_uri`
ClientMetadataResource::ClientMetadataUri(_) => None,
}
// Otherwise use the connection_url as the client_name.
.unwrap_or((connection_url.to_string(), None, connection_url.to_string(), client_id))
}
1 change: 1 addition & 0 deletions identity-wallet/src/state/core_utils/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ impl CredentialType {
Ok(())
}
_ => {
// TODO: make use of `app_handle.path().data_dir()` to make this work on mobile.
let json_schema_path = format!("resources/jsonschemas/{version}.json");

validate_credential_against_schema(json_schema_path, data)?;
Expand Down
Loading
Loading