diff --git a/.changeset/afraid-readers-swim.md b/.changeset/afraid-readers-swim.md
new file mode 100644
index 000000000..f7828b608
--- /dev/null
+++ b/.changeset/afraid-readers-swim.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Add ProvideTransactionGenericContext Task
diff --git a/.changeset/afraid-readers-swimmer.md b/.changeset/afraid-readers-swimmer.md
new file mode 100644
index 000000000..817d38c8a
--- /dev/null
+++ b/.changeset/afraid-readers-swimmer.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/context-module": patch
+---
+
+Add ProvideTransactionGenericContext Task
diff --git a/tmp_changeset/calm-months-live.md b/.changeset/calm-months-live.md
similarity index 56%
rename from tmp_changeset/calm-months-live.md
rename to .changeset/calm-months-live.md
index 9cf9e7d6a..07cd31d49 100644
--- a/tmp_changeset/calm-months-live.md
+++ b/.changeset/calm-months-live.md
@@ -1,5 +1,5 @@
---
-"@ledgerhq/keyring-btc": minor
+"@ledgerhq/device-signer-kit-btc": minor
---
Implement MerkleTree and MerkleMap services
diff --git a/.changeset/clever-badgers-pull.md b/.changeset/clever-badgers-pull.md
new file mode 100644
index 000000000..f2ee79b41
--- /dev/null
+++ b/.changeset/clever-badgers-pull.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-management-kit-sample": minor
+---
+
+Add GetAddress Solana Signer use case
diff --git a/.changeset/config.json b/.changeset/config.json
index c720bb119..d792eda2d 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -12,5 +12,11 @@
"access": "public",
"baseBranch": "develop",
"updateInternalDependencies": "patch",
- "ignore": ["@ledgerhq/keyring-btc"]
+ "ignore": [
+ "@ledgerhq/ledger-dmk-docs",
+ "@ledgerhq/device-management-kit-sample",
+ "@ledgerhq/device-signer-kit-btc",
+ "@ledgerhq/device-signer-kit-ethereum",
+ "@ledgerhq/context-module"
+ ]
}
diff --git a/.changeset/cool-dancers-coin.md b/.changeset/cool-dancers-coin.md
new file mode 100644
index 000000000..3aa3fa81d
--- /dev/null
+++ b/.changeset/cool-dancers-coin.md
@@ -0,0 +1,6 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+"@ledgerhq/context-module": patch
+---
+
+Update license to Apache-2.0
diff --git a/.changeset/cuddly-ducks-confessio0n.md b/.changeset/cuddly-ducks-confessio0n.md
new file mode 100644
index 000000000..d618d83d8
--- /dev/null
+++ b/.changeset/cuddly-ducks-confessio0n.md
@@ -0,0 +1,6 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+"@ledgerhq/device-management-kit-sample": patch
+---
+
+Rename keyring to signer
diff --git a/tmp_changeset/cuddly-impalas-sing.md b/.changeset/cuddly-impalas-sing.md
similarity index 53%
rename from tmp_changeset/cuddly-impalas-sing.md
rename to .changeset/cuddly-impalas-sing.md
index 9bbf2168c..26244e39b 100644
--- a/tmp_changeset/cuddly-impalas-sing.md
+++ b/.changeset/cuddly-impalas-sing.md
@@ -1,5 +1,5 @@
---
-"@ledgerhq/keyring-btc": minor
+"@ledgerhq/device-signer-kit-btc": minor
---
Implement GetExtendedPublicKeyCommand
diff --git a/.changeset/eleven-spies-explainer.md b/.changeset/eleven-spies-explainer.md
new file mode 100644
index 000000000..1ba22779b
--- /dev/null
+++ b/.changeset/eleven-spies-explainer.md
@@ -0,0 +1,6 @@
+---
+"@ledgerhq/context-module": patch
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Use esbuild to build libraries
diff --git a/.changeset/fuzzy-cups-refuse.md b/.changeset/fuzzy-cups-refuse.md
new file mode 100644
index 000000000..a098a0a4f
--- /dev/null
+++ b/.changeset/fuzzy-cups-refuse.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Adapt SignTransactionCommand and add StoreTransaction to prepare for the new version of Ethereum app (1.13)
diff --git a/.changeset/great-paws-sell.md b/.changeset/great-paws-sell.md
new file mode 100644
index 000000000..33f798c32
--- /dev/null
+++ b/.changeset/great-paws-sell.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Add support for EIP712 filters with missing token
diff --git a/.changeset/hip-ducks-study.md b/.changeset/hip-ducks-study.md
new file mode 100644
index 000000000..a0e629db4
--- /dev/null
+++ b/.changeset/hip-ducks-study.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Fix clear signing of EIP712 messages with an empty array
diff --git a/.changeset/lemon-impalas-flash.md b/.changeset/lemon-impalas-flash.md
new file mode 100644
index 000000000..01d169da6
--- /dev/null
+++ b/.changeset/lemon-impalas-flash.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Allow signing a message as a byte array
diff --git a/.changeset/lemon-suits-noticer.md b/.changeset/lemon-suits-noticer.md
new file mode 100644
index 000000000..f39ba06c0
--- /dev/null
+++ b/.changeset/lemon-suits-noticer.md
@@ -0,0 +1,6 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+"@ledgerhq/device-management-kit-sample": patch
+---
+
+Rename SDK to DMK
diff --git a/.changeset/loud-balloons-poke.md b/.changeset/loud-balloons-poke.md
new file mode 100644
index 000000000..2dd25db3e
--- /dev/null
+++ b/.changeset/loud-balloons-poke.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Add support for V1 clear signing contexts
diff --git a/tmp_changeset/lucky-keys-explode.md b/.changeset/lucky-keys-explode.md
similarity index 53%
rename from tmp_changeset/lucky-keys-explode.md
rename to .changeset/lucky-keys-explode.md
index fb482bd0c..92b949c54 100644
--- a/tmp_changeset/lucky-keys-explode.md
+++ b/.changeset/lucky-keys-explode.md
@@ -1,5 +1,5 @@
---
-"@ledgerhq/keyring-btc": minor
+"@ledgerhq/device-signer-kit-btc": minor
---
Implement GetMasterFingerprintCommand
diff --git a/.changeset/metal-wombats-relate.md b/.changeset/metal-wombats-relate.md
new file mode 100644
index 000000000..3b6d9423c
--- /dev/null
+++ b/.changeset/metal-wombats-relate.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Reconstruct V full value for legacy transactions
diff --git a/.changeset/mighty-vans-kiss.md b/.changeset/mighty-vans-kiss.md
new file mode 100644
index 000000000..de801c9ec
--- /dev/null
+++ b/.changeset/mighty-vans-kiss.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-btc": patch
+---
+
+Implement wallet policy service
diff --git a/.changeset/nasty-islands-pretend.md b/.changeset/nasty-islands-pretend.md
new file mode 100644
index 000000000..c010f0090
--- /dev/null
+++ b/.changeset/nasty-islands-pretend.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-management-kit-sample": minor
+---
+
+Add solana getAppConfiguration use case
diff --git a/.changeset/nine-tools-bow.md b/.changeset/nine-tools-bow.md
new file mode 100644
index 000000000..1ef12d18d
--- /dev/null
+++ b/.changeset/nine-tools-bow.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-management-kit-sample": patch
+---
+
+Implement basic Flipper client for the Ledger Device Management Kit
diff --git a/.changeset/perfect-deers-sneeze.md b/.changeset/perfect-deers-sneeze.md
new file mode 100644
index 000000000..7cb606bab
--- /dev/null
+++ b/.changeset/perfect-deers-sneeze.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-btc": patch
+---
+
+Rename packages
diff --git a/.changeset/plenty-snakes-agree.md b/.changeset/plenty-snakes-agree.md
new file mode 100644
index 000000000..1fafd2cf5
--- /dev/null
+++ b/.changeset/plenty-snakes-agree.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Add ProvideTransactionInformation command
diff --git a/.changeset/slow-lies-float.md b/.changeset/slow-lies-float.md
new file mode 100644
index 000000000..402e83483
--- /dev/null
+++ b/.changeset/slow-lies-float.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Add ProvideTransactionFieldDescription command
diff --git a/.changeset/smart-games-brush.md b/.changeset/smart-games-brush.md
new file mode 100644
index 000000000..d3461c64d
--- /dev/null
+++ b/.changeset/smart-games-brush.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-management-kit-sample": patch
+---
+
+Add mockserver integration with transport
diff --git a/.changeset/spicy-toes-ring.md b/.changeset/spicy-toes-ring.md
new file mode 100644
index 000000000..ae5535879
--- /dev/null
+++ b/.changeset/spicy-toes-ring.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-management-kit-sample": patch
+---
+
+Add signTransaction usecase for Solana signer
diff --git a/.changeset/strange-mayflies-repair.md b/.changeset/strange-mayflies-repair.md
new file mode 100644
index 000000000..535652c80
--- /dev/null
+++ b/.changeset/strange-mayflies-repair.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/context-module": patch
+---
+
+Fix CAL test signatures for EIP712
diff --git a/.changeset/sweet-stingrays-give.md b/.changeset/sweet-stingrays-give.md
new file mode 100644
index 000000000..08ef915a2
--- /dev/null
+++ b/.changeset/sweet-stingrays-give.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Prevent chunking legacy transactions just before the EIP-155 marker
diff --git a/.changeset/tall-hairs-cheer.md b/.changeset/tall-hairs-cheer.md
new file mode 100644
index 000000000..d9f6dbaf7
--- /dev/null
+++ b/.changeset/tall-hairs-cheer.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-btc": minor
+---
+
+Create device-signer-kit-btc package
diff --git a/.changeset/tasty-falcons-doubts.md b/.changeset/tasty-falcons-doubts.md
new file mode 100644
index 000000000..b27d56d5a
--- /dev/null
+++ b/.changeset/tasty-falcons-doubts.md
@@ -0,0 +1,7 @@
+---
+"@ledgerhq/context-module": patch
+"@ledgerhq/device-signer-kit-ethereum": patch
+"@ledgerhq/device-management-kit-sample": patch
+---
+
+Use type keyword when importing type
diff --git a/.changeset/ten-carrots-wait.md b/.changeset/ten-carrots-wait.md
new file mode 100644
index 000000000..70ae7dd59
--- /dev/null
+++ b/.changeset/ten-carrots-wait.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Remove legacy parameter for internal sign transacion command
diff --git a/.changeset/tiny-hornets-grin.md b/.changeset/tiny-hornets-grin.md
new file mode 100644
index 000000000..28450ba89
--- /dev/null
+++ b/.changeset/tiny-hornets-grin.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-management-kit-sample": patch
+---
+
+Add unlock timeout input in open app device action
diff --git a/.changeset/tiny-otters-draw.md b/.changeset/tiny-otters-draw.md
new file mode 100644
index 000000000..06299e3f7
--- /dev/null
+++ b/.changeset/tiny-otters-draw.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-management-kit-sample": minor
+---
+
+Add keyring eth provider
diff --git a/.changeset/twelve-snakes-agreeing.md b/.changeset/twelve-snakes-agreeing.md
new file mode 100644
index 000000000..0ac9b33eb
--- /dev/null
+++ b/.changeset/twelve-snakes-agreeing.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-management-kit-sample": patch
+---
+
+New use case listenToKnownDevices
diff --git a/.changeset/twelve-stingrays-shake.md b/.changeset/twelve-stingrays-shake.md
new file mode 100644
index 000000000..403ba5994
--- /dev/null
+++ b/.changeset/twelve-stingrays-shake.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Early return when EIP712 Domain fails to be sent
diff --git a/tmp_changeset/weak-ads-chew.md b/.changeset/weak-ads-chew.md
similarity index 55%
rename from tmp_changeset/weak-ads-chew.md
rename to .changeset/weak-ads-chew.md
index d41b3070f..0a4e2d756 100644
--- a/tmp_changeset/weak-ads-chew.md
+++ b/.changeset/weak-ads-chew.md
@@ -1,5 +1,5 @@
---
-"@ledgerhq/keyring-btc": patch
+"@ledgerhq/device-signer-kit-btc": patch
---
Implement PSBT parser and mapper services
diff --git a/.changeset/witty-boats-lay.md b/.changeset/witty-boats-lay.md
new file mode 100644
index 000000000..7d0bb6138
--- /dev/null
+++ b/.changeset/witty-boats-lay.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/device-signer-kit-ethereum": patch
+---
+
+Add ProvideEnum command
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 7b51cc8f3..35977fda7 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -52,22 +52,5 @@ updates:
typescript:
patterns:
- "typescript*"
- - "@types*"
exclude-patterns:
- - "typescript-eslint"
-
- # Sample App dependencies for pnpm
- - package-ecosystem: "npm"
- directory: "/apps/sample"
- schedule:
- interval: "weekly"
- day: "sunday"
- timezone: "Europe/Paris"
- labels:
- - "dependencies"
- reviewers:
- - "LedgerHQ/live-devices"
- commit-message:
- prefix: "âŦī¸ (sample) [NO-ISSUE]: "
- ignore:
- - dependency-name: "typescript"
\ No newline at end of file
+ - "typescript-eslint"
\ No newline at end of file
diff --git a/.github/workflows/generate_sbom.yml b/.github/workflows/generate_sbom.yml
index cfd48aab1..78cd8a011 100644
--- a/.github/workflows/generate_sbom.yml
+++ b/.github/workflows/generate_sbom.yml
@@ -15,4 +15,4 @@ jobs:
- uses: LedgerHQ/device-sdk-ts/.github/actions/setup-toolchain-composite@develop
- - uses: ./.github/actions/generate-sbom-composite
+ - uses: LedgerHQ/device-sdk-ts/.github/actions/generate-sbom-composite@develop
diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml
new file mode 100644
index 000000000..35f13c7ca
--- /dev/null
+++ b/.github/workflows/merge_queue.yml
@@ -0,0 +1,28 @@
+name: Merge Queue Checks
+on:
+ merge_group:
+
+env:
+ FORCE_COLOR: "1"
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref_name != 'develop' && github.ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ checks:
+ name: Run health check and unit tests
+ runs-on: ledgerhq-device-sdk
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: LedgerHQ/device-sdk-ts/.github/actions/setup-toolchain-composite@develop
+
+ - name: Health check
+ id: health-check
+ run: pnpm health-check
+
+ - name: Tests
+ id: unit-tests
+ if: ${{ steps.health-check.conclusion == 'success' }}
+ run: pnpm test -- -- --maxWorkers=50%
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index a6d6fd476..04293c013 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -2,6 +2,8 @@ name: Pull Request Checks
on:
pull_request:
types: [opened, synchronize, reopened, edited]
+ branches-ignore:
+ - main
env:
FORCE_COLOR: "1"
@@ -49,10 +51,10 @@ jobs:
- name: Tests
id: unit-tests
if: ${{ steps.health-check.conclusion == 'success' }}
- run: pnpm test:coverage -- --max-warnings=0
+ run: pnpm test:coverage -- --maxWorkers=50%
- - uses: sonarsource/sonarqube-scan-action@v3
- if: ${{ steps.unit-tests.conclusion == 'success' && github.actor != 'dependabot[bot]' && github.event.pull_request.head.repo.fork == 'false' }}
+ - uses: sonarsource/sonarqube-scan-action@v4
+ if: ${{ steps.unit-tests.conclusion == 'success' && github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork }}
env:
- SONAR_TOKEN: ${{ secrets.GREEN_SONAR_TOKEN }}
- SONAR_HOST_URL: ${{ secrets.GREEN_SONAR_HOST_URL }}
+ SONAR_TOKEN: ${{ secrets.PUBLIC_GREEN_SONAR_TOKEN }}
+ SONAR_HOST_URL: ${{ secrets.PUBLIC_SONAR_HOST_URL }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5b00b6999..674622fc1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -46,9 +46,8 @@ jobs:
- name: Publish
id: changesets
- uses: changesets/action@v1
+ uses: ledgerhq/changeset-action-ledger@main
with:
- version: echo "not running version"
publish: pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/snapshot_release.yml b/.github/workflows/snapshot_release.yml
index 67c89ac30..269386700 100644
--- a/.github/workflows/snapshot_release.yml
+++ b/.github/workflows/snapshot_release.yml
@@ -30,9 +30,6 @@ jobs:
- uses: LedgerHQ/device-sdk-ts/.github/actions/setup-toolchain-composite@develop
- - name: install dependencies
- run: pnpm install
-
- name: build libraries
run: pnpm build
diff --git a/.github/workflows/update_toolchain.yml b/.github/workflows/update_toolchain.yml
new file mode 100644
index 000000000..8ae5d842f
--- /dev/null
+++ b/.github/workflows/update_toolchain.yml
@@ -0,0 +1,70 @@
+name: Update toolchain
+
+on:
+ schedule:
+ - cron: "0 0 * * 0"
+ workflow_dispatch:
+
+env:
+ BRANCH_NAME: chore/update-toolchain
+
+jobs:
+ update-toolchain:
+ runs-on: ["ledgerhq-device-sdk"]
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: LedgerHQ/device-sdk-ts/.github/actions/setup-toolchain-composite@develop
+
+ - name: Setup git and branch
+ run: |
+ git config user.name 'github-actions[bot]'
+ git config user.email 'github-actions[bot]@users.noreply.github.com'
+ git checkout -b ${{ env.BRANCH_NAME }}
+
+ - name: Update toolchain
+ run: |
+ proto outdated --update
+
+ - name: Check for changes
+ id: changes
+ run: |
+ echo "status=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT
+
+ - name: Set new versions
+ if: steps.changes.outputs.status > 0
+ id: new-versions
+ run: |
+ proto use
+ pnpm i
+ echo "version=$(pnpm -v)" >> $GITHUB_OUTPUT
+
+ - name: Update package.json
+ if: steps.changes.outputs.status > 0
+ run: |
+ jq '.packageManager = "pnpm@${{ steps.new-versions.outputs.version }}"' package.json > tmp.json && mv tmp.json package.json
+
+ - name: Health check
+ if: steps.changes.outputs.status > 0
+ id: health-check
+ run: pnpm health-check
+
+ - name: Tests
+ id: unit-tests
+ if: steps.changes.outputs.status > 0 && success()
+ run: pnpm test:coverage -- --maxWorkers=50%
+
+ - name: Create PR
+ if: steps.changes.outputs.status > 0 && success()
+ run: |
+ git add .prototools
+ git add package.json
+ git commit -m "đ§ (repo) [NO-ISSUE]: Update toolchain"
+ git push origin ${{ env.BRANCH_NAME }}
+ gh pr create \
+ --title "đ§ (repo) [NO-ISSUE]: Update toolchain" \
+ --body "This PR updates the toolchain (node, npm, pnpm) to the newest versions" \
+ --base develop \
+ --head ${{ env.BRANCH_NAME }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml
index e44813323..0ca1a6a35 100644
--- a/.github/workflows/version.yml
+++ b/.github/workflows/version.yml
@@ -15,17 +15,14 @@ jobs:
- uses: LedgerHQ/device-sdk-ts/.github/actions/setup-toolchain-composite@develop
- - name: install dependencies
- run: pnpm install
-
- name: build libraries
run: pnpm build:libs
- - name: create release pull request or publish
+ - name: create release pull request
uses: changesets/action@v1
with:
version: pnpm bump
- commit: "đ (release): versioning packages"
- title: "đ (release) [NO-ISSUE]: versioning packages"
+ commit: "đ (release): Versioning packages"
+ title: "đ (release) [NO-ISSUE]: Versioning packages"
env:
GITHUB_TOKEN: ${{ github.token }}
diff --git a/.prototools b/.prototools
index 8961f47a9..9cb538e05 100644
--- a/.prototools
+++ b/.prototools
@@ -1,3 +1,3 @@
-node = "20.17.0"
-npm = "10.8.2"
-pnpm = "9.10.0"
+node = "20.18.0"
+npm = "10.9.0"
+pnpm = "9.13.2"
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 000000000..7b8623d8e
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,9 @@
+# How does it work? => https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
+# This is a comment.
+# Each line is a file pattern followed by one or more owners.
+# Order is important; the last matching pattern takes the most
+# precedence. When someone opens a pull request that only
+# modifies JS files, only @js-owner and not the global
+# owner(s) will be requested for a review.
+
+* @ledgerhq/live-devices
diff --git a/README.md b/README.md
index b90d78b08..152bd5c12 100644
--- a/README.md
+++ b/README.md
@@ -4,9 +4,6 @@
-
@@ -29,6 +26,10 @@
+
+
+
+
@@ -57,11 +58,11 @@ The purpose of the Ledger Device Management Kit(LDMK in short) is to provide a l
## How does it works
-The Device Management Kit features an interface for applications to handle any Ledge device (a.k. hardware wallets). It convert intention into
+The Device Management Kit features an interface for applications to handle any Ledger device (a.k. hardware wallets). It convert intention into
```mermaid
flowchart LR;
- application(Application) <--API--> DSDK(DeviceSDK) <--USB/BLE--> device(Device);
+ application(Application) <--API--> LDMK(LedgerDeviceManagementKit) <--USB/BLE--> device(Device);
```
The Device Management Kit is available in 3 different environments (web, Android & iOS).
@@ -72,7 +73,7 @@ This repository is dedicated to **web environment** and is written in TypeScript
### Repository
-The Device Management Kit is structured as a monorepository whose prupose is to centralise all the TypeScript code related to the SDK in one place.
+The Device Management Kit is structured as a monorepository whose prupose is to centralise all the TypeScript code related to the Device Management Kit in one place.
This project uses [turbo monorepo](https://turbo.build/repo/docs) to build and release different packages on NPM registry and a sample demo application on Vercel.
@@ -80,16 +81,16 @@ This project uses [turbo monorepo](https://turbo.build/repo/docs) to build and r
A brief description of this project packages:
-| Name | Path | Description |
-| --------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
-| @ledgerhq/device-sdk-sample | apps/sample | React Next web app used to test & demonstrate the Web Device Management Kit |
-| @ledgerhq/eslint-config-dsdk | packages/config/eslint | internal package which contains eslint shared config. Used by `extends: ["@ledgerhq/dsdk"]` in `.eslintrc`. |
-| @ledgerhq/jest-config-dsdk | packages/config/jest | internal package which contains jest shared config. Used by `preset: "@ledgerhq/jest-config-dsdk"` in `jest.config.ts` |
-| @ledgerhq/tsconfig-dsdk | packages/config/typescript | internal package which contains typescript shared config. Used by `"extends": "@ledgerhq/tsconfig-dsdk/sdk"` in `tsconfig.json` |
-| @ledgerhq/device-management-kit | packages/core | external package that contains the core of the Web SDK |
-| @ledgerhq/device-sdk-signer | packages/signer | external package that contains device coin application dedicated handlers |
-| @ledgerhq/device-sdk-trusted-apps | packages/trusted-apps | external package that contains device trusted application dedicated handlers |
-| @ledgerhq/device-sdk-ui | packages/ui | external package |
+| Name | Path | Description |
+|----------------------------------------|--------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|
+| @ledgerhq/device-management-kit-sample | apps/sample | React Next web app used to test & demonstrate the Web Device Management Kit |
+| @ledgerhq/eslint-config-dsdk | packages/config/eslint | internal package which contains eslint shared config. Used by `extends: ["@ledgerhq/dsdk"]` in `.eslintrc`. |
+| @ledgerhq/jest-config-dsdk | packages/config/jest | internal package which contains jest shared config. Used by `preset: "@ledgerhq/jest-config-dsdk"` in `jest.config.ts` |
+| @ledgerhq/tsconfig-dsdk | packages/config/typescript | internal package which contains typescript shared config. Used by `"extends": "@ledgerhq/tsconfig-dsdk/tsconfig.sdk"` in `tsconfig.json` |
+| @ledgerhq/device-management-kit | packages/device-management-kit | external package that contains the core of the Web Device Management Kit |
+| @ledgerhq/device-signer-kit-ethereum | packages/signer/signer-eth | external package that contains device ethereum coin application dedicated handlers |
+| @ledgerhq/device-signer-kit-solana | packages/signer/signer-solana | external package that contains device solana coin application dedicated handlers |
+| @ledgerhq/device-management-kit-flipper-plugin-client | packages/flipper-plugin-client | external package that contains [flipper](https://github.com/facebook/flipper) logger for Device Management Kit |
# Getting started
@@ -182,7 +183,7 @@ Each package is built using the following command (at the root of the monorepo).
Device Management Kit main module.
```bash
-pnpm core build
+pnpm dmk build
```
### Signer
@@ -258,7 +259,7 @@ Finally, we should add a script in the correct `package.json` as a shortcut to t
eg:
```
-pnpm core module:create
+pnpm dmk module:create
```
Under the hood, the script looks like this:
@@ -301,4 +302,4 @@ Each individual project may include its own specific guidelines, located within
# License
-Please check each project [`LICENSE`](https://github.com/LedgerHQ/device-sdk-ts/blob/develop/LICENSE.md) file, most of them are under the `MIT` license.
+Please check each project [`LICENSE`](https://github.com/LedgerHQ/device-sdk-ts/blob/develop/LICENSE.md) file, most of them are under the `Apache-2.0` license.
diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore
new file mode 100644
index 000000000..2077bd84c
--- /dev/null
+++ b/apps/docs/.gitignore
@@ -0,0 +1,42 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# Sentry Config File
+.sentryclirc
+
+# Playwright test results
+test-results/
diff --git a/apps/docs/.prettierignore b/apps/docs/.prettierignore
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/core/.prettierrc.js b/apps/docs/.prettierrc.js
similarity index 100%
rename from packages/core/.prettierrc.js
rename to apps/docs/.prettierrc.js
diff --git a/apps/docs/eslint.config.mjs b/apps/docs/eslint.config.mjs
new file mode 100644
index 000000000..6f7e467ff
--- /dev/null
+++ b/apps/docs/eslint.config.mjs
@@ -0,0 +1,32 @@
+import baseConfig from "@ledgerhq/eslint-config-dsdk";
+import globals from "globals";
+
+export default [
+ ...baseConfig,
+ {
+ ignores: [".next"],
+ },
+ {
+ languageOptions: {
+ parserOptions: {
+ project: "./tsconfig.eslint.json",
+ },
+ },
+ },
+ {
+ files: [
+ "next.config.js",
+ "postcss.config.js",
+ "tailwind.config.js",
+ "theme.config.tsx",
+ ],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ rules: {
+ "@typescript-eslint/no-var-requires": "off",
+ },
+ },
+];
diff --git a/apps/docs/next.config.js b/apps/docs/next.config.js
new file mode 100644
index 000000000..22f367e90
--- /dev/null
+++ b/apps/docs/next.config.js
@@ -0,0 +1,8 @@
+// eslint-disable-next-line @typescript-eslint/no-require-imports
+const withNextra = require("nextra")({
+ defaultShowCopyCode: true,
+ theme: "nextra-theme-docs",
+ themeConfig: "./theme.config.tsx",
+});
+
+module.exports = withNextra();
diff --git a/apps/docs/package.json b/apps/docs/package.json
new file mode 100644
index 000000000..b1dc832a1
--- /dev/null
+++ b/apps/docs/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@ledgerhq/ledger-dmk-docs",
+ "version": "1.0.0",
+ "description": "",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "eslint",
+ "lint:fix": "eslint --fix",
+ "prettier": "prettier . --check",
+ "prettier:fix": "prettier . --write",
+ "typecheck": "tsc --noEmit"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "autoprefixer": "^10.4.17",
+ "next": "^14.1.0",
+ "nextra": "^2.13.3",
+ "nextra-theme-docs": "^2.13.3",
+ "postcss": "^8.4.35",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "tailwindcss": "^3.4.1"
+ },
+ "devDependencies": {
+ "@ledgerhq/eslint-config-dsdk": "workspace:*",
+ "@ledgerhq/prettier-config-dsdk": "workspace:*",
+ "@ledgerhq/tsconfig-dsdk": "workspace:*",
+ "@types/node": "^22.7.5",
+ "@types/react": "^18.3.11",
+ "globals": "15.11.0"
+ }
+}
diff --git a/apps/docs/pages/_app.mdx b/apps/docs/pages/_app.mdx
new file mode 100644
index 000000000..d412129bd
--- /dev/null
+++ b/apps/docs/pages/_app.mdx
@@ -0,0 +1,5 @@
+import "../style.css";
+
+export default function App({ Component, pageProps }) {
+ return ;
+}
diff --git a/apps/docs/pages/_meta.json b/apps/docs/pages/_meta.json
new file mode 100644
index 000000000..eaa43e4fd
--- /dev/null
+++ b/apps/docs/pages/_meta.json
@@ -0,0 +1,17 @@
+{
+ "index": {
+ "title": "Home",
+ "theme": {
+ "layout": "raw"
+ },
+ "display": "hidden"
+ },
+ "docs": {
+ "title": "Documentation",
+ "type": "page"
+ },
+ "references": {
+ "title": "References",
+ "type": "page"
+ }
+}
diff --git a/apps/docs/pages/docs/_meta.json b/apps/docs/pages/docs/_meta.json
new file mode 100644
index 000000000..db267d3ea
--- /dev/null
+++ b/apps/docs/pages/docs/_meta.json
@@ -0,0 +1,8 @@
+{
+ "docs": "Ledger Device Management Kits",
+ "explanations": "Explanations",
+ "begginers": "Begginer's guide",
+ "integration_walkthroughs": "Integration Walkthrough",
+ "migrations": "Migrations",
+ "references": "References (TSDoc)"
+}
diff --git a/apps/docs/pages/docs/begginers.mdx b/apps/docs/pages/docs/begginers.mdx
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/docs/pages/docs/begginers/_meta.json b/apps/docs/pages/docs/begginers/_meta.json
new file mode 100644
index 000000000..0ca1e3447
--- /dev/null
+++ b/apps/docs/pages/docs/begginers/_meta.json
@@ -0,0 +1,6 @@
+{
+ "setup": "Setup",
+ "init_dmk": "Initialize Device Management Kit",
+ "discover_and_connect": "Discover and connect",
+ "exchange_data": "Exchange data with the device"
+}
diff --git a/apps/docs/pages/docs/begginers/discover_and_connect.mdx b/apps/docs/pages/docs/begginers/discover_and_connect.mdx
new file mode 100644
index 000000000..06036d4bb
--- /dev/null
+++ b/apps/docs/pages/docs/begginers/discover_and_connect.mdx
@@ -0,0 +1,40 @@
+# Connecting to a Device
+
+There are two steps to connecting to a device:
+
+- **Discovery**: `sdk.startDiscovering()`
+ - Returns an observable which will emit a new `DiscoveredDevice` for every scanned device.
+ - The `DiscoveredDevice` objects contain information about the device model.
+ - Use one of these values to connect to a given discovered device.
+- **Connection**: `sdk.connect({ deviceId: device.id })`
+ - Returns a Promise resolving in a device session identifier `DeviceSessionId`.
+ - **Keep this device session identifier to further interact with the device.**
+ - Then, `sdk.getConnectedDevice({ sessionId })` returns the `ConnectedDevice`, which contains information about the device model and its name.
+
+```ts
+sdk.startDiscovering().subscribe({
+ next: (device) => {
+ sdk.connect({ deviceId: device.id }).then((sessionId) => {
+ const connectedDevice = sdk.getConnectedDevice({ sessionId });
+ });
+ },
+ error: (error) => {
+ console.error(error);
+ },
+});
+```
+
+Then once a device is connected:
+
+- **Disconnection**: `sdk.disconnect({ sessionId })`
+- **Observe the device session state**: `sdk.getDeviceSessionState({ sessionId })`
+ - This will return an `Observable` to listen to the known information about the device:
+ - device status:
+ - ready to process a command
+ - busy
+ - locked
+ - disconnected
+ - device name
+ - information on the OS
+ - battery status
+ - currently opened app
diff --git a/apps/docs/pages/docs/begginers/exchange_data.mdx b/apps/docs/pages/docs/begginers/exchange_data.mdx
new file mode 100644
index 000000000..6446f7b4f
--- /dev/null
+++ b/apps/docs/pages/docs/begginers/exchange_data.mdx
@@ -0,0 +1,187 @@
+# Exchange data with the device
+
+## Sending an APDU
+
+Once you have a connected device, you can send it APDU commands.
+
+> âšī¸ It is recommended to use the [pre-defined commands](#sending-a-pre-defined-command) when possible, or [build your own command](#building-a-new-command), to avoid dealing with the APDU directly. It will make your code more reusable.
+
+```ts
+import {
+ ApduBuilder,
+ ApduParser,
+ CommandUtils,
+} from "@ledgerhq/device-management-kit";
+
+// ### 1. Building the APDU
+// Use `ApduBuilder` to easily build the APDU and add data to its data field.
+
+// Build the APDU to open the Bitcoin app
+const openAppApduArgs = {
+ cla: 0xe0,
+ ins: 0xd8,
+ p1: 0x00,
+ p2: 0x00,
+};
+const apdu = new ApduBuilder(openAppApduArgs)
+ .addAsciiStringToData("Bitcoin")
+ .build();
+
+// ### 2. Sending the APDU
+
+const apduResponse = await sdk.sendApdu({ sessionId, apdu });
+
+// ### 3. Parsing the result
+
+const parser = new ApduParser(apduResponse);
+
+if (!CommandUtils.isSuccessResponse(apduResponse)) {
+ throw new Error(
+ `Unexpected status word: ${parser.encodeToHexaString(
+ apduResponse.statusCode,
+ )}`,
+ );
+}
+```
+
+## Sending a Pre-defined Command
+
+There are some pre-defined commands that you can send to a connected device.
+
+The `sendCommand` method will take care of building the APDU, sending it to the device and returning the parsed response.
+
+> ## âī¸ Error Responses
+>
+> Most of the commands will reject with an error if the device is locked.
+> Ensure that the device is unlocked before sending commands. You can check the device session state (`sdk.getDeviceSessionState`) to know if the device is locked.
+>
+> Most of the commands will reject with an error if the response status word is not `0x9000` (success response from the device).
+
+### Open App
+
+This command will open the app with the given name. If the device is unlocked, it will not resolve/reject until the user has confirmed or denied the app opening on the device.
+
+```ts
+import { OpenAppCommand } from "@ledgerhq/device-management-kit";
+
+const command = new OpenAppCommand("Bitcoin"); // Open the Bitcoin app
+
+await sdk.sendCommand({ sessionId, command });
+```
+
+### Close App
+
+This command will close the currently opened app.
+
+```ts
+import { CloseAppCommand } from "@ledgerhq/device-management-kit";
+
+const command = new CloseAppCommand();
+
+await sdk.sendCommand({ sessionId, command });
+```
+
+### Get OS Version
+
+This command will return information about the currently installed OS on the device.
+
+> âšī¸ If you want this information you can simply get it from the device session state by observing it with `sdk.getDeviceSessionState({ sessionId })`.
+
+```ts
+import { GetOsVersionCommand } from "@ledgerhq/device-management-kit";
+
+const command = new GetOsVersionCommand();
+
+const { seVersion, mcuSephVersion, mcuBootloaderVersion } =
+ await sdk.sendCommand({ sessionId, command });
+```
+
+### Get App and Version
+
+This command will return the name and version of the currently running app on the device.
+
+> âšī¸ If you want this information you can simply get it from the device session state by observing it with `sdk.getDeviceSessionState({ sessionId })`.
+
+```ts
+import { GetAppAndVersionCommand } from "@ledgerhq/device-management-kit";
+
+const command = new GetAppAndVersionCommand();
+
+const { name, version } = await sdk.sendCommand({ sessionId, command });
+```
+
+## Sending a Pre-defined flow - Device Actions
+
+Device actions define a succession of commands to be sent to the device.
+
+They are useful for actions that require user interaction, like opening an app,
+or approving a transaction.
+
+The result of a device action execution is an observable that will emit different states of the action execution. These states contain information about the current status of the action, some intermediate values like the user action required, and the final result.
+
+### Open App Device Action
+
+```ts
+import {
+ OpenAppDeviceAction,
+ OpenAppDAState,
+} from "@ledgerhq/device-management-kit";
+
+const openAppDeviceAction = new OpenAppDeviceAction({ appName: "Bitcoin" });
+
+const { observable, cancel } = await dmk.executeDeviceAction({
+ sessionId,
+ openAppDeviceAction,
+});
+
+observable.subscribe({
+ next: (state: OpenAppDAState) => {
+ switch (state.status) {
+ case DeviceActionStatus.NotStarted:
+ console.log("Action not started yet");
+ break;
+ case DeviceActionStatus.Pending:
+ const {
+ intermediateValue: { userActionRequired },
+ } = state;
+ switch (userActionRequired) {
+ case UserActionRequiredType.None:
+ console.log("No user action required");
+ break;
+ case UserActionRequiredType.ConfirmOpenApp:
+ console.log(
+ "The user should confirm the app opening on the device",
+ );
+ break;
+ case UserActionRequiredType.UnlockDevice:
+ console.log("The user should unlock the device");
+ break;
+ default:
+ /**
+ * you should make sure that you handle all the possible user action
+ * required types by displaying them to the user.
+ */
+ throw new Exception("Unhandled user action required");
+ break;
+ }
+ console.log("Action is pending");
+ break;
+ case DeviceActionStatus.Stopped:
+ console.log("Action has been stopped");
+ break;
+ case DeviceActionStatus.Completed:
+ const { output } = state;
+ console.log("Action has been completed", output);
+ break;
+ case DeviceActionStatus.Error:
+ const { error } = state;
+ console.log("An error occurred during the action", error);
+ break;
+ }
+ },
+});
+```
+
+### Example in React
+
+Check [the sample app](https://github.com/LedgerHQ/device-sdk-ts/tree/develop/apps/sample) for an advanced example showcasing all possible usages of the Device Management Kit in a React app.
diff --git a/apps/docs/pages/docs/begginers/init_dmk.mdx b/apps/docs/pages/docs/begginers/init_dmk.mdx
new file mode 100644
index 000000000..c2e6453b6
--- /dev/null
+++ b/apps/docs/pages/docs/begginers/init_dmk.mdx
@@ -0,0 +1,23 @@
+# Setting up the SDK
+
+The core package exposes an SDK builder `DeviceSdkBuilder` which will be used to initialise the SDK with your configuration.
+
+For now it allows you to add one or more custom loggers.
+
+In the following example, we add a console logger (`.addLogger(new ConsoleLogger())`). Then we build the SDK with `.build()`.
+
+**The returned object will be the entrypoint for all your interactions with the SDK. You should keep it as a SINGLETON .**
+
+The SDK should be built only once in your application runtime so keep a reference of this object somewhere.
+
+```ts
+import {
+ ConsoleLogger,
+ DeviceSdk,
+ DeviceSdkBuilder,
+} from "@ledgerhq/device-management-kit";
+
+export const sdk = new DeviceSdkBuilder()
+ .addLogger(new ConsoleLogger())
+ .build();
+```
diff --git a/apps/docs/pages/docs/begginers/setup.mdx b/apps/docs/pages/docs/begginers/setup.mdx
new file mode 100644
index 000000000..292c2a6b5
--- /dev/null
+++ b/apps/docs/pages/docs/begginers/setup.mdx
@@ -0,0 +1,23 @@
+# Setup
+
+## Description
+
+This package contains the core of the Device Management Kit. It provides a simple interface to handle Ledger devices and features the Device Management Kit's entry points, classes, types, structures, and models.
+
+## Installation
+
+To install the dmk package, run the following command:
+
+```sh
+npm install @ledgerhq/device-management-kit
+```
+
+## Usage
+
+### Compatibility
+
+This library works in [any browser supporting the WebHID API](https://developer.mozilla.org/en-US/docs/Web/API/WebHID_API#browser_compatibility).
+
+### Pre-requisites
+
+Some of the APIs exposed return objects of type `Observable` from RxJS. Ensure you are familiar with the basics of the Observer pattern and RxJS before using this SDK. You can refer to [RxJS documentation](https://rxjs.dev/guide/overview) for more information.
diff --git a/apps/docs/pages/docs/docs.mdx b/apps/docs/pages/docs/docs.mdx
new file mode 100644
index 000000000..04abedc30
--- /dev/null
+++ b/apps/docs/pages/docs/docs.mdx
@@ -0,0 +1,34 @@
+import { Callout } from "nextra/components";
+
+# Documentation
+
+Here you will find the all the documention related to the device management kit and all the signer coming along with it.
+
+
+ This project is still in early development so we allow ourselves to make
+ breaking changes regarding the usage of the Libraries.
+
+That's why any feedback is relevant for us in order to be able to make it stable as soon as
+possible. Get in touch with us on the [Ledger Discord
+server](https://developers.ledger.com/discord/) to provide your feedbacks.
+
+You can follow the migration guidelines [here](./migrations/)
+
+
+
+## Glossary
+
+Through all the documentation we will use some acronyme that you can find the following description :
+
+- DMK: Device Management Kit
+- DSK: Device Signer Kit
+
+## Libraries
+
+Here you can found a summary of all the libraries that are composing the DMK
+
+| Library | NPM | Version |
+| ---------------------- | ---------------------------------------------------------------------------------------------------------- | ------- |
+| Device Management Kit | [@LedgerHQ/device-mangement-kit](https://www.npmjs.com/package/@ledgerhq/device-management-kit) | 0.4.0 |
+| Device Signer Ethereum | [@LedgerHQ/device-signer-kit-ethereum](https://www.npmjs.com/package/@ledgerhq/device-signer-kit-ethereum) | 1.0.0 |
+| Context Module | [@LedgerHQ/context-module](https://www.npmjs.com/package/@ledgerhq/context-module) | 1.0.0 |
diff --git a/apps/docs/pages/docs/explanations.mdx b/apps/docs/pages/docs/explanations.mdx
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/docs/pages/docs/explanations/_meta.json b/apps/docs/pages/docs/explanations/_meta.json
new file mode 100644
index 000000000..047f56a7e
--- /dev/null
+++ b/apps/docs/pages/docs/explanations/_meta.json
@@ -0,0 +1,6 @@
+{
+ "introduction": "Why the Device Management Kit?",
+ "ledgerjs": "Differences with LedgerJS",
+ "dmk": "Device Management Kit",
+ "signers": "Signer kits"
+}
diff --git a/apps/docs/pages/docs/explanations/dmk.mdx b/apps/docs/pages/docs/explanations/dmk.mdx
new file mode 100644
index 000000000..620db4379
--- /dev/null
+++ b/apps/docs/pages/docs/explanations/dmk.mdx
@@ -0,0 +1,91 @@
+import { Callout } from "nextra/components";
+
+# Device Management Kit
+
+The device management kit is the entry point for all the other libraries related to.
+As we wanted to make the project modular.
+
+## Main Features
+
+- Discovering and connecting to Ledger devices via USB, through [WebHID](https://developer.mozilla.org/en-US/docs/Web/API/WebHID_API).
+- Discovering and connecting to Ledger devices via BLE, through [WebBLE](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API).
+- Observing the state of a connected device.
+- Sending custom APDU commands to Ledger devices.
+- Sending a set of pre-defined commands to Ledger devices.
+ - Get OS version
+ - Get app and version
+ - Open app
+ - Close app
+ - Get battery status
+- Execute a flow of commands with **DeviceAction**.
+
+> [!NOTE]
+> At the moment we do not provide the possibility to distinguish two devices of the same model, via WebHID and to avoid connection to the same device twice.
+
+## Communicate with the device
+
+DMK is offering several ways to communicate with the device.
+
+### Send APDU
+
+
+ This method is not recommended for most of the use cases. We recommend using
+ the _Command_ or _DeviceAction_ instead.
+
+
+You can send APDU commands to the device using the `sendApdu` method of the `dmk` instance.
+Parameters:
+
+- `sessionId`: string - The session ID, which an identifier of the connection with a device.
+- `apdu`: UInt8Array - Byte array of data to be send to the device.
+
+```typescript
+await dmk.sendApdu({ sessionId, apdu });
+```
+
+### Commands
+
+Commands are pre-defined actions that you can send to the device.
+You can use the `sendCommand` method of the `dmk` instance to send a command to the device.
+Parameters:
+
+- `sessionId`: string - The session ID, which an identifier of the connection with a device.
+- `command`: Command - The command to be sent to the device.
+
+```typescript
+import { OpenAppCommand } from "@ledgerhq/device-management-kit";
+
+const command = new OpenAppCommand("Bitcoin"); // Open the Bitcoin app
+await dmk.sendCommand({ sessionId, command });
+```
+
+### Device Actions
+
+Device actions are a set of commands that are executed in a sequence.
+You can use the `executeDeviceAction` method of the `dmk` instance to execute a device action.
+
+It is returning an observable that will emit different states of the action execution.
+A device action is cancellable, you can cancel it by calling the `cancel` function returned by the `executeDeviceAction` method.
+
+Parameters:
+
+- `sessionId`: string - The session ID, which an identifier of the connection with a device.
+- `deviceAction`: DeviceAction - The DeviceAction to be sent to the device.
+
+```typescript
+const openAppDeviceAction = new OpenAppDeviceAction({ appName: "Bitcoin" });
+
+const { observable, cancel } = await dmk.executeDeviceAction({
+ sessionId,
+ openAppDeviceAction,
+});
+```
+
+## State Management
+
+For each connected device, we are managing and providing a device state.
+The states are:
+
+- `connected`: The device is connected.
+- `locked`: The device is locked.
+- `busy`: The device is busy, so not reachable.
diff --git a/apps/docs/pages/docs/explanations/introduction.mdx b/apps/docs/pages/docs/explanations/introduction.mdx
new file mode 100644
index 000000000..2f5b75904
--- /dev/null
+++ b/apps/docs/pages/docs/explanations/introduction.mdx
@@ -0,0 +1,9 @@
+# Why Device Management Kit ?
+
+The DMK is a set of tools and libraries that allow you to manage your devices in a secure and efficient way. It is designed to be used in conjunction with the Device Signer Kit, which is a set of tools and libraries that allow you to sign transactions securely.
+
+It has been designed for external developers with the main idea to provide the best experience for developers to interact with Ledger devices.
+
+It should as maximum as possible abstract the complexity of the communication with the device and provide a simple and easy-to-use API.
+
+It tends to be a replacement for the LedgerJS libraries, mainly `hw-app-XXX` and `transport-XXX` libraries. These libraries are intended to be deprecated in the future.
diff --git a/apps/docs/pages/docs/explanations/ledgerjs.mdx b/apps/docs/pages/docs/explanations/ledgerjs.mdx
new file mode 100644
index 000000000..aa0f242a1
--- /dev/null
+++ b/apps/docs/pages/docs/explanations/ledgerjs.mdx
@@ -0,0 +1,32 @@
+# Differences with LedgerJS
+
+Device management kit aim to replace LedgerJS libraries, mainly `hw-app-XXX` and `transport-XXX` libraries
+
+## Current Problems
+
+Ledger JS libraries where initially made for **Ledger Live** applications. As Ledger Live is a pretty old project (> 7 years),
+we have inevitably a big technical debt. Moreover time make that some part of the logic are today hard to understand and to maintain.
+
+Moreover, some device behavior are not well handled by the libraries. For example, opening an application on the device will cause unexpected disconnection.
+Another feedback we have learnt from partners (software wallets) is that we have a lack of simplicity in the libraries, it require low level knowledge to use them (ex: APDU concept).
+
+## Target
+
+LedgerJS was intended for Ledger Live. It was not designed to be used by third party developers.
+With DMK we are targeting **third party developers first**.
+
+## Abstract complexity
+
+As said above, we wanted to reduce the entry level to interact with Ledger devices.
+So we have tried as much as possible to abstract the complexity of the communication with the device and provide a simple and easy-to-use API.
+
+## Multiple Connected devices
+
+With LedgerJS, it was impossible to be connected at the same time to two devices.
+With DMK, we have made it possible to connect to multiple devices at the same time and interact from one to another.
+
+## When prefer LedgerJS instead of DMK ?
+
+As we are still missing some features in DMK, you may still need to use LedgerJS in some cases.
+For example, we are missing signer for some blockchains, so you may still need to use LedgerJS to sign transactions.
+Some solutions are studied at the moment to provide a better compatibility between DMK and LedgerJS.
diff --git a/apps/docs/pages/docs/explanations/signers.mdx b/apps/docs/pages/docs/explanations/signers.mdx
new file mode 100644
index 000000000..f9414ce84
--- /dev/null
+++ b/apps/docs/pages/docs/explanations/signers.mdx
@@ -0,0 +1,13 @@
+# Signer Kits
+
+As ledger device are able to install application that will allow to be compatible with different blockchain,
+we have created these kits.
+
+Each **signer kit** is coming along with a Ledger Embedded App (ex: _signer-kit-eth_ is comig with _ledger app ethereum_ ).
+
+The main goal of each signer is to ease interaction with the app in the seamlessly possible way.
+
+## Available Signers
+
+- [Signer Ethereum](./signers/eth)
+- [Signer Solana](./signers/solana)
diff --git a/apps/docs/pages/docs/explanations/signers/_meta.json b/apps/docs/pages/docs/explanations/signers/_meta.json
new file mode 100644
index 000000000..a47efbd21
--- /dev/null
+++ b/apps/docs/pages/docs/explanations/signers/_meta.json
@@ -0,0 +1,5 @@
+{
+ "eth": "Signer Ethereum (EVM)",
+ "solana": "Signer Solana",
+ "btc": "Signer Bitcoin"
+}
diff --git a/apps/docs/pages/docs/explanations/signers/btc.mdx b/apps/docs/pages/docs/explanations/signers/btc.mdx
new file mode 100644
index 000000000..24530b979
--- /dev/null
+++ b/apps/docs/pages/docs/explanations/signers/btc.mdx
@@ -0,0 +1,3 @@
+# Bitcoin Signer Kit
+
+_Coming Soon..._
diff --git a/apps/docs/pages/docs/explanations/signers/eth.mdx b/apps/docs/pages/docs/explanations/signers/eth.mdx
new file mode 100644
index 000000000..942d5b121
--- /dev/null
+++ b/apps/docs/pages/docs/explanations/signers/eth.mdx
@@ -0,0 +1,538 @@
+# Ethereum Signer Kit
+
+## Features
+
+Following doc is matching for v1.0.0 of the Ethereum Signer Kit.
+
+### 1: Get Address
+
+This method allows users to retrieve the Ethereum address according to given `derivationPath`.
+
+```typescript
+const { observable, cancel } = signerEth.getAddress(derivationPath, options);
+```
+
+**Parameters**
+
+- `derivationPath`
+
+ - **Required**
+ - **Type:** `string` (e.g., `"44'/60'/0'/0/0"`)
+ - The derivation path used for the Ethereum address. See [here](https://www.ledger.com/blog/understanding-crypto-addresses-and-derivation-paths) for more information.
+
+- `options`
+
+ - Optional
+ - Type: `AddressOptions`
+
+ ```typescript
+ type AddressOptions = {
+ checkOnDevice?: boolean;
+ returnChainCode?: boolean;
+ };
+ ```
+
+ - `checkOnDevice`: An optional boolean indicating whether user confirmation on the device is required (`true`) or not (`false`).
+ - `returnChainCode`: An optional boolean indicating whether the chain code should be returned (`true`) or not (`false`).
+
+**Returns**
+
+- `observable`
+
+ - An [Observable](https://rxjs.dev/guide/observable) object that contains the [`DeviceActionState`](https://github.com/LedgerHQ/device-sdk-ts/blob/develop/packages/device-management-kit/src/api/device-action/model/DeviceActionState.ts) derived instance, which reprensents the operation's state. For example:
+
+ ```typescript
+ observable.subscribe({
+ next: (state: DeviceActionState) => {
+ switch (state.status) {
+ case DeviceActionStatus.NotStarted: {
+ console.log("The action is not started yet.");
+ break;
+ }
+ case DeviceActionStatus.Pending: {
+ const {
+ intermediateValue: { requiredUserInteraction },
+ } = state;
+ // Access the intermediate value here, explained below
+ console.log(
+ "The action is pending and the intermediate value is: ",
+ intermediateValue,
+ );
+ break;
+ }
+ case DeviceActionStatus.Stopped: {
+ console.log("The action has been stopped.");
+ break;
+ }
+ case DeviceActionStatus.Completed: {
+ const { output } = state;
+ // Access the output of the completed action here
+ console.log("The action has been completed: ", output);
+ break;
+ }
+ case DeviceActionStatus.Error: {
+ const { error } = state;
+ // Access the error here if occured
+ console.log("An error occured during the action: ", error);
+ break;
+ }
+ }
+ },
+ });
+ ```
+
+ - When the action status is `DeviceActionStatus.Pending`, the state will include an `intermediateValue` object that provides useful information for interaction:
+
+ ```typescript
+ const { requiredUserInteraction } = intermediateValue;
+
+ switch (requiredUserInteraction) {
+ case UserInteractionRequired.VerifyAddress: {
+ // User needs to verify the address displayed on the device
+ console.log(
+ "User needs to verify the address displayed on the device.",
+ );
+ break;
+ }
+ case UserInteractionRequired.None: {
+ // No user action required
+ console.log("No user action needed.");
+ break;
+ }
+ case UserInteractionRequired.UnlockDevice: {
+ // User needs to unlock the device
+ console.log("The user needs to unlock the device.");
+ break;
+ }
+ case UserInteractionRequired.ConfirmOpenApp: {
+ // User needs to confirm on the device to open the app
+ console.log("The user needs to confirm on the device to open the app.");
+ break;
+ }
+ default:
+ // Type guard to ensure all cases are handled
+ const uncaughtUserInteraction: never = requiredUserInteraction;
+ console.error(
+ "Unhandled user interaction case:",
+ uncaughtUserInteraction,
+ );
+ }
+ ```
+
+ - When the action status is `DeviceActionStatus.Completed`, the execution result can be accessed through the `output` property in the state. The `output` property is of type `GetAddressCommandResponse`, which has the following structure:
+
+ ```typescript
+ type GetAddressCommandResponse = {
+ publicKey: string;
+ address: `0x${string}`;
+ chainCode?: string;
+ };
+ ```
+
+- `cancel`
+ - The function without a return value to cancel the action on the Ledger device.
+
+### 2: Sign Transaction
+
+This method enables users to securely sign transactions using clear signing on Ledger devices.
+
+```typescript
+const { observable, cancel } = signerEth.signTransaction(
+ derivationPath,
+ transaction,
+ options,
+);
+```
+
+**Parameters**
+
+- `derivationPath`
+
+ - **Required**
+ - **Type:** `string` (e.g., `"44'/60'/0'/0/0"`)
+ - The derivation path used in the transaction. See [here](https://www.ledger.com/blog/understanding-crypto-addresses-and-derivation-paths) for more information.
+
+- `transaction`
+
+ - **Required**
+ - **Type:**`Transaction` (compatible with [ethers v5](https://docs.ethers.org/v5/) or [ethers v6](https://docs.ethers.org/v6/))
+ - The transaction object that needs to be signed.
+
+- `options`
+
+ - **Optional**
+ - **Type:** `TransactionOptions`
+
+ ```typescript
+ type TransactionOptions = {
+ domain?: string;
+ };
+ ```
+
+ - `domain` An optional string representing the domain present in the transaction. Currently, only ENS domains are supported.
+
+**Returns**
+
+- `observable`
+
+ - An [Observable](https://rxjs.dev/guide/observable) object that contains the [`DeviceActionState`](https://github.com/LedgerHQ/device-sdk-ts/blob/develop/packages/device-management-kit/src/api/device-action/model/DeviceActionState.ts) derived instance which reprensents the operation's state. For example:
+
+ ```typescript
+ observable.subscribe({
+ next: (state: SignTransactionDAState) => {
+ switch (state.status) {
+ case DeviceActionStatus.NotStarted: {
+ console.log("The action is not started yet.");
+ break;
+ }
+ case DeviceActionStatus.Pending: {
+ const {
+ intermediateValue: { requiredUserInteraction },
+ } = state;
+ // Access the intermediate value here, explained below
+ console.log(
+ "The action is pending and the intermediate value is: ",
+ intermediateValue,
+ );
+ break;
+ }
+ case DeviceActionStatus.Stopped: {
+ console.log("The action has been stopped.");
+ break;
+ }
+ case DeviceActionStatus.Completed: {
+ const { output } = state;
+ // Access the output of the completed action here
+ console.log("The action has been completed: ", output);
+ break;
+ }
+ case DeviceActionStatus.Error: {
+ const { error } = state;
+ // Access the error here if occured
+ console.log("An error occured during the action: ", error);
+ break;
+ }
+ }
+ },
+ });
+ ```
+
+ - When the action status is `DeviceActionStatus.Pending`, the state will include an `intermediateValue` object that provides useful information for interaction:
+
+ ```typescript
+ const { requiredUserInteraction } = intermediateValue;
+
+ switch (requiredUserInteraction) {
+ case UserInteractionRequired.SignTransaction: {
+ // User needs to sign the transaction displayed on the device
+ console.log(
+ "User needs to sign the transaction displayed on the device.",
+ );
+ break;
+ }
+ case UserInteractionRequired.None: {
+ // No user action required
+ console.log("No user action needed.");
+ break;
+ }
+ case UserInteractionRequired.UnlockDevice: {
+ // User needs to unlock the device
+ console.log("The user needs to unlock the device.");
+ break;
+ }
+ case UserInteractionRequired.ConfirmOpenApp: {
+ // User needs to confirm on the device to open the app
+ console.log("The user needs to confirm on the device to open the app.");
+ break;
+ }
+ default:
+ // Type guard to ensure all cases are handled
+ const uncaughtUserInteraction: never = requiredUserInteraction;
+ console.error(
+ "Unhandled user interaction case:",
+ uncaughtUserInteraction,
+ );
+ }
+ ```
+
+ - When the action status is `DeviceActionStatus.Completed`, the execution result can be accessed through the `output` property in the state. This property is a `Signature` object with the following structure:
+
+ ```typescript
+ type Signature = {
+ r: `0x${string}`;
+ s: `0x${string}`;
+ v: number;
+ };
+ ```
+
+- `cancel`
+ - The function without a return value to cancel the action on the Ledger device.
+
+### 3: Sign Message
+
+This method allows users to sign a text string that is displayed on Ledger devices.
+
+```typescript
+const { observable, cancel } = signerEth.signMessage(derivationPath, message);
+```
+
+**Parameters**
+
+- `derivationPath`
+
+ - **Required**
+ - **Type:** `string` (e.g., `"44'/60'/0'/0/0"`)
+ - The derivation path used by the Ethereum message. See [here](https://www.ledger.com/blog/understanding-crypto-addresses-and-derivation-paths) for more information.
+
+- `message`
+
+ - **Required**
+ - **Type:** `string`
+ - The message to be signed, which will be displayed on the Ledger device.
+
+**Returns**
+
+- `observable`
+
+ - An [Observable](https://rxjs.dev/guide/observable) object that contains the [`DeviceActionState`](https://github.com/LedgerHQ/device-sdk-ts/blob/develop/packages/device-management-kit/src/api/device-action/model/DeviceActionState.ts) derived instance which reprensents the operation's state. For example:
+
+ ```typescript
+ observable.subscribe({
+ next: (state: SignPersonalMessageDAState) => {
+ switch (state.status) {
+ case DeviceActionStatus.NotStarted: {
+ console.log("The action is not started yet.");
+ break;
+ }
+ case DeviceActionStatus.Pending: {
+ const {
+ intermediateValue: { requiredUserInteraction },
+ } = state;
+ // Access the intermediate value here, explained below
+ console.log(
+ "The action is pending and the intermediate value is: ",
+ intermediateValue,
+ );
+ break;
+ }
+ case DeviceActionStatus.Stopped: {
+ console.log("The action has been stopped.");
+ break;
+ }
+ case DeviceActionStatus.Completed: {
+ const { output } = state;
+ // Access the output of the completed action here
+ console.log("The action has been completed: ", output);
+ break;
+ }
+ case DeviceActionStatus.Error: {
+ const { error } = state;
+ // Access the error here if occured
+ console.log("An error occured during the action: ", error);
+ break;
+ }
+ }
+ },
+ });
+ ```
+
+ - When the action status is `DeviceActionStatus.Pending`, the state will include an `intermediateValue` object that provides useful information for interaction:
+
+ ```typescript
+ const { requiredUserInteraction } = intermediateValue;
+
+ switch (requiredUserInteraction) {
+ case UserInteractionRequired.SignPersonalMessage: {
+ // User needs to sign the message displayed on the device
+ console.log("User needs to sign the message displayed on the device.");
+ break;
+ }
+ case UserInteractionRequired.None: {
+ // No user action required
+ console.log("No user action needed.");
+ break;
+ }
+ case UserInteractionRequired.UnlockDevice: {
+ // User needs to unlock the device
+ console.log("The user needs to unlock the device.");
+ break;
+ }
+ case UserInteractionRequired.ConfirmOpenApp: {
+ // User needs to confirm on the device to open the app
+ console.log("The user needs to confirm on the device to open the app.");
+ break;
+ }
+ default:
+ // Type guard to ensure all cases are handled
+ const uncaughtUserInteraction: never = requiredUserInteraction;
+ console.error(
+ "Unhandled user interaction case:",
+ uncaughtUserInteraction,
+ );
+ }
+ ```
+
+ - When the action status is `DeviceActionStatus.Completed`, the execution result can be accessed through the `output` property in the state. This property is a `Signature` object with the following structure:
+
+ ```typescript
+ type Signature = {
+ r: `0x${string}`;
+ s: `0x${string}`;
+ v: number;
+ };
+ ```
+
+- `cancel`
+ - The function without a return value to cancel the action on the Ledger device.
+
+### 4: Sign TypedData
+
+This method enables users to sign an Ethereum message following the [EIP-712](https://eips.ethereum.org/EIPS/eip-712) specification.
+
+```typescript
+const { observable, cancel } = signerEth.signTypedData(
+ derivationPath,
+ typedData,
+);
+```
+
+**Parameters**
+
+- `derivationPath`
+
+ - **Required**
+ - **Type:** `string` (e.g., `"44'/60'/0'/0/0"`)
+ - The derivation path used by the Ethereum message. See [here](https://www.ledger.com/blog/understanding-crypto-addresses-and-derivation-paths) for more information.
+
+- `typedData`
+
+ - **Required**
+ - **Type:** `TypedData`
+
+ ```typescript
+ interface TypedData {
+ domain: TypedDataDomain;
+ types: Record>;
+ primaryType: string;
+ message: Record;
+ }
+
+ interface TypedDataDomain {
+ name?: string;
+ version?: string;
+ chainId?: number;
+ verifyingContract?: string;
+ salt?: string;
+ }
+
+ interface TypedDataField {
+ name: string;
+ type: string;
+ }
+ ```
+
+ - The typed data as defined at [EIP-712](https://eips.ethereum.org/EIPS/eip-712).
+
+**Returns**
+
+- `observable`
+
+ - An [Observable](https://rxjs.dev/guide/observable) object that contains the [`DeviceActionState`](https://github.com/LedgerHQ/device-sdk-ts/blob/develop/packages/device-management-kit/src/api/device-action/model/DeviceActionState.ts) derived instance which reprensents the operation's state. For example:
+
+ ```typescript
+ observable.subscribe({
+ next: (state: SignTypedDataDAState) => {
+ switch (state.status) {
+ case DeviceActionStatus.NotStarted: {
+ console.log("The action is not started yet.");
+ break;
+ }
+ case DeviceActionStatus.Pending: {
+ const { intermediateValue } = state;
+ // Access the intermediate value here, explained below
+ console.log(
+ "The action is pending and the intermediate value is: ",
+ requiredUserInteraction,
+ );
+ break;
+ }
+ case DeviceActionStatus.Stopped: {
+ console.log("The action has been stopped.");
+ break;
+ }
+ case DeviceActionStatus.Completed: {
+ const { output } = state;
+ // Access the output of the completed action here, explained below
+ console.log("The action has been completed: ", output);
+ break;
+ }
+ case DeviceActionStatus.Error: {
+ const { error } = state;
+ // Access the error here if occured
+ console.log("An error occured during the action: ", error);
+ break;
+ }
+ }
+ },
+ });
+ ```
+
+ - When the action status is `DeviceActionStatus.Pending`, the state will include an `intermediateValue` object that provides useful information for interaction:
+
+ ```typescript
+ const { requiredUserInteraction } = intermediateValue;
+
+ switch (requiredUserInteraction) {
+ case UserInteractionRequired.SignTypedData: {
+ // User needs to sign the typed data displayed on the device
+ console.log(
+ "User needs to sign the typed data displayed on the device.",
+ );
+ break;
+ }
+ case UserInteractionRequired.None: {
+ // No user action required
+ console.log("No user action needed.");
+ break;
+ }
+ case UserInteractionRequired.UnlockDevice: {
+ // User needs to unlock the device
+ console.log("The user needs to unlock the device.");
+ break;
+ }
+ case UserInteractionRequired.ConfirmOpenApp: {
+ // User needs to confirm on the device to open the app
+ console.log("The user needs to confirm on the device to open the app.");
+ break;
+ }
+ default:
+ // Type guard to ensure all cases are handled
+ const uncaughtUserInteraction: never = requiredUserInteraction;
+ console.error(
+ "Unhandled user interaction case:",
+ uncaughtUserInteraction,
+ );
+ }
+ ```
+
+ - When the action status is `DeviceActionStatus.Completed`, the execution result can be accessed through the `output` property in the state. This property is a `Signature` object with the following structure:
+
+ ```typescript
+ type Signature = {
+ r: `0x${string}`;
+ s: `0x${string}`;
+ v: number;
+ };
+ ```
+
+- `cancel`
+ - The function without a return value to cancel the action on the Ledger device.
+
+## Example
+
+We encourage you to explore the Ethereum Signer by trying it out in our online [sample application](https://app.devicesdk.ledger-test.com/). Experience how it works and see its capabilities in action. Of course, you will need a Ledger device connected.
+
+## Clear Signing Initiative
+
+As the signer is already integrating the context module. It will also provide the capability of clear sign transaction
+if we have the data related to the transaction.
diff --git a/apps/docs/pages/docs/explanations/signers/solana.mdx b/apps/docs/pages/docs/explanations/signers/solana.mdx
new file mode 100644
index 000000000..810b3802e
--- /dev/null
+++ b/apps/docs/pages/docs/explanations/signers/solana.mdx
@@ -0,0 +1,479 @@
+# Solana Signer Kit
+
+## Features
+
+Following doc is matching for v1.0.0 of the Ethereum Signer Kit.
+
+### 1: Get Address
+
+This method allows users to retrieve the Solana address according to given `derivationPath`.
+
+```typescript
+const { observable, cancel } = signerSolana.getAddress(derivationPath, options);
+```
+
+**Parameters**
+
+- `derivationPath`
+
+ - **Required**
+ - **Type:** `string` (e.g., `"44'/501'/0'"`)
+ - The derivation path used for the Solana address. See [here](https://www.ledger.com/blog/understanding-crypto-addresses-and-derivation-paths) for more information.
+
+- `options`
+
+ - Optional
+ - Type: `AddressOptions`
+
+ ```typescript
+ type AddressOptions = {
+ checkOnDevice?: boolean;
+ };
+ ```
+
+ - `checkOnDevice`: An optional boolean indicating whether user confirmation on the device is required (`true`) or not (`false`).
+
+**Returns**
+
+- `observable`
+
+ - An [Observable](https://rxjs.dev/guide/observable) object that contains the [`DeviceActionState`](https://github.com/LedgerHQ/device-sdk-ts/blob/develop/packages/device-management-kit/src/api/device-action/model/DeviceActionState.ts) derived instance, which reprensents the operation's state. For example:
+
+ ```typescript
+ observable.subscribe({
+ next: (state: DeviceActionState) => {
+ switch (state.status) {
+ case DeviceActionStatus.NotStarted: {
+ console.log("The action is not started yet.");
+ break;
+ }
+ case DeviceActionStatus.Pending: {
+ const {
+ intermediateValue: { requiredUserInteraction },
+ } = state;
+ // Access the intermediate value here, explained below
+ console.log(
+ "The action is pending and the intermediate value is: ",
+ intermediateValue,
+ );
+ break;
+ }
+ case DeviceActionStatus.Stopped: {
+ console.log("The action has been stopped.");
+ break;
+ }
+ case DeviceActionStatus.Completed: {
+ const { output } = state;
+ // Access the output of the completed action here
+ console.log("The action has been completed: ", output);
+ break;
+ }
+ case DeviceActionStatus.Error: {
+ const { error } = state;
+ // Access the error here if occured
+ console.log("An error occured during the action: ", error);
+ break;
+ }
+ }
+ },
+ });
+ ```
+
+ - When the action status is `DeviceActionStatus.Pending`, the state will include an `intermediateValue` object that provides useful information for interaction:
+
+ ```typescript
+ const { requiredUserInteraction } = intermediateValue;
+
+ switch (requiredUserInteraction) {
+ case UserInteractionRequired.VerifyAddress: {
+ // User needs to verify the address displayed on the device
+ console.log(
+ "User needs to verify the address displayed on the device.",
+ );
+ break;
+ }
+ case UserInteractionRequired.None: {
+ // No user action required
+ console.log("No user action needed.");
+ break;
+ }
+ case UserInteractionRequired.UnlockDevice: {
+ // User needs to unlock the device
+ console.log("The user needs to unlock the device.");
+ break;
+ }
+ case UserInteractionRequired.ConfirmOpenApp: {
+ // User needs to confirm on the device to open the app
+ console.log("The user needs to confirm on the device to open the app.");
+ break;
+ }
+ default:
+ // Type guard to ensure all cases are handled
+ const uncaughtUserInteraction: never = requiredUserInteraction;
+ console.error(
+ "Unhandled user interaction case:",
+ uncaughtUserInteraction,
+ );
+ }
+ ```
+
+ - When the action status is `DeviceActionStatus.Completed`, the execution result can be accessed through the `output` property in the state. The `output` property is of type `PublicKey` in `base58`.
+
+ ```typescript
+ type PublicKey = string; // address in base58 format
+ ```
+
+- `cancel`
+ - The function without a return value to cancel the action on the Ledger device.
+
+### 2: Sign Transaction
+
+This method enables users to securely sign transactions using clear signing on Ledger devices.
+
+```typescript
+const { observable, cancel } = signerSolana.signTransaction(
+ derivationPath,
+ transaction,
+ options,
+);
+```
+
+**Parameters**
+
+- `derivationPath`
+
+ - **Required**
+ - **Type:** `string` (e.g., `"44'/501'/0'"`)
+ - The derivation path used in the transaction. See [here](https://www.ledger.com/blog/understanding-crypto-addresses-and-derivation-paths) for more information.
+
+- `transaction`
+
+ - **Required**
+ - **Type:** `Uint8Array`
+ - The transaction object that needs to be signed.
+
+- `options`
+
+ - **Optional**
+ - **Type:** `TBD`
+ - No option defined yet, but will be used for clear signing in a near future.
+
+ ```typescript
+ type TransactionOptions = {};
+ ```
+
+**Returns**
+
+- `observable`
+
+ - An [Observable](https://rxjs.dev/guide/observable) object that contains the [`DeviceActionState`](https://github.com/LedgerHQ/device-sdk-ts/blob/develop/packages/device-management-kit/src/api/device-action/model/DeviceActionState.ts) derived instance which reprensents the operation's state. For example:
+
+ ```typescript
+ observable.subscribe({
+ next: (state: SignTransactionDAState) => {
+ switch (state.status) {
+ case DeviceActionStatus.NotStarted: {
+ console.log("The action is not started yet.");
+ break;
+ }
+ case DeviceActionStatus.Pending: {
+ const {
+ intermediateValue: { requiredUserInteraction },
+ } = state;
+ // Access the intermediate value here, explained below
+ console.log(
+ "The action is pending and the intermediate value is: ",
+ intermediateValue,
+ );
+ break;
+ }
+ case DeviceActionStatus.Stopped: {
+ console.log("The action has been stopped.");
+ break;
+ }
+ case DeviceActionStatus.Completed: {
+ const { output } = state;
+ // Access the output of the completed action here
+ console.log("The action has been completed: ", output);
+ break;
+ }
+ case DeviceActionStatus.Error: {
+ const { error } = state;
+ // Access the error here if occured
+ console.log("An error occured during the action: ", error);
+ break;
+ }
+ }
+ },
+ });
+ ```
+
+ - When the action status is `DeviceActionStatus.Pending`, the state will include an `intermediateValue` object that provides useful information for interaction:
+
+ ```typescript
+ const { requiredUserInteraction } = intermediateValue;
+
+ switch (requiredUserInteraction) {
+ case UserInteractionRequired.SignTransaction: {
+ // User needs to sign the transaction displayed on the device
+ console.log(
+ "User needs to sign the transaction displayed on the device.",
+ );
+ break;
+ }
+ case UserInteractionRequired.None: {
+ // No user action required
+ console.log("No user action needed.");
+ break;
+ }
+ case UserInteractionRequired.UnlockDevice: {
+ // User needs to unlock the device
+ console.log("The user needs to unlock the device.");
+ break;
+ }
+ case UserInteractionRequired.ConfirmOpenApp: {
+ // User needs to confirm on the device to open the app
+ console.log("The user needs to confirm on the device to open the app.");
+ break;
+ }
+ default:
+ // Type guard to ensure all cases are handled
+ const uncaughtUserInteraction: never = requiredUserInteraction;
+ console.error(
+ "Unhandled user interaction case:",
+ uncaughtUserInteraction,
+ );
+ }
+ ```
+
+ - When the action status is `DeviceActionStatus.Completed`, the execution result can be accessed through the `output` property in the state. This property is a `Signature` object with the following structure:
+
+ ```typescript
+ type Signature = Uint8Array;
+ ```
+
+- `cancel`
+ - The function without a return value to cancel the action on the Ledger device.
+
+### 3: Sign Message
+
+This method allows users to sign a text string that is displayed on Ledger devices.
+
+```typescript
+const { observable, cancel } = signerSolana.signMessage(
+ derivationPath,
+ message,
+);
+```
+
+**Parameters**
+
+- `derivationPath`
+
+ - **Required**
+ - **Type:** `string` (e.g., `"44'/501'/0'"`)
+ - The derivation path used by the Solana message. See [here](https://www.ledger.com/blog/understanding-crypto-addresses-and-derivation-paths) for more information.
+
+- `message`
+
+ - **Required**
+ - **Type:** `string`
+ - The message to be signed, which will be displayed on the Ledger device.
+
+**Returns**
+
+- `observable`
+
+ - An [Observable](https://rxjs.dev/guide/observable) object that contains the [`DeviceActionState`](https://github.com/LedgerHQ/device-sdk-ts/blob/develop/packages/device-management-kit/src/api/device-action/model/DeviceActionState.ts) derived instance which reprensents the operation's state. For example:
+
+ ```typescript
+ observable.subscribe({
+ next: (state: SignPersonalMessageDAState) => {
+ switch (state.status) {
+ case DeviceActionStatus.NotStarted: {
+ console.log("The action is not started yet.");
+ break;
+ }
+ case DeviceActionStatus.Pending: {
+ const {
+ intermediateValue: { requiredUserInteraction },
+ } = state;
+ // Access the intermediate value here, explained below
+ console.log(
+ "The action is pending and the intermediate value is: ",
+ intermediateValue,
+ );
+ break;
+ }
+ case DeviceActionStatus.Stopped: {
+ console.log("The action has been stopped.");
+ break;
+ }
+ case DeviceActionStatus.Completed: {
+ const { output } = state;
+ // Access the output of the completed action here
+ console.log("The action has been completed: ", output);
+ break;
+ }
+ case DeviceActionStatus.Error: {
+ const { error } = state;
+ // Access the error here if occured
+ console.log("An error occured during the action: ", error);
+ break;
+ }
+ }
+ },
+ });
+ ```
+
+ - When the action status is `DeviceActionStatus.Pending`, the state will include an `intermediateValue` object that provides useful information for interaction:
+
+ ```typescript
+ const { requiredUserInteraction } = intermediateValue;
+
+ switch (requiredUserInteraction) {
+ case UserInteractionRequired.SignPersonalMessage: {
+ // User needs to sign the message displayed on the device
+ console.log("User needs to sign the message displayed on the device.");
+ break;
+ }
+ case UserInteractionRequired.None: {
+ // No user action required
+ console.log("No user action needed.");
+ break;
+ }
+ case UserInteractionRequired.UnlockDevice: {
+ // User needs to unlock the device
+ console.log("The user needs to unlock the device.");
+ break;
+ }
+ case UserInteractionRequired.ConfirmOpenApp: {
+ // User needs to confirm on the device to open the app
+ console.log("The user needs to confirm on the device to open the app.");
+ break;
+ }
+ default:
+ // Type guard to ensure all cases are handled
+ const uncaughtUserInteraction: never = requiredUserInteraction;
+ console.error(
+ "Unhandled user interaction case:",
+ uncaughtUserInteraction,
+ );
+ }
+ ```
+
+ - When the action status is `DeviceActionStatus.Completed`, the execution result can be accessed through the `output` property in the state. This property is a `Signature` object with the following structure:
+
+ ```typescript
+ type Signature = Uint8Array;
+ ```
+
+- `cancel`
+ - The function without a return value to cancel the action on the Ledger device.
+
+### 4: Get App Configuration
+
+This method allow the user to fetch the current app configuration.
+
+```typescript
+const { observable, cancel } = signerSolana.getAppConfiguration();
+```
+
+**Returns**
+
+- `observable`
+
+ - An [Observable](https://rxjs.dev/guide/observable) object that contains the [`DeviceActionState`](https://github.com/LedgerHQ/device-sdk-ts/blob/develop/packages/device-management-kit/src/api/device-action/model/DeviceActionState.ts) derived instance which reprensents the operation's state. For example:
+
+ ```typescript
+ observable.subscribe({
+ next: (state: SignTypedDataDAState) => {
+ switch (state.status) {
+ case DeviceActionStatus.NotStarted: {
+ console.log("The action is not started yet.");
+ break;
+ }
+ case DeviceActionStatus.Pending: {
+ const { intermediateValue } = state;
+ // Access the intermediate value here, explained below
+ console.log(
+ "The action is pending and the intermediate value is: ",
+ requiredUserInteraction,
+ );
+ break;
+ }
+ case DeviceActionStatus.Stopped: {
+ console.log("The action has been stopped.");
+ break;
+ }
+ case DeviceActionStatus.Completed: {
+ const { output } = state;
+ // Access the output of the completed action here, explained below
+ console.log("The action has been completed: ", output);
+ break;
+ }
+ case DeviceActionStatus.Error: {
+ const { error } = state;
+ // Access the error here if occured
+ console.log("An error occured during the action: ", error);
+ break;
+ }
+ }
+ },
+ });
+ ```
+
+ - When the action status is `DeviceActionStatus.Pending`, the state will include an `intermediateValue` object that provides useful information for interaction:
+
+ ```typescript
+ const { requiredUserInteraction } = intermediateValue;
+
+ switch (requiredUserInteraction) {
+ case UserInteractionRequired.SignTypedData: {
+ // User needs to sign the typed data displayed on the device
+ console.log(
+ "User needs to sign the typed data displayed on the device.",
+ );
+ break;
+ }
+ case UserInteractionRequired.None: {
+ // No user action required
+ console.log("No user action needed.");
+ break;
+ }
+ case UserInteractionRequired.UnlockDevice: {
+ // User needs to unlock the device
+ console.log("The user needs to unlock the device.");
+ break;
+ }
+ case UserInteractionRequired.ConfirmOpenApp: {
+ // User needs to confirm on the device to open the app
+ console.log("The user needs to confirm on the device to open the app.");
+ break;
+ }
+ default:
+ // Type guard to ensure all cases are handled
+ const uncaughtUserInteraction: never = requiredUserInteraction;
+ console.error(
+ "Unhandled user interaction case:",
+ uncaughtUserInteraction,
+ );
+ }
+ ```
+
+ - When the action status is `DeviceActionStatus.Completed`, the execution result can be accessed through the `output` property in the state. This property is a `Signature` object with the following structure:
+
+ ```typescript
+ type AppConfiguration = {
+ blindSigningEnabled: boolean;
+ pubKeyDisplayMode: PublicKeyDisplayMode;
+ version: string;
+ };
+ ```
+
+- `cancel`
+ - The function without a return value to cancel the action on the Ledger device.
+
+## Example
+
+We encourage you to explore the Solana Signer by trying it out in our online [sample application](https://app.devicesdk.ledger-test.com/). Experience how it works and see its capabilities in action. Of course, you will need a Ledger device connected.
diff --git a/apps/docs/pages/docs/integration_walkthroughs.mdx b/apps/docs/pages/docs/integration_walkthroughs.mdx
new file mode 100644
index 000000000..5218a5f6e
--- /dev/null
+++ b/apps/docs/pages/docs/integration_walkthroughs.mdx
@@ -0,0 +1 @@
+# Integration Walkthrough
diff --git a/apps/docs/pages/docs/integration_walkthroughs/_meta.json b/apps/docs/pages/docs/integration_walkthroughs/_meta.json
new file mode 100644
index 000000000..dd7ec8efa
--- /dev/null
+++ b/apps/docs/pages/docs/integration_walkthroughs/_meta.json
@@ -0,0 +1,3 @@
+{
+ "how_to": "How to ..."
+}
diff --git a/apps/docs/pages/docs/integration_walkthroughs/how_to/_meta.json b/apps/docs/pages/docs/integration_walkthroughs/how_to/_meta.json
new file mode 100644
index 000000000..453a0d648
--- /dev/null
+++ b/apps/docs/pages/docs/integration_walkthroughs/how_to/_meta.json
@@ -0,0 +1,4 @@
+{
+ "how_to_use_a_signer": "use a Signer",
+ "build_custom_command": "build a custom command"
+}
diff --git a/apps/docs/pages/docs/integration_walkthroughs/how_to/build_custom_command.mdx b/apps/docs/pages/docs/integration_walkthroughs/how_to/build_custom_command.mdx
new file mode 100644
index 000000000..70e03c1bd
--- /dev/null
+++ b/apps/docs/pages/docs/integration_walkthroughs/how_to/build_custom_command.mdx
@@ -0,0 +1,73 @@
+# Build a Custom Command
+
+You can build your own command simply by extending the `Command` class and implementing the `getApdu` and `parseResponse` methods.
+
+Then you can use the `sendCommand` method to send it to a connected device.
+
+This is strongly recommended over direct usage of `sendApdu`.
+
+Check the existing commands for a variety of examples.
+
+```typescript
+export class MyCustomCommand
+ implements Command
+{
+ args: GetAddressCommandArgs;
+
+ constructor(args: GetAddressCommandArgs) {
+ this.args = args;
+ }
+
+ getApdu(): Apdu {
+
+ // Main args for the APDU
+ const getEthAddressArgs: ApduBuilderArgs = {
+ cla: 0xe0, // Command CLA
+ ins: 0x02, // Command INS
+ p1: 0x00, //Parameter P1
+ p2: 0x00, //Parameter P1
+ };
+
+ //Add the data attache to the APDU with the builder
+ const builder = new ApduBuilder(getEthAddressArgs);
+ builder.add32BitUIntToData(0);
+ ... // Add more data to the APDU
+ return builder.build();
+ }
+
+ parseResponse(
+ response: ApduResponse,
+ ): CommandResult {
+ //Create Apdu Parser
+ const parser = new ApduParser(response);
+
+ // FIRST: check status word
+ if (!CommandUtils.isSuccessResponse(response)) {
+ return CommandResultFactory({
+ error: GlobalCommandErrorHandler.handle(response),
+ });
+ }
+
+ // Extract fields from the response
+ const customAttributes1 = parser.extract8BitUInt();
+ if (customAttributes1 === undefined) {
+ return CommandResultFactory({
+ error: new InvalidStatusWordError("Public key length is missing"),
+ });
+ }
+ ... // Extract more fields from the response
+
+ return CommandResultFactory({
+ MyCustomResponse: {
+ customAttributes1,
+ customAttributes2,
+ ...
+ },
+ });
+ }
+}
+```
+
+## Usage of ApduBuilder
+
+## Usage of ApduParser
diff --git a/apps/docs/pages/docs/integration_walkthroughs/how_to/how_to_use_a_signer.mdx b/apps/docs/pages/docs/integration_walkthroughs/how_to/how_to_use_a_signer.mdx
new file mode 100644
index 000000000..c66d35b5e
--- /dev/null
+++ b/apps/docs/pages/docs/integration_walkthroughs/how_to/how_to_use_a_signer.mdx
@@ -0,0 +1,32 @@
+# Use a signer
+
+> [Note] We will show the usage of the signer with the Ethereum signer.
+> The same logic can be applied to the other signers.
+
+## Installation
+
+> **Note:** This module is not standalone; it depends on the [@ledgerhq/device-management-kit](https://github.com/LedgerHQ/device-sdk-ts/tree/develop/packages/device-management-kit) package, so you need to install it first.
+
+To install the `device-signer-kit-ethereum` package, run the following command:
+
+```sh
+npm install @ledgerhq/device-signer-kit-ethereum
+```
+
+## Usage
+
+To initialize an Ethereum signer instance, you need a Ledger Device Management Kit instance and the ID of the session of the connected device. Use the `SignerEthBuilder` along with the [Context Module](https://github.com/LedgerHQ/device-sdk-ts/tree/develop/packages/signer/context-module) by default developed by Ledger:
+
+```typescript
+// Initialize an Ethereum signer instance using default context module
+const signerEth = new SignerEthBuilder({ sdk, sessionId }).build();
+```
+
+You can also configure the context module yourself:
+
+```typescript
+// Initialize an Ethereum signer instance using customized context module
+const signerEth = new SignerEthBuilder({ sdk, sessionId })
+ .withContextModule(customContextModule)
+ .build();
+```
diff --git a/apps/docs/pages/docs/migrations.mdx b/apps/docs/pages/docs/migrations.mdx
new file mode 100644
index 000000000..4a20c2ebc
--- /dev/null
+++ b/apps/docs/pages/docs/migrations.mdx
@@ -0,0 +1,11 @@
+# Migrations
+
+Find below all the migrations guide available:
+
+## Device Management Kit
+
+- migration from [v0.4.0 to v0.5.0](./migrations/04_to_05/)
+
+## Device Signer Kit - Ethereum
+
+_coming soon_
diff --git a/apps/docs/pages/docs/migrations/04_to_05.mdx b/apps/docs/pages/docs/migrations/04_to_05.mdx
new file mode 100644
index 000000000..5fe07eb92
--- /dev/null
+++ b/apps/docs/pages/docs/migrations/04_to_05.mdx
@@ -0,0 +1,3 @@
+# Migration from 0.4.0 to 0.5.0
+
+_coming soon_
diff --git a/apps/docs/pages/docs/migrations/_meta.json b/apps/docs/pages/docs/migrations/_meta.json
new file mode 100644
index 000000000..7c82c5769
--- /dev/null
+++ b/apps/docs/pages/docs/migrations/_meta.json
@@ -0,0 +1,3 @@
+{
+ "04_to_05": "DMK: Migrate from v0.4.0 to 0.5.0"
+}
diff --git a/apps/docs/pages/docs/references.mdx b/apps/docs/pages/docs/references.mdx
new file mode 100644
index 000000000..f17d827d8
--- /dev/null
+++ b/apps/docs/pages/docs/references.mdx
@@ -0,0 +1,3 @@
+#References
+
+Device Management Kit References
diff --git a/apps/docs/pages/docs/references/_meta.json b/apps/docs/pages/docs/references/_meta.json
new file mode 100644
index 000000000..94a99ac11
--- /dev/null
+++ b/apps/docs/pages/docs/references/_meta.json
@@ -0,0 +1,4 @@
+{
+ "dmk": "Device Management Kit",
+ "signer_eth": "Signer Kit Ethereum"
+}
diff --git a/apps/docs/pages/index.mdx b/apps/docs/pages/index.mdx
new file mode 100644
index 000000000..17ae03b2c
--- /dev/null
+++ b/apps/docs/pages/index.mdx
@@ -0,0 +1,30 @@
+---
+title: Ledger Device Management Kit Documentation
+---
+
+export function Card({ title, description, link, icon }) {
+ return (
+
+ );
+}
+
+export function Hero() {
+ return (
+
+
Interact with Ledger Devices
+
+ Seamlessly connect Ledger's devices into your applications with our{" "}
+ Device Management Kit.
+
+
+ );
+}
+
+
diff --git a/apps/docs/postcss.config.js b/apps/docs/postcss.config.js
new file mode 100644
index 000000000..0e01dd929
--- /dev/null
+++ b/apps/docs/postcss.config.js
@@ -0,0 +1,9 @@
+// If you want to use other PostCSS plugins, see the following:
+// https://tailwindcss.com/docs/using-with-preprocessors
+/** @type {import("postcss").Postcss} */
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/apps/docs/style.css b/apps/docs/style.css
new file mode 100644
index 000000000..2a6f5b82f
--- /dev/null
+++ b/apps/docs/style.css
@@ -0,0 +1,106 @@
+/* Hero Section */
+
+.title {
+ font-size: 2em;
+}
+
+.hero {
+ padding: 60px 20px;
+ text-align: center;
+ height: 60vh;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.hero h1 {
+ font-size: 2.5em;
+ margin-bottom: 20px;
+}
+
+.hero p {
+ font-size: 1.2em;
+}
+
+em {
+ color: #ff5300;
+}
+
+/* Cards */
+.card-container {
+ display: flex;
+ justify-content: center;
+ margin: 40px 0;
+}
+
+.card {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ border-radius: 8px;
+ padding: 20px;
+ width: 250px;
+ text-align: center;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ margin: 0 20px;
+}
+
+.card .icon {
+ font-size: 3em;
+}
+
+.card h3 {
+ margin-top: 20px;
+ margin-bottom: 10px;
+ font-size: 1.5em;
+}
+
+.card p {
+ font-size: 1em;
+ margin-bottom: 20px;
+}
+
+.btn {
+ display: inline-block;
+ background-color: #ff5300;
+ color: white;
+ padding: 10px 20px;
+ text-align: center;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.btn:hover {
+ background-color: #ff5300;
+}
+
+/* Additional Resources */
+.resources-container {
+ text-align: center;
+ padding: 40px 20px;
+}
+
+.resources-container h2 {
+ font-size: 1.8em;
+ margin-bottom: 20px;
+}
+
+.resources-container ul {
+ list-style: none;
+ padding: 0;
+}
+
+.resources-container ul li {
+ margin: 10px 0;
+}
+
+.resources-container ul li a {
+ font-size: 1.1em;
+ text-decoration: none;
+ color: #333;
+}
+
+.resources-container ul li a:hover {
+ text-decoration: underline;
+}
diff --git a/apps/docs/tailwind.config.js b/apps/docs/tailwind.config.js
new file mode 100644
index 000000000..521c405bd
--- /dev/null
+++ b/apps/docs/tailwind.config.js
@@ -0,0 +1,10 @@
+/** @type {import("tailwindcss").Config} */
+
+module.exports = {
+ content: ["./pages/**/*.{js,ts,jsx,tsx,mdx}", "./theme.config.tsx"],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+ darkMode: "class",
+};
diff --git a/apps/docs/theme.config.tsx b/apps/docs/theme.config.tsx
new file mode 100644
index 000000000..600894f9f
--- /dev/null
+++ b/apps/docs/theme.config.tsx
@@ -0,0 +1,103 @@
+/* eslint-disable no-restricted-syntax */
+import React from "react";
+import { useRouter } from "next/router";
+
+export default {
+ head: (
+ <>
+
+
+
+
+ >
+ ),
+ logo: (
+
+
+ Ledger logo
+
+
+
Ledger Device Management Kit
+
+ ),
+ project: {
+ link: "https://github.com/LedgerHQ/device-sdk-ts",
+ },
+ docsRepositoryBase:
+ "https://github.com/LedgerHQ/device-sdk-ts/tree/main/apps/docs",
+ chat: {
+ link: "https://developers.ledger.com/discord/",
+ },
+ editLink: {
+ text: "Edit this page on GitHub â",
+ },
+ feedback: {
+ content: "Question? Give us feedback â",
+ labels: "feedback",
+ },
+ useNextSeoProps() {
+ const { asPath } = useRouter();
+ if (asPath === "/") {
+ return {
+ titleTemplate: "Ledger DMK - Documentation",
+ };
+ }
+ const word = asPath.split("/")[1];
+ const capitalizedWord = word.charAt(0).toUpperCase() + word.slice(1);
+ return {
+ titleTemplate: `Ledger DMK - ${capitalizedWord}`,
+ };
+ },
+ footer: {
+ text: (
+
+
+
+
+
+ Copyright Š 2024 Ledger SAS. All rights reserved.
+
+
+ ),
+ },
+};
diff --git a/apps/docs/tsconfig.eslint.json b/apps/docs/tsconfig.eslint.json
new file mode 100644
index 000000000..9cb4a43f9
--- /dev/null
+++ b/apps/docs/tsconfig.eslint.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.json",
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ "next.config.js",
+ "postcss.config.js",
+ "eslint.config.mjs",
+ "tailwind.config.js"
+ ]
+}
diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json
new file mode 100644
index 000000000..4e49202f2
--- /dev/null
+++ b/apps/docs/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "@ledgerhq/tsconfig-dsdk/tsconfig.web",
+ "compilerOptions": {
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ "eslint.config.mjs",
+ "tailwind.config.js"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/apps/sample/.gitignore b/apps/sample/.gitignore
index 0ea48f883..2077bd84c 100644
--- a/apps/sample/.gitignore
+++ b/apps/sample/.gitignore
@@ -37,3 +37,6 @@ next-env.d.ts
# Sentry Config File
.sentryclirc
+
+# Playwright test results
+test-results/
diff --git a/apps/sample/next.config.js b/apps/sample/next.config.js
index 3dad7f968..8a4688eca 100644
--- a/apps/sample/next.config.js
+++ b/apps/sample/next.config.js
@@ -1,55 +1,37 @@
/** @type {import('next').NextConfig} */
+// eslint-disable-next-line @typescript-eslint/no-require-imports
+const { withSentryConfig } = require("@sentry/nextjs");
+
const nextConfig = {
reactStrictMode: true,
compiler: {
styledComponents: true,
},
-};
-
-module.exports = nextConfig;
-
-// Injected content via Sentry wizard below
-
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const { withSentryConfig } = require("@sentry/nextjs");
-
-module.exports = withSentryConfig(
- module.exports,
- {
- // For all available options, see:
- // https://github.com/getsentry/sentry-webpack-plugin#options
-
- // Suppresses source map uploading logs during build
- silent: true,
- org: "ledger",
- project: "device-sdk-sample",
+ env: {
+ SDK_CONFIG_TRANSPORT:
+ process.env.npm_lifecycle_event === "dev:default-mock"
+ ? "MOCK_SERVER"
+ : "",
},
- {
- // For all available options, see:
- // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
-
- // Upload a larger set of source maps for prettier stack traces (increases build time)
- widenClientFileUpload: true,
-
- // Transpiles SDK to be compatible with IE11 (increases bundle size)
- transpileClientSDK: true,
-
- // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. (increases server load)
- // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
- // side errors will fail.
- tunnelRoute: "/monitoring",
-
- // Hides source maps from generated client bundles
- hideSourceMaps: true,
+};
- // Automatically tree-shake Sentry logger statements to reduce bundle size
- disableLogger: true,
+// Define Sentry configuration options
+const sentryWebpackPluginOptions = {
+ // For all available options, see:
+ // https://github.com/getsentry/sentry-webpack-plugin#options
+
+ silent: true,
+ org: "ledger",
+ project: "device-management-kit-sample",
+
+ // Additional Sentry options
+ widenClientFileUpload: true, // Upload a larger set of source maps for prettier stack traces (increases build time)
+ transpileClientSDK: true, // Transpiles SDK to be compatible with IE11 (increases bundle size)
+ tunnelRoute: "/monitoring", // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
+ hideSourceMaps: true, // Hides source maps from generated client bundles
+ disableLogger: true, // Automatically tree-shake Sentry logger statements to reduce bundle size
+ automaticVercelMonitors: true, // Enables automatic instrumentation of Vercel Cron Monitors
+};
- // Enables automatic instrumentation of Vercel Cron Monitors.
- // See the following for more information:
- // https://docs.sentry.io/product/crons/
- // https://vercel.com/docs/cron-jobs
- automaticVercelMonitors: true,
- },
-);
+module.exports = withSentryConfig(nextConfig, sentryWebpackPluginOptions);
diff --git a/apps/sample/package.json b/apps/sample/package.json
index 462684b2a..2663105fa 100644
--- a/apps/sample/package.json
+++ b/apps/sample/package.json
@@ -1,28 +1,35 @@
{
- "name": "@ledgerhq/device-sdk-sample",
+ "name": "@ledgerhq/device-management-kit-sample",
"version": "0.1.3",
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:default-mock": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"lint:fix": "eslint --fix",
"prettier": "prettier . --check",
"prettier:fix": "prettier . --write",
- "typecheck": "tsc --noEmit"
+ "typecheck": "tsc --noEmit",
+ "test:playwright": "pnpm playwright test"
},
"dependencies": {
"@ledgerhq/context-module": "workspace:*",
"@ledgerhq/device-management-kit": "workspace:*",
+ "@ledgerhq/device-management-kit-flipper-plugin-client": "workspace:*",
"@ledgerhq/device-signer-kit-ethereum": "workspace:*",
- "@ledgerhq/react-ui": "^0.16.0",
+ "@ledgerhq/device-signer-kit-solana": "workspace:*",
+ "@ledgerhq/device-transport-kit-mock-client": "workspace:*",
+ "@ledgerhq/react-ui": "^0.16.2",
"@sentry/nextjs": "^8.32.0",
+ "@playwright/test": "^1.48.2",
"ethers": "^6.13.2",
"next": "14.2.13",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-lottie": "^1.2.4",
+ "rxjs": "^7.8.1",
"styled-components": "^5.3.11"
},
"devDependencies": {
@@ -33,8 +40,7 @@
"@types/react-lottie": "^1.2.10",
"@types/styled-components": "^5.1.25",
"autoprefixer": "^10.4.20",
- "globals": "15.10.0",
- "postcss": "^8.4.47",
- "typescript": "5.6.2"
+ "globals": "15.11.0",
+ "postcss": "^8.4.47"
}
}
diff --git a/apps/sample/playwright.config.ts b/apps/sample/playwright.config.ts
new file mode 100644
index 000000000..700951daa
--- /dev/null
+++ b/apps/sample/playwright.config.ts
@@ -0,0 +1,27 @@
+import { defineConfig, type PlaywrightTestConfig } from "@playwright/test";
+import path from "path";
+
+export const config: PlaywrightTestConfig = {
+ testDir: "./playwright/cases",
+ retries: 2,
+ use: {
+ headless: true,
+ viewport: { width: 1280, height: 720 },
+ actionTimeout: 0,
+ trace: "on",
+ },
+ reporter: [["list"]],
+ reportSlowTests: {
+ max: 0,
+ threshold: 60_000,
+ },
+ webServer: {
+ command: `sh ${path.join(__dirname, "playwright/start-servers.sh")}`,
+ port: 3000,
+ timeout: 120 * 1000,
+ reuseExistingServer: !process.env.CI,
+ },
+};
+
+// eslint-disable-next-line no-restricted-syntax
+export default defineConfig(config);
diff --git a/apps/sample/playwright/cases/device-action_list-apps.spec.ts b/apps/sample/playwright/cases/device-action_list-apps.spec.ts
new file mode 100644
index 000000000..eef568ac8
--- /dev/null
+++ b/apps/sample/playwright/cases/device-action_list-apps.spec.ts
@@ -0,0 +1,46 @@
+/* eslint-disable no-restricted-imports */
+import { expect, test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import { getLastDeviceResponseContent } from "../utils/utils";
+import {
+ whenConnectingDevice,
+ whenExecuteDeviceAction,
+ whenNavigateTo,
+} from "../utils/whenHandlers";
+
+interface ListAppsResponse {
+ status: string;
+ output?: object[];
+ error?: object;
+ pending?: object;
+}
+
+test.describe("device action: list apps", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("device should list apps via device action", async ({ page }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("Then execute list apps via device action", async () => {
+ await whenNavigateTo(page, "/device-actions");
+
+ await whenExecuteDeviceAction(page, "List apps");
+
+ await page.waitForTimeout(1000);
+
+ const response = (await getLastDeviceResponseContent(
+ page,
+ )) as ListAppsResponse;
+
+ expect(response.status).toBe("completed");
+ expect(response.output).toBeInstanceOf(Array);
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/device-action_open-app.spec.ts b/apps/sample/playwright/cases/device-action_open-app.spec.ts
new file mode 100644
index 000000000..fec1e9557
--- /dev/null
+++ b/apps/sample/playwright/cases/device-action_open-app.spec.ts
@@ -0,0 +1,45 @@
+/* eslint-disable no-restricted-imports */
+import { expect, test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import { getLastDeviceResponseContent } from "../utils/utils";
+import {
+ whenConnectingDevice,
+ whenExecuteDeviceAction,
+ whenNavigateTo,
+} from "../utils/whenHandlers";
+
+interface OpenAppResponse {
+ status: string;
+ error?: object;
+ pending?: object;
+}
+
+test.describe("device action: open bitcoin app", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("device should open bitcoin app via device action", async ({ page }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("Then execute open app via device action", async () => {
+ await whenNavigateTo(page, "/device-actions");
+
+ await whenExecuteDeviceAction(page, "Open app", {
+ inputField: "input-text_appName",
+ inputValue: "Bitcoin",
+ });
+
+ const response = (await getLastDeviceResponseContent(
+ page,
+ )) as OpenAppResponse;
+
+ expect(response.status).toBe("completed");
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/device-command_close-bitcoin-app.spec.ts b/apps/sample/playwright/cases/device-command_close-bitcoin-app.spec.ts
new file mode 100644
index 000000000..f29950172
--- /dev/null
+++ b/apps/sample/playwright/cases/device-command_close-bitcoin-app.spec.ts
@@ -0,0 +1,62 @@
+/* eslint-disable no-restricted-imports */
+import { expect, test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import { getLastDeviceResponseContent } from "../utils/utils";
+import {
+ whenCloseDrawer,
+ whenConnectingDevice,
+ whenExecuteDeviceCommand,
+ whenNavigateTo,
+} from "../utils/whenHandlers";
+
+interface CloseAppResponse {
+ status: string;
+ error?: object;
+ pending?: object;
+}
+
+test.describe("device command: close bitcoin app", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("device should open and close bitcoin app via device command", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("Then execute open app via device command", async () => {
+ await whenNavigateTo(page, "/commands");
+
+ await whenExecuteDeviceCommand(page, "Open app", {
+ inputField: "input-text_appName",
+ inputValue: "Bitcoin",
+ });
+
+ const response = (await getLastDeviceResponseContent(
+ page,
+ "span",
+ )) as CloseAppResponse;
+
+ expect(response.status).toBe("SUCCESS");
+ });
+
+ await test.step("Then execute close app via device command", async () => {
+ await whenCloseDrawer(page);
+
+ await whenExecuteDeviceCommand(page, "Close app");
+
+ const response = (await getLastDeviceResponseContent(
+ page,
+ "span",
+ )) as CloseAppResponse;
+
+ expect(response.status).toBe("SUCCESS");
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/device-command_get-app-and-version.spec.ts b/apps/sample/playwright/cases/device-command_get-app-and-version.spec.ts
new file mode 100644
index 000000000..a7abe7dfd
--- /dev/null
+++ b/apps/sample/playwright/cases/device-command_get-app-and-version.spec.ts
@@ -0,0 +1,74 @@
+/* eslint-disable no-restricted-imports */
+import { expect, test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import { getLastDeviceResponseContent } from "../utils/utils";
+import {
+ whenCloseDrawer,
+ whenConnectingDevice,
+ whenExecuteDeviceCommand,
+ whenNavigateTo,
+} from "../utils/whenHandlers";
+
+interface getAppAndVersionResponse {
+ status: string;
+ data?: {
+ name: string;
+ version: string;
+ flags: object;
+ };
+ error?: object;
+ pending?: object;
+}
+
+interface openAppResponse {
+ status: string;
+ error?: object;
+ pending?: object;
+}
+
+test.describe("device command: get app and version", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("device should get app and version via device command", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("Then execute open app via device command", async () => {
+ await whenNavigateTo(page, "/commands");
+
+ await whenExecuteDeviceCommand(page, "Open app", {
+ inputField: "input-text_appName",
+ inputValue: "Bitcoin",
+ });
+
+ const response = (await getLastDeviceResponseContent(
+ page,
+ "span",
+ )) as openAppResponse;
+
+ expect(response.status).toBe("SUCCESS");
+ });
+
+ await test.step("Then execute get app and version via device command", async () => {
+ await whenCloseDrawer(page);
+
+ await whenExecuteDeviceCommand(page, "Get app and version");
+
+ const response = (await getLastDeviceResponseContent(
+ page,
+ "span",
+ )) as getAppAndVersionResponse;
+
+ expect(response.status).toBe("SUCCESS");
+ expect(response?.data?.name).toBe("Bitcoin");
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/device-command_open-bitcoin-app.spec.ts b/apps/sample/playwright/cases/device-command_open-bitcoin-app.spec.ts
new file mode 100644
index 000000000..ba8bce09f
--- /dev/null
+++ b/apps/sample/playwright/cases/device-command_open-bitcoin-app.spec.ts
@@ -0,0 +1,48 @@
+/* eslint-disable no-restricted-imports */
+import { expect, test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import { getLastDeviceResponseContent } from "../utils/utils";
+import {
+ whenConnectingDevice,
+ whenExecuteDeviceCommand,
+ whenNavigateTo,
+} from "../utils/whenHandlers";
+
+interface commandOpenAppResponse {
+ status: string;
+ error?: object;
+ pending?: object;
+}
+
+test.describe("device command: open bitcoin app", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("device should open bitcoin app via device command", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("Then execute open app via device command", async () => {
+ await whenNavigateTo(page, "/commands");
+
+ await whenExecuteDeviceCommand(page, "Open app", {
+ inputField: "input-text_appName",
+ inputValue: "Bitcoin",
+ });
+
+ const response = (await getLastDeviceResponseContent(
+ page,
+ "span",
+ )) as commandOpenAppResponse;
+
+ expect(response.status).toBe("SUCCESS");
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/device-connection.spec.ts b/apps/sample/playwright/cases/device-connection.spec.ts
new file mode 100644
index 000000000..c9c2341ee
--- /dev/null
+++ b/apps/sample/playwright/cases/device-connection.spec.ts
@@ -0,0 +1,33 @@
+/* eslint-disable no-restricted-imports */
+import { test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import { whenConnectingDevice } from "../utils/whenHandlers";
+
+test.describe("device connection", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("first device should connect", async ({ page }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+ });
+
+ test("second device should connect", async ({ page }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("Given second device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 1);
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/device-disconnection.spec.ts b/apps/sample/playwright/cases/device-disconnection.spec.ts
new file mode 100644
index 000000000..c4b9fbff7
--- /dev/null
+++ b/apps/sample/playwright/cases/device-disconnection.spec.ts
@@ -0,0 +1,53 @@
+/* eslint-disable no-restricted-imports */
+import { test } from "@playwright/test";
+
+import {
+ thenDeviceIsConnected,
+ thenNoDeviceIsConnected,
+} from "../utils/thenHandlers";
+import {
+ whenConnectingDevice,
+ whenDisconnectDevice,
+} from "../utils/whenHandlers";
+
+test.describe("device disconnection", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("first device should disconnect", async ({ page }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("Then disconnect device", async () => {
+ await whenDisconnectDevice(page);
+
+ await thenNoDeviceIsConnected(page);
+ });
+ });
+
+ test("first and second devices should disconnect", async ({ page }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("Given second device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 1);
+ });
+
+ await test.step("Then disconnect device", async () => {
+ await whenDisconnectDevice(page);
+
+ await whenDisconnectDevice(page);
+
+ await thenNoDeviceIsConnected(page);
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/eth_get-address_happy-paths.spec.ts b/apps/sample/playwright/cases/eth_get-address_happy-paths.spec.ts
new file mode 100644
index 000000000..06644b21c
--- /dev/null
+++ b/apps/sample/playwright/cases/eth_get-address_happy-paths.spec.ts
@@ -0,0 +1,161 @@
+/* eslint-disable no-restricted-imports */
+import { expect, test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import {
+ getLastDeviceResponseContent,
+ isValidEthereumAddress,
+ isValidPublicKey,
+} from "../utils/utils";
+import {
+ whenClicking,
+ whenConnectingDevice,
+ whenExecute,
+ whenExecuteDeviceAction,
+ whenNavigateTo,
+} from "../utils/whenHandlers";
+
+interface GetAddressResponse {
+ status: string;
+ output?: {
+ publicKey: string;
+ address: string;
+ };
+ error?: object;
+ pending?: object;
+}
+
+test.describe("ETH Signer: get address, happy paths", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("device should return ETH pubKey and address when fed default derivation path", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("When execute ETH: get address", async () => {
+ await whenNavigateTo(page, "/signer");
+
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ await whenExecuteDeviceAction(page, "Get address", {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/0",
+ });
+ });
+
+ await test.step("Then verify that response is successful and it contains an address and pKey", async () => {
+ await page.waitForTimeout(1000);
+
+ const response = (await getLastDeviceResponseContent(
+ page,
+ )) as GetAddressResponse;
+
+ expect(response.status).toBe("completed");
+ expect(isValidEthereumAddress(response?.output?.address || "")).toBe(
+ true,
+ );
+ expect(isValidPublicKey(response?.output?.publicKey || "")).toBe(true);
+ });
+ });
+
+ test("device should return ETH pubKey and address when fed default derivation path and wait to checked on device", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("When execute ETH: get address with checkOnDevice on", async () => {
+ await whenNavigateTo(page, "/signer");
+
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ await whenClicking(page, "CTA_command-Get address");
+
+ await whenClicking(page, "input-switch_checkOnDevice");
+
+ await whenExecute("device-action")(page, "Get address", {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/0",
+ });
+ });
+
+ await test.step("Then verify that response is successful and it contains an address and pKey after timeout", async () => {
+ await page.waitForTimeout(1000);
+ expect(
+ ((await getLastDeviceResponseContent(page)) as GetAddressResponse)
+ .status,
+ ).toBe("pending");
+
+ await page.waitForTimeout(1000);
+ expect(
+ ((await getLastDeviceResponseContent(page)) as GetAddressResponse)
+ .status,
+ ).toBe("pending");
+
+ await page.waitForTimeout(2000);
+ const response = (await getLastDeviceResponseContent(
+ page,
+ )) as GetAddressResponse;
+
+ expect(response.status).toBe("completed");
+ expect(isValidEthereumAddress(response?.output?.address || "")).toBe(
+ true,
+ );
+ expect(isValidPublicKey(response?.output?.publicKey || "")).toBe(true);
+ });
+ });
+
+ test("device should return a different ETH pubKey and address when fed a different derivation path", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("Then execute ETH: get address", async () => {
+ await whenNavigateTo(page, "/signer");
+
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ await whenExecuteDeviceAction(page, "Get address", {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/0",
+ });
+ });
+
+ await test.step("Then veryfy that the address with a different derivation path is different", async () => {
+ await page.waitForTimeout(1000);
+
+ const addressWithFirstAddressIndex = (
+ (await getLastDeviceResponseContent(page)) as GetAddressResponse
+ )?.output?.address;
+
+ await whenExecute("device-action")(page, "Get address", {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/1",
+ });
+
+ await page.waitForTimeout(1000);
+
+ const addressWithSecondAddressIndex = (
+ (await getLastDeviceResponseContent(page)) as GetAddressResponse
+ )?.output?.address;
+
+ expect(addressWithFirstAddressIndex).not.toBe(
+ addressWithSecondAddressIndex,
+ );
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/eth_get-address_unhappy-paths.spec.ts b/apps/sample/playwright/cases/eth_get-address_unhappy-paths.spec.ts
new file mode 100644
index 000000000..35789636d
--- /dev/null
+++ b/apps/sample/playwright/cases/eth_get-address_unhappy-paths.spec.ts
@@ -0,0 +1,79 @@
+/* eslint-disable no-restricted-imports */
+import { expect, test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import { getLastDeviceResponseContent } from "../utils/utils";
+import {
+ whenClicking,
+ whenConnectingDevice,
+ whenExecute,
+ whenExecuteDeviceAction,
+ whenNavigateTo,
+} from "../utils/whenHandlers";
+
+interface GetAddressResponse {
+ status: string;
+ output?: {
+ publicKey: string;
+ address: string;
+ };
+ error?: object;
+ pending?: object;
+}
+
+test.describe("ETH Signer: get address, unhappy paths", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("device should return error if pub key is malformed", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("Then execute ETH: get address with malformed derivation paths", async () => {
+ await whenNavigateTo(page, "/signer");
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ const malformedDerivationPaths = [
+ "aa'/60'/0'/0/0",
+ "44'/aa'/0'/0/0",
+ "44'/60'/aa'/0/0",
+ "44'/60'/0'/aa/0",
+ "44'/60'/0'/0/aa",
+ ];
+
+ await whenExecuteDeviceAction(page, "Get address", {
+ inputField: "input-text_derivationPath",
+ inputValue: malformedDerivationPaths[0],
+ });
+
+ await page.waitForTimeout(1000);
+
+ expect(
+ ((await getLastDeviceResponseContent(page)) as GetAddressResponse)
+ .status,
+ ).toBe("error");
+
+ for (let i = 1; i < malformedDerivationPaths.length; i++) {
+ const path = malformedDerivationPaths[i];
+
+ await whenExecute("device-action")(page, "Get address", {
+ inputField: "input-text_derivationPath",
+ inputValue: path,
+ });
+
+ await page.waitForTimeout(1000);
+
+ expect(
+ ((await getLastDeviceResponseContent(page)) as GetAddressResponse)
+ .status,
+ ).toBe("error");
+ }
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/eth_sign-message_happy-paths.spec.ts b/apps/sample/playwright/cases/eth_sign-message_happy-paths.spec.ts
new file mode 100644
index 000000000..33b8c3b98
--- /dev/null
+++ b/apps/sample/playwright/cases/eth_sign-message_happy-paths.spec.ts
@@ -0,0 +1,193 @@
+/* eslint-disable no-restricted-imports */
+import { expect, test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import { getLastDeviceResponseContent, isValid256BitHex } from "../utils/utils";
+import {
+ whenClicking,
+ whenConnectingDevice,
+ whenExecute,
+ whenExecuteDeviceAction,
+ whenNavigateTo,
+} from "../utils/whenHandlers";
+
+interface SignMessageResponse {
+ status: string;
+ output?: {
+ r: string;
+ s: string;
+ v: string;
+ };
+ error?: object;
+ pending?: object;
+}
+
+test.describe("ETH Signer: sign message, happy paths", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("device should sign a message when fed default derivation path", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("When execute ETH: sign message", async () => {
+ await whenNavigateTo(page, "/signer");
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ await whenExecuteDeviceAction(page, "Sign message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/0",
+ },
+ {
+ inputField: "input-text_message",
+ inputValue: "hello, world!",
+ },
+ ]);
+ });
+
+ await test.step("Then verify the response is successful and contains signed message", async () => {
+ await page.waitForTimeout(1000);
+
+ const response = (await getLastDeviceResponseContent(
+ page,
+ )) as SignMessageResponse;
+
+ expect(response.status).toBe("completed");
+ expect(isValid256BitHex(response?.output?.r || "")).toBe(true);
+ expect(isValid256BitHex(response?.output?.s || "")).toBe(true);
+ });
+ });
+
+ test("device should output a different result when fed a different derivation path", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("When execute ETH: sign message", async () => {
+ await whenNavigateTo(page, "/signer");
+
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ await whenExecuteDeviceAction(page, "Sign message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/0",
+ },
+ {
+ inputField: "input-text_message",
+ inputValue: "hello, world!",
+ },
+ ]);
+ });
+
+ await test.step("Then verify the response with different address index is successful and contains a different signed message", async () => {
+ await page.waitForTimeout(1000);
+
+ const responseWithDefaultDerivationPath =
+ (await getLastDeviceResponseContent(page)) as SignMessageResponse;
+
+ await whenExecute("device-action")(page, "Sign message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/1",
+ },
+ {
+ inputField: "input-text_message",
+ inputValue: "hello, world!",
+ },
+ ]);
+
+ await page.waitForTimeout(1000);
+
+ const responseWithSecondDerivationPath =
+ (await getLastDeviceResponseContent(page)) as SignMessageResponse;
+
+ expect(responseWithDefaultDerivationPath?.output?.r).toBeDefined();
+ expect(responseWithDefaultDerivationPath?.output?.s).toBeDefined();
+ expect(responseWithSecondDerivationPath?.output?.r).toBeDefined();
+ expect(responseWithSecondDerivationPath?.output?.s).toBeDefined();
+
+ expect(responseWithDefaultDerivationPath?.output?.r).not.toBe(
+ responseWithSecondDerivationPath?.output?.r,
+ );
+ expect(responseWithDefaultDerivationPath?.output?.s).not.toBe(
+ responseWithSecondDerivationPath?.output?.s,
+ );
+ });
+ });
+
+ test("device should output a different result when fed a different message", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("When execute ETH: sign message", async () => {
+ await whenNavigateTo(page, "/signer");
+
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ await whenExecuteDeviceAction(page, "Sign message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/0",
+ },
+ {
+ inputField: "input-text_message",
+ inputValue: "hello, world!",
+ },
+ ]);
+ });
+
+ await test.step("Then verify the response with different message is successful and contains a different signed message", async () => {
+ await page.waitForTimeout(1000);
+
+ const responseWithDefaultMessage = (await getLastDeviceResponseContent(
+ page,
+ )) as SignMessageResponse;
+
+ await whenExecute("device-action")(page, "Sign message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/0",
+ },
+ {
+ inputField: "input-text_message",
+ inputValue: "Bonjour le monde!",
+ },
+ ]);
+
+ await page.waitForTimeout(1000);
+
+ const responseWithSecondMessage = (await getLastDeviceResponseContent(
+ page,
+ )) as SignMessageResponse;
+
+ expect(responseWithDefaultMessage?.output?.r).toBeDefined();
+ expect(responseWithDefaultMessage?.output?.s).toBeDefined();
+ expect(responseWithSecondMessage?.output?.r).toBeDefined();
+ expect(responseWithSecondMessage?.output?.s).toBeDefined();
+
+ expect(responseWithDefaultMessage?.output?.r).not.toBe(
+ responseWithSecondMessage?.output?.r,
+ );
+ expect(responseWithDefaultMessage?.output?.s).not.toBe(
+ responseWithSecondMessage?.output?.s,
+ );
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/eth_sign-message_unhappy-paths.spec.ts b/apps/sample/playwright/cases/eth_sign-message_unhappy-paths.spec.ts
new file mode 100644
index 000000000..edf77039f
--- /dev/null
+++ b/apps/sample/playwright/cases/eth_sign-message_unhappy-paths.spec.ts
@@ -0,0 +1,92 @@
+/* eslint-disable no-restricted-imports */
+import { expect, test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import { getLastDeviceResponseContent } from "../utils/utils";
+import {
+ whenClicking,
+ whenConnectingDevice,
+ whenExecute,
+ whenExecuteDeviceAction,
+ whenNavigateTo,
+} from "../utils/whenHandlers";
+
+interface SignMessageResponse {
+ status: string;
+ output?: {
+ r: string;
+ s: string;
+ v: string;
+ };
+ error?: object;
+ pending?: object;
+}
+
+test.describe("ETH Signer: sign message, unhappy paths", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("device should return error if derivation path is malformed when signing a message", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("When execute ETH: sign message with malformed derivation paths", async () => {
+ await whenNavigateTo(page, "/signer");
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ const malformedDerivationPaths = [
+ "aa'/60'/0'/0/0",
+ "44'/aa'/0'/0/0",
+ "44'/60'/aa'/0/0",
+ "44'/60'/0'/aa/0",
+ "44'/60'/0'/0/aa",
+ ];
+
+ await whenExecuteDeviceAction(page, "Sign message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: malformedDerivationPaths[0],
+ },
+ {
+ inputField: "input-text_message",
+ inputValue: "hello, world!",
+ },
+ ]);
+
+ await page.waitForTimeout(1000);
+
+ expect(
+ ((await getLastDeviceResponseContent(page)) as SignMessageResponse)
+ .status,
+ ).toBe("error");
+
+ for (let i = 1; i < malformedDerivationPaths.length; i++) {
+ const path = malformedDerivationPaths[i];
+
+ await whenExecute("device-action")(page, "Sign message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: path,
+ },
+ {
+ inputField: "input-text_message",
+ inputValue: "hello, world!",
+ },
+ ]);
+
+ await page.waitForTimeout(1000);
+
+ expect(
+ ((await getLastDeviceResponseContent(page)) as SignMessageResponse)
+ .status,
+ ).toBe("error");
+ }
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/eth_sign-transaction_happy-paths.spec.ts b/apps/sample/playwright/cases/eth_sign-transaction_happy-paths.spec.ts
new file mode 100644
index 000000000..b15fbabfd
--- /dev/null
+++ b/apps/sample/playwright/cases/eth_sign-transaction_happy-paths.spec.ts
@@ -0,0 +1,133 @@
+/* eslint-disable no-restricted-imports */
+import { expect, test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import { getLastDeviceResponseContent, isValid256BitHex } from "../utils/utils";
+import {
+ whenClicking,
+ whenConnectingDevice,
+ whenExecute,
+ whenExecuteDeviceAction,
+ whenNavigateTo,
+} from "../utils/whenHandlers";
+
+interface SignTransactionResponse {
+ status: string;
+ output?: {
+ r: string;
+ s: string;
+ v: string;
+ };
+ error?: object;
+ pending?: object;
+}
+
+test.describe("ETH Signer: sign transaction, happy paths", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ const rawTransactionHex =
+ "0x02f8b4018325554c847735940085022d0b7c608307a12094dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb000000000000000000000000920ab45225b3057293e760a3c2d74643ad696a1b000000000000000000000000000000000000000000000000000000012a05f200c080a009e2ef5a2c4b7a1d7f0d868388f3949a00a1bdc5669c59b73e57b2a4e7c5e29fa0754aa9f4f1acc99561678492a20c31e01da27d648e69665f7768f96db39220ca";
+
+ test("device should sign a transaction when fed default derivation path", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("When execute ETH: sign transaction", async () => {
+ await whenNavigateTo(page, "/signer");
+
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ await whenExecuteDeviceAction(page, "Sign transaction", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/0",
+ },
+ {
+ inputField: "input-text_transaction",
+ inputValue: rawTransactionHex,
+ },
+ ]);
+ });
+
+ await test.step("Then verify the response is successful and contains signed transaction", async () => {
+ await page.waitForTimeout(1000);
+
+ const response = (await getLastDeviceResponseContent(
+ page,
+ )) as SignTransactionResponse;
+
+ expect(response.status).toBe("completed");
+ expect(isValid256BitHex(response?.output?.r || "")).toBe(true);
+ expect(isValid256BitHex(response?.output?.s || "")).toBe(true);
+ });
+ });
+
+ test("device should output a different result when fed a different derivation path", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("When execute ETH: sign transaction", async () => {
+ await whenNavigateTo(page, "/signer");
+
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ await whenExecuteDeviceAction(page, "Sign transaction", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/0",
+ },
+ {
+ inputField: "input-text_transaction",
+ inputValue: rawTransactionHex,
+ },
+ ]);
+ });
+
+ await test.step("Then verify the response with different address index is successful and contains a different signed message", async () => {
+ await page.waitForTimeout(1000);
+
+ const responseWithDefaultDerivationPath =
+ (await getLastDeviceResponseContent(page)) as SignTransactionResponse;
+
+ await whenExecute("device-action")(page, "Sign transaction", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/1",
+ },
+ {
+ inputField: "input-text_transaction",
+ inputValue: rawTransactionHex,
+ },
+ ]);
+
+ await page.waitForTimeout(1000);
+
+ const responseWithSecondDerivationPath =
+ (await getLastDeviceResponseContent(page)) as SignTransactionResponse;
+
+ expect(responseWithDefaultDerivationPath?.output?.r).toBeDefined();
+ expect(responseWithDefaultDerivationPath?.output?.s).toBeDefined();
+ expect(responseWithSecondDerivationPath?.output?.r).toBeDefined();
+ expect(responseWithSecondDerivationPath?.output?.s).toBeDefined();
+
+ expect(responseWithDefaultDerivationPath?.output?.r).not.toBe(
+ responseWithSecondDerivationPath?.output?.r,
+ );
+ expect(responseWithDefaultDerivationPath?.output?.s).not.toBe(
+ responseWithSecondDerivationPath?.output?.s,
+ );
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/eth_sign-transaction_unhappy-paths.spec.ts b/apps/sample/playwright/cases/eth_sign-transaction_unhappy-paths.spec.ts
new file mode 100644
index 000000000..35b79dd58
--- /dev/null
+++ b/apps/sample/playwright/cases/eth_sign-transaction_unhappy-paths.spec.ts
@@ -0,0 +1,98 @@
+/* eslint-disable no-restricted-imports */
+import { expect, test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import { getLastDeviceResponseContent } from "../utils/utils";
+import {
+ whenClicking,
+ whenConnectingDevice,
+ whenExecute,
+ whenExecuteDeviceAction,
+ whenNavigateTo,
+} from "../utils/whenHandlers";
+
+interface SignTransactionResponse {
+ status: string;
+ output?: {
+ r: string;
+ s: string;
+ v: string;
+ };
+ error?: object;
+ pending?: object;
+}
+
+test.describe("ETH Signer: sign transaction, unhappy paths", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("device should return error if derivation path is malformed when signing a transaction", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("When execute ETH: sign transaction with malformed derivation paths", async () => {
+ await whenNavigateTo(page, "/signer");
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ const malformedDerivationPaths = [
+ "aa'/60'/0'/0/0",
+ "44'/aa'/0'/0/0",
+ "44'/60'/aa'/0/0",
+ "44'/60'/0'/aa/0",
+ "44'/60'/0'/0/aa",
+ ];
+
+ const transactionHex =
+ "0x02f8b4018325554c847735940085022d0b7c608307a12094dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb000000000000000000000000920ab45225b3057293e760a3c2d74643ad696a1b000000000000000000000000000000000000000000000000000000012a05f200c080a009e2ef5a2c4b7a1d7f0d868388f3949a00a1bdc5669c59b73e57b2a4e7c5e29fa0754aa9f4f1acc99561678492a20c31e01da27d648e69665f7768f96db39220ca";
+
+ await whenExecuteDeviceAction(page, "Sign transaction", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: malformedDerivationPaths[0],
+ },
+ {
+ inputField: "input-text_transaction",
+ inputValue: transactionHex,
+ },
+ ]);
+
+ await page.waitForTimeout(1000);
+
+ expect(
+ ((await getLastDeviceResponseContent(page)) as SignTransactionResponse)
+ .status,
+ ).toBe("error");
+
+ for (let i = 1; i < malformedDerivationPaths.length; i++) {
+ const path = malformedDerivationPaths[i];
+
+ await whenExecute("device-action")(page, "Sign transaction", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: path,
+ },
+ {
+ inputField: "input-text_transaction",
+ inputValue: transactionHex,
+ },
+ ]);
+
+ await page.waitForTimeout(1000);
+
+ expect(
+ (
+ (await getLastDeviceResponseContent(
+ page,
+ )) as SignTransactionResponse
+ ).status,
+ ).toBe("error");
+ }
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/eth_sign-typed-message-happy-paths.spec.ts b/apps/sample/playwright/cases/eth_sign-typed-message-happy-paths.spec.ts
new file mode 100644
index 000000000..233231a19
--- /dev/null
+++ b/apps/sample/playwright/cases/eth_sign-typed-message-happy-paths.spec.ts
@@ -0,0 +1,197 @@
+/* eslint-disable no-restricted-imports */
+import { expect, test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import { getLastDeviceResponseContent, isValid256BitHex } from "../utils/utils";
+import {
+ whenClicking,
+ whenConnectingDevice,
+ whenExecute,
+ whenExecuteDeviceAction,
+ whenNavigateTo,
+} from "../utils/whenHandlers";
+
+interface SignEIP712MessageResponse {
+ status: string;
+ output?: {
+ r: string;
+ s: string;
+ v: string;
+ };
+ error?: object;
+ pending?: object;
+}
+
+test.describe("ETH Signer: sign EIP712 message, happy paths", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("device should sign an EIP712 message when fed default derivation path", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("When execute ETH: sign EIP712 message", async () => {
+ await whenNavigateTo(page, "/signer");
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ await whenExecuteDeviceAction(page, "Sign typed message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/0",
+ },
+ {
+ inputField: "input-text_message",
+ inputValue:
+ '{"domain":{"name":"USD Coin","verifyingContract":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","chainId":1,"version":"2"},"primaryType":"Permit","message":{"deadline":1718992051,"nonce":0,"spender":"0x111111125421ca6dc452d289314280a0f8842a65","owner":"0x6cbcd73cd8e8a42844662f0a0e76d7f79afd933d","value":"115792089237316195423570985008687907853269984665640564039457584007913129639935"},"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]}}',
+ },
+ ]);
+ });
+
+ await test.step("Then verify the response is successful and contains signed message", async () => {
+ await page.waitForTimeout(1000);
+
+ const response = (await getLastDeviceResponseContent(
+ page,
+ )) as SignEIP712MessageResponse;
+
+ expect(response.status).toBe("completed");
+ expect(isValid256BitHex(response?.output?.r || "")).toBe(true);
+ expect(isValid256BitHex(response?.output?.s || "")).toBe(true);
+ });
+ });
+
+ test("device should output a different result when fed a different derivation path", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("When execute ETH: sign EIP712 message", async () => {
+ await whenNavigateTo(page, "/signer");
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ await whenExecuteDeviceAction(page, "Sign typed message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/0",
+ },
+ {
+ inputField: "input-text_message",
+ inputValue:
+ '{"domain":{"name":"USD Coin","verifyingContract":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","chainId":1,"version":"2"},"primaryType":"Permit","message":{"deadline":1718992051,"nonce":0,"spender":"0x111111125421ca6dc452d289314280a0f8842a65","owner":"0x6cbcd73cd8e8a42844662f0a0e76d7f79afd933d","value":"115792089237316195423570985008687907853269984665640564039457584007913129639935"},"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]}}',
+ },
+ ]);
+ });
+
+ await test.step("Then verify the response with different address index is successful and contains a different signed message", async () => {
+ await page.waitForTimeout(1000);
+
+ const responseWithDefaultDerivationPath =
+ (await getLastDeviceResponseContent(page)) as SignEIP712MessageResponse;
+
+ await whenExecute("device-action")(page, "Sign typed message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/1",
+ },
+ {
+ inputField: "input-text_message",
+ inputValue:
+ '{"domain":{"name":"USD Coin","verifyingContract":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","chainId":1,"version":"2"},"primaryType":"Permit","message":{"deadline":1718992051,"nonce":0,"spender":"0x111111125421ca6dc452d289314280a0f8842a65","owner":"0x6cbcd73cd8e8a42844662f0a0e76d7f79afd933d","value":"115792089237316195423570985008687907853269984665640564039457584007913129639935"},"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]}}',
+ },
+ ]);
+
+ await page.waitForTimeout(1000);
+
+ const responseWithSecondDerivationPath =
+ (await getLastDeviceResponseContent(page)) as SignEIP712MessageResponse;
+
+ expect(responseWithDefaultDerivationPath?.output?.r).toBeDefined();
+ expect(responseWithDefaultDerivationPath?.output?.s).toBeDefined();
+ expect(responseWithSecondDerivationPath?.output?.r).toBeDefined();
+ expect(responseWithSecondDerivationPath?.output?.s).toBeDefined();
+
+ expect(responseWithDefaultDerivationPath?.output?.r).not.toBe(
+ responseWithSecondDerivationPath?.output?.r,
+ );
+ expect(responseWithDefaultDerivationPath?.output?.s).not.toBe(
+ responseWithSecondDerivationPath?.output?.s,
+ );
+ });
+ });
+
+ test("device should output a different result when fed a different EIP712 message", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("When execute ETH: sign EIP712 message", async () => {
+ await whenNavigateTo(page, "/signer");
+
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ await whenExecuteDeviceAction(page, "Sign typed message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/0",
+ },
+ {
+ inputField: "input-text_message",
+ inputValue:
+ '{"domain":{"name":"USD Coin","verifyingContract":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","chainId":1,"version":"2"},"primaryType":"Permit","message":{"deadline":1718992051,"nonce":0,"spender":"0x111111125421ca6dc452d289314280a0f8842a65","owner":"0x6cbcd73cd8e8a42844662f0a0e76d7f79afd933d","value":"115792089237316195423570985008687907853269984665640564039457584007913129639935"},"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]}}',
+ },
+ ]);
+ });
+
+ await test.step("Then verify the response with different EIP712 message is successful and contains a different signed message", async () => {
+ await page.waitForTimeout(1000);
+
+ const responseWithDefaultMessage = (await getLastDeviceResponseContent(
+ page,
+ )) as SignEIP712MessageResponse;
+
+ await whenExecute("device-action")(page, "Sign typed message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: "44'/60'/0'/0/0",
+ },
+ {
+ inputField: "input-text_message",
+ inputValue:
+ '{"domain":{"name":"USD Coin","verifyingContract":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","chainId":1,"version":"2"},"primaryType":"Permit","message":{"deadline":1718992051,"nonce":0,"spender":"0x111111125421ca6dc452d289314280a0f8842a65","owner":"0x6cbcd73cd8e8a42844662f0a0e76d7f79afda5cd","value":"115792089237316195423570985008687907853269984665640564039457584007913129639935"},"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]}}',
+ },
+ ]);
+
+ await page.waitForTimeout(1000);
+
+ const responseWithSecondMessage = (await getLastDeviceResponseContent(
+ page,
+ )) as SignEIP712MessageResponse;
+
+ expect(responseWithDefaultMessage?.output?.r).toBeDefined();
+ expect(responseWithDefaultMessage?.output?.s).toBeDefined();
+ expect(responseWithSecondMessage?.output?.r).toBeDefined();
+ expect(responseWithSecondMessage?.output?.s).toBeDefined();
+
+ expect(responseWithDefaultMessage?.output?.r).not.toBe(
+ responseWithSecondMessage?.output?.r,
+ );
+ expect(responseWithDefaultMessage?.output?.s).not.toBe(
+ responseWithSecondMessage?.output?.s,
+ );
+ });
+ });
+});
diff --git a/apps/sample/playwright/cases/eth_sign-typed-message-unhappy-paths.spec.ts b/apps/sample/playwright/cases/eth_sign-typed-message-unhappy-paths.spec.ts
new file mode 100644
index 000000000..81ea18096
--- /dev/null
+++ b/apps/sample/playwright/cases/eth_sign-typed-message-unhappy-paths.spec.ts
@@ -0,0 +1,101 @@
+/* eslint-disable no-restricted-imports */
+import { expect, test } from "@playwright/test";
+
+import { thenDeviceIsConnected } from "../utils/thenHandlers";
+import { getLastDeviceResponseContent } from "../utils/utils";
+import {
+ whenClicking,
+ whenConnectingDevice,
+ whenExecute,
+ whenExecuteDeviceAction,
+ whenNavigateTo,
+} from "../utils/whenHandlers";
+
+interface SignEIP712MessageResponse {
+ status: string;
+ output?: {
+ r: string;
+ s: string;
+ v: string;
+ };
+ error?: object;
+ pending?: object;
+}
+
+test.describe("ETH Signer: sign EIP712 message, unhappy paths", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ });
+
+ test("device should return error if derivation path is malformed when signing a typed message", async ({
+ page,
+ }) => {
+ await test.step("Given first device is connected", async () => {
+ await whenConnectingDevice(page);
+
+ await thenDeviceIsConnected(page, 0);
+ });
+
+ await test.step("When execute ETH: sign typed message with malformed derivation paths", async () => {
+ await whenNavigateTo(page, "/signer");
+
+ await whenClicking(page, "CTA_command-Ethereum");
+
+ const malformedDerivationPaths = [
+ "aa'/60'/0'/0/0",
+ "44'/aa'/0'/0/0",
+ "44'/60'/aa'/0/0",
+ "44'/60'/0'/aa/0",
+ "44'/60'/0'/0/aa",
+ ];
+
+ const message = `{"domain":{"name":"USD Coin","verifyingContract":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","chainId":1,"version":"2"},"primaryType":"Permit","message":{"deadline":1718992051,"nonce":0,"spender":"0x111111125421ca6dc452d289314280a0f8842a65","owner":"0x6cbcd73cd8e8a42844662f0a0e76d7f79afd933d","value":"115792089237316195423570985008687907853269984665640564039457584007913129639935"},"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]}}`;
+
+ await whenExecuteDeviceAction(page, "Sign typed message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: malformedDerivationPaths[0],
+ },
+ {
+ inputField: "input-text_message",
+ inputValue: message,
+ },
+ ]);
+
+ await page.waitForTimeout(1000);
+
+ expect(
+ (
+ (await getLastDeviceResponseContent(
+ page,
+ )) as SignEIP712MessageResponse
+ ).status,
+ ).toBe("error");
+
+ for (let i = 1; i < malformedDerivationPaths.length; i++) {
+ const path = malformedDerivationPaths[i];
+
+ await whenExecute("device-action")(page, "Sign typed message", [
+ {
+ inputField: "input-text_derivationPath",
+ inputValue: path,
+ },
+ {
+ inputField: "input-text_message",
+ inputValue: message,
+ },
+ ]);
+
+ await page.waitForTimeout(1000);
+
+ expect(
+ (
+ (await getLastDeviceResponseContent(
+ page,
+ )) as SignEIP712MessageResponse
+ ).status,
+ ).toBe("error");
+ }
+ });
+ });
+});
diff --git a/apps/sample/playwright/start-servers.sh b/apps/sample/playwright/start-servers.sh
new file mode 100644
index 000000000..bd62ae8ae
--- /dev/null
+++ b/apps/sample/playwright/start-servers.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+#echo "Starting mock server..."
+#(cd ../../../.. && cd device-sdk-mock-webserver && ./gradlew run) &
+#MOCK_SERVER_PID=$!
+#
+#while ! nc -z localhost 8080; do
+# echo "Waiting for mock server to start..."
+# sleep 1
+#done
+#echo "mock server is up!"
+
+echo "Starting sample app..."
+(cd .. && pnpm sample dev:default-mock) &
+SAMPLE_APP_PID=$!
+
+while ! nc -z localhost 3000; do
+ echo "Waiting for sample app to start..."
+ sleep 1
+done
+echo "sample app is up!"
+
+# trap to kill the background processes on script exit
+trap "kill $MOCK_SERVER_PID $SAMPLE_APP_PID" EXIT
+
+wait $MOCK_SERVER_PID $SAMPLE_APP_PID
diff --git a/apps/sample/playwright/utils/thenHandlers.ts b/apps/sample/playwright/utils/thenHandlers.ts
new file mode 100644
index 000000000..194fc574b
--- /dev/null
+++ b/apps/sample/playwright/utils/thenHandlers.ts
@@ -0,0 +1,57 @@
+import { expect, type Locator, type Page } from "@playwright/test";
+
+import { asyncPipe } from "@/utils/pipes";
+
+const getDeviceLocator =
+ (deviceIndex: number = 0) =>
+ (page: Page): Page => {
+ const targetChild = page
+ .getByTestId("container_devices")
+ .locator("> *")
+ .nth(deviceIndex);
+ return targetChild as unknown as Page;
+ };
+
+const verifyDeviceConnectedStatus = async (
+ locator: Locator,
+): Promise => {
+ await expect(
+ locator.getByTestId("text_device-connection-status"),
+ ).toContainText("CONNECTED");
+ await expect(
+ locator.getByTestId("text_device-connection-status"),
+ ).toBeVisible();
+ return locator;
+};
+
+export const thenDeviceIsConnected = (
+ page: Page,
+ deviceIndex: number = 0,
+): Promise =>
+ asyncPipe(getDeviceLocator(deviceIndex), verifyDeviceConnectedStatus)(page);
+
+const getAllDeviceNames = async (page: Page): Promise => {
+ return page
+ .getByTestId("container_devices")
+ .locator("> *")
+ .getByTestId("text_device-name")
+ .allTextContents();
+};
+
+const verifyDevicesNotVisible =
+ (deviceNames: string[]) =>
+ async (page: Page): Promise => {
+ await Promise.all(
+ deviceNames.map(async (deviceName) => {
+ await expect(
+ page
+ .getByTestId("text_device-name")
+ .locator(`:has-text("${deviceName}")`),
+ ).not.toBeVisible();
+ }),
+ );
+ return page;
+ };
+
+export const thenNoDeviceIsConnected = (page: Page): Promise =>
+ asyncPipe(getAllDeviceNames, verifyDevicesNotVisible)(page);
diff --git a/apps/sample/playwright/utils/utils.ts b/apps/sample/playwright/utils/utils.ts
new file mode 100644
index 000000000..3af053ad7
--- /dev/null
+++ b/apps/sample/playwright/utils/utils.ts
@@ -0,0 +1,89 @@
+import { type Locator, type Page } from "@playwright/test";
+
+import { asyncPipe } from "@/utils/pipes";
+
+export const getScreenshot = async (
+ page: Page,
+ title: string = "screenshot",
+): Promise => {
+ await page.screenshot({
+ path: `./playwright/${title}.png`,
+ fullPage: true,
+ });
+};
+
+const getResponses = async (page: Page): Promise => {
+ return page
+ .locator('[data-testid="box_device-commands-responses"] > *')
+ .all();
+};
+
+const filterNonHiddenElements = async (
+ responses: Locator[],
+): Promise => {
+ const results = await Promise.all(
+ responses.map(async (response) => {
+ const isHidden = await response.evaluate((el) => {
+ const style = getComputedStyle(el);
+ return style.display === "none" || style.visibility === "hidden";
+ });
+ return !isHidden ? response : null;
+ }),
+ );
+ return results.filter((child) => child !== null) as Locator[];
+};
+
+const getLastResponse = (responses: Locator[]): Locator | null => {
+ return responses.length > 0 ? responses[responses.length - 1] : null;
+};
+
+const getLastChildOfElementByTag =
+ (tagType: string) =>
+ async (element: Locator | null): Promise => {
+ if (!element) return null;
+ try {
+ const children = element.locator(`:scope > ${tagType}`);
+ const lastChild = children.last();
+ await lastChild.waitFor({ state: "attached" });
+ return lastChild;
+ } catch (error) {
+ console.error(
+ `Error getting last child of type '${tagType}' for element: ${error}`,
+ );
+ return null;
+ }
+ };
+
+const parseJSONContent = async (
+ element: Locator | null,
+): Promise => {
+ if (!element) return null;
+ try {
+ const textContent = await element.innerText();
+ return JSON.parse(textContent) as T;
+ } catch (error) {
+ console.error(`Error parsing JSON content: ${error}`);
+ return null;
+ }
+};
+
+export const getLastDeviceResponseContent = async (
+ page: Page,
+ tagType: string = "div > span",
+): Promise =>
+ await asyncPipe(
+ getResponses,
+ filterNonHiddenElements,
+ getLastResponse,
+ getLastChildOfElementByTag(tagType),
+ parseJSONContent,
+ )(page);
+
+export const isValidEthereumAddress = (address: string): boolean =>
+ /^0x[a-fA-F0-9]{40}$/.test(address);
+
+export const isValidPublicKey = (publicKey: string): boolean =>
+ /^04[a-fA-F0-9]{128}$/.test(publicKey);
+
+export const isValid256BitHex = (value: string): boolean =>
+ /^0x[a-fA-F0-9]{64}$/.test(value) || /^[a-fA-F0-9]{64}$/.test(value);
diff --git a/apps/sample/playwright/utils/whenHandlers.ts b/apps/sample/playwright/utils/whenHandlers.ts
new file mode 100644
index 000000000..5c2c0a113
--- /dev/null
+++ b/apps/sample/playwright/utils/whenHandlers.ts
@@ -0,0 +1,107 @@
+import { type Locator, type Page } from "@playwright/test";
+
+import { asyncPipe } from "@/utils/pipes";
+
+type DeviceCommandParams = {
+ inputField?: string;
+ inputValue?: string;
+};
+
+const clickByTestId =
+ (testId: string) =>
+ async (page: Page): Promise => {
+ await page.getByTestId(testId).click();
+ return page;
+ };
+
+const clickBySelector =
+ (selector: string) =>
+ async (page: Page): Promise => {
+ await page.locator(selector).click();
+ return page;
+ };
+
+const waitForNavigation =
+ (route: string) =>
+ async (page: Page): Promise => {
+ const expectedURL = `http://localhost:3000${route}`;
+ await page.waitForURL(expectedURL, { timeout: 10000 });
+ await page.waitForLoadState("networkidle");
+ return page;
+ };
+
+const fillInputFields =
+ (params: DeviceCommandParams | DeviceCommandParams[]) =>
+ async (page: Page): Promise => {
+ const paramsArray = Array.isArray(params) ? params : [params];
+ for (const { inputField, inputValue } of paramsArray) {
+ if (inputField && inputValue) {
+ const input = page.getByTestId(inputField);
+ await input.waitFor({ state: "visible" });
+ await input.fill(inputValue);
+ }
+ }
+ return page;
+ };
+
+export const whenConnectingDevice = (page: Page): Promise =>
+ clickByTestId("CTA_select-device")(page);
+
+export const whenClicking = (page: Page, ctaSelector: string): Promise =>
+ clickByTestId(ctaSelector)(page);
+
+export const whenNavigateTo = (page: Page, route: string): Promise =>
+ asyncPipe(
+ clickByTestId(`CTA_route-to-${route}`),
+ waitForNavigation(route),
+ )(page);
+
+const executeClickCommand =
+ (command: string, navigate: boolean) =>
+ async (page: Page): Promise => {
+ if (navigate) {
+ await page.getByTestId(`CTA_command-${command}`).click();
+ }
+ return page;
+ };
+
+const executeClickSend =
+ (type: string) =>
+ async (page: Page): Promise => {
+ await page.getByTestId(`CTA_send-${type}`).click();
+ return page;
+ };
+
+export const whenExecute =
+ (type: string, navigate: boolean = false) =>
+ (
+ page: Page,
+ command: string,
+ params: DeviceCommandParams | DeviceCommandParams[] = [],
+ ): Promise =>
+ asyncPipe(
+ executeClickCommand(command, navigate),
+ fillInputFields(params),
+ executeClickSend(type),
+ )(page);
+
+export const whenExecuteDeviceAction = whenExecute("device-action", true);
+export const whenExecuteDeviceCommand = whenExecute("device-command", true);
+
+const getFirstDevice = (page: Page): Locator =>
+ page.getByTestId("container_devices").locator("> *").first();
+
+const clickDeviceOptionAndDisconnect = async (page: Page): Promise => {
+ await page.getByTestId("dropdown_device-option").click();
+ await page.getByTestId("CTA_disconnect-device").click();
+ return page;
+};
+
+export const whenDisconnectDevice = (page: Page): Promise =>
+ asyncPipe(getFirstDevice, clickDeviceOptionAndDisconnect)(page);
+
+const drawerCloseButtonSelector =
+ 'svg path[d="M20.328 18.84L13.488 12l6.84-6.84-1.536-1.44L12 10.512 5.208 3.72 3.672 5.16l6.84 6.84-6.84 6.84 1.536 1.44L12 13.488l6.792 6.792 1.536-1.44z"]';
+
+export const whenCloseDrawer = (page: Page): Promise =>
+ clickBySelector(drawerCloseButtonSelector)(page);
diff --git a/apps/sample/src/app/client-layout.tsx b/apps/sample/src/app/client-layout.tsx
index fd1f44cfc..d3fff05f8 100644
--- a/apps/sample/src/app/client-layout.tsx
+++ b/apps/sample/src/app/client-layout.tsx
@@ -9,14 +9,16 @@
*/
"use client";
-import React, { PropsWithChildren } from "react";
+import React, { type PropsWithChildren } from "react";
import { Flex, StyleProvider } from "@ledgerhq/react-ui";
-import styled, { DefaultTheme } from "styled-components";
+import styled, { type DefaultTheme } from "styled-components";
import { Header } from "@/components/Header";
import { Sidebar } from "@/components/Sidebar";
-import { SdkProvider } from "@/providers/DeviceSdkProvider";
+import { DmkProvider } from "@/providers/DeviceManagementKitProvider";
import { DeviceSessionsProvider } from "@/providers/DeviceSessionsProvider";
+import { DmkConfigProvider } from "@/providers/DmkConfig";
+import { SignerEthProvider } from "@/providers/SignerEthProvider";
import { GlobalStyle } from "@/styles/globalstyles";
const Root = styled(Flex)`
@@ -36,22 +38,26 @@ const PageContainer = styled(Flex)`
const ClientRootLayout: React.FC = ({ children }) => {
return (
-
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
);
};
diff --git a/apps/sample/src/app/global-error.tsx b/apps/sample/src/app/global-error.tsx
index d8ff57341..8ac209f14 100644
--- a/apps/sample/src/app/global-error.tsx
+++ b/apps/sample/src/app/global-error.tsx
@@ -2,7 +2,7 @@
import React, { useEffect } from "react";
import * as Sentry from "@sentry/nextjs";
-import Error, { ErrorProps } from "next/error";
+import Error, { type ErrorProps } from "next/error";
export default function GlobalError({ error }: { error: ErrorProps }) {
useEffect(() => {
diff --git a/apps/sample/src/app/keyring/ethereum/page.tsx b/apps/sample/src/app/keyring/ethereum/page.tsx
deleted file mode 100644
index 26cb849dd..000000000
--- a/apps/sample/src/app/keyring/ethereum/page.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-"use client";
-import React from "react";
-
-import { KeyringEthView } from "@/components/KeyringEthView";
-import { SessionIdWrapper } from "@/components/SessionIdWrapper";
-
-const Keyring: React.FC = () => {
- return ;
-};
-
-export default Keyring;
diff --git a/apps/sample/src/app/keyring/page.tsx b/apps/sample/src/app/keyring/page.tsx
deleted file mode 100644
index 134e7bf48..000000000
--- a/apps/sample/src/app/keyring/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-"use client";
-import React from "react";
-
-import { KeyringView } from "@/components/KeyringView";
-
-const Keyring: React.FC = () => {
- return ;
-};
-
-export default Keyring;
diff --git a/apps/sample/src/app/layout.tsx b/apps/sample/src/app/layout.tsx
index ba1b346ab..caa868bab 100644
--- a/apps/sample/src/app/layout.tsx
+++ b/apps/sample/src/app/layout.tsx
@@ -6,7 +6,7 @@
* Combines Styled Components Registry with the ClientRootLayout
* for rendering the application.
*/
-import React, { PropsWithChildren } from "react";
+import React, { type PropsWithChildren } from "react";
import { StyledComponentsRegistry } from "@/lib/registry";
diff --git a/apps/sample/src/app/mock/page.tsx b/apps/sample/src/app/mock/page.tsx
new file mode 100644
index 000000000..44dc7df13
--- /dev/null
+++ b/apps/sample/src/app/mock/page.tsx
@@ -0,0 +1,10 @@
+"use client";
+import React from "react";
+
+import { MockView } from "@/components/MockView";
+
+const Mock: React.FC = () => {
+ return ;
+};
+
+export default Mock;
diff --git a/apps/sample/src/app/signer/ethereum/page.tsx b/apps/sample/src/app/signer/ethereum/page.tsx
new file mode 100644
index 000000000..c297848d3
--- /dev/null
+++ b/apps/sample/src/app/signer/ethereum/page.tsx
@@ -0,0 +1,11 @@
+"use client";
+import React from "react";
+
+import { SessionIdWrapper } from "@/components/SessionIdWrapper";
+import { SignerEthView } from "@/components/SignerEthView";
+
+const Signer: React.FC = () => {
+ return ;
+};
+
+export default Signer;
diff --git a/apps/sample/src/app/signer/page.tsx b/apps/sample/src/app/signer/page.tsx
new file mode 100644
index 000000000..5231d75af
--- /dev/null
+++ b/apps/sample/src/app/signer/page.tsx
@@ -0,0 +1,10 @@
+"use client";
+import React from "react";
+
+import { SignerView } from "@/components/SignerView";
+
+const Signer: React.FC = () => {
+ return ;
+};
+
+export default Signer;
diff --git a/apps/sample/src/app/signer/solana/page.tsx b/apps/sample/src/app/signer/solana/page.tsx
new file mode 100644
index 000000000..85b74a573
--- /dev/null
+++ b/apps/sample/src/app/signer/solana/page.tsx
@@ -0,0 +1,11 @@
+"use client";
+import React from "react";
+
+import { SessionIdWrapper } from "@/components/SessionIdWrapper";
+import { SignerSolanaView } from "@/components/SignerSolanaView";
+
+const Signer: React.FC = () => {
+ return ;
+};
+
+export default Signer;
diff --git a/apps/sample/src/components/ApduView/index.tsx b/apps/sample/src/components/ApduView/index.tsx
index d7344fb04..11d409736 100644
--- a/apps/sample/src/components/ApduView/index.tsx
+++ b/apps/sample/src/components/ApduView/index.tsx
@@ -1,10 +1,10 @@
import React, { useCallback, useState } from "react";
-import { ApduResponse } from "@ledgerhq/device-management-kit";
+import { type ApduResponse } from "@ledgerhq/device-management-kit";
import { Button, Divider, Flex, Grid, Input, Text } from "@ledgerhq/react-ui";
-import styled, { DefaultTheme } from "styled-components";
+import styled, { type DefaultTheme } from "styled-components";
import { useApduForm } from "@/hooks/useApduForm";
-import { useSdk } from "@/providers/DeviceSdkProvider";
+import { useDmk } from "@/providers/DeviceManagementKitProvider";
import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider";
const Root = styled(Flex).attrs({ mx: 15, mt: 10, mb: 5 })`
@@ -69,7 +69,7 @@ export const ApduView: React.FC = () => {
useApduForm();
const [loading, setLoading] = useState(false);
const [apduResponse, setApduResponse] = useState();
- const sdk = useSdk();
+ const dmk = useDmk();
const {
state: { selectedId: selectedSessionId },
} = useDeviceSessionsContext();
@@ -78,7 +78,7 @@ export const ApduView: React.FC = () => {
setLoading(true);
let rawApduResponse;
try {
- rawApduResponse = await sdk.sendApdu({
+ rawApduResponse = await dmk.sendApdu({
sessionId: selectedSessionId ?? "",
apdu: getRawApdu(values),
});
@@ -89,7 +89,7 @@ export const ApduView: React.FC = () => {
setLoading(false);
}
},
- [getRawApdu, sdk, selectedSessionId],
+ [getRawApdu, dmk, selectedSessionId],
);
return (
diff --git a/apps/sample/src/components/AvailableDevices/index.tsx b/apps/sample/src/components/AvailableDevices/index.tsx
new file mode 100644
index 000000000..02fbb759f
--- /dev/null
+++ b/apps/sample/src/components/AvailableDevices/index.tsx
@@ -0,0 +1,94 @@
+import React, { useCallback, useState } from "react";
+import { type DiscoveredDevice } from "@ledgerhq/device-management-kit";
+import { Flex, Icons, Text } from "@ledgerhq/react-ui";
+import styled from "styled-components";
+
+import { AvailableDevice } from "@/components/Device";
+import { useAvailableDevices } from "@/hooks/useAvailableDevices";
+import { useDmk } from "@/providers/DeviceManagementKitProvider";
+import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider";
+
+const Title = styled(Text)<{ disabled: boolean }>`
+ :hover {
+ user-select: none;
+ text-decoration: ${(p) => (p.disabled ? "none" : "underline")};
+ cursor: ${(p) => (p.disabled ? "default" : "pointer")};
+ }
+`;
+
+export const AvailableDevices: React.FC> = () => {
+ const discoveredDevices = useAvailableDevices();
+ const noDevice = discoveredDevices.length === 0;
+
+ const [unfolded, setUnfolded] = useState(false);
+
+ const toggleUnfolded = useCallback(() => {
+ setUnfolded((prev) => !prev);
+ }, []);
+
+ return (
+ <>
+
+
+ Available devices ({discoveredDevices.length})
+
+
+ {unfolded ? (
+
+ ) : (
+
+ )}
+
+
+
+ {unfolded
+ ? discoveredDevices.map((device) => (
+
+ ))
+ : null}
+
+ >
+ );
+};
+
+const KnownDevice: React.FC = (
+ device,
+) => {
+ const { deviceModel, connected } = device;
+ const dmk = useDmk();
+ const { dispatch } = useDeviceSessionsContext();
+ const connectToDevice = useCallback(() => {
+ dmk.connect({ device }).then((sessionId) => {
+ dispatch({
+ type: "add_session",
+ payload: {
+ sessionId,
+ connectedDevice: dmk.getConnectedDevice({ sessionId }),
+ },
+ });
+ });
+ }, [dmk, device, dispatch]);
+
+ return (
+
+
+
+ );
+};
diff --git a/apps/sample/src/components/CalView/CalAvailabilityResponse.tsx b/apps/sample/src/components/CalView/CalAvailabilityResponse.tsx
index f26f1300d..69cae7b03 100644
--- a/apps/sample/src/components/CalView/CalAvailabilityResponse.tsx
+++ b/apps/sample/src/components/CalView/CalAvailabilityResponse.tsx
@@ -1,7 +1,7 @@
import React from "react";
import { Box, Flex, Icons, InfiniteLoader, Text } from "@ledgerhq/react-ui";
-import { Descriptor } from "./CalNetworkDataSource";
+import { type Descriptor } from "./CalNetworkDataSource";
type CalAvailabilityResponseProps = {
loading: boolean;
diff --git a/apps/sample/src/components/CalView/CalCheckDappDrawer.tsx b/apps/sample/src/components/CalView/CalCheckDappDrawer.tsx
new file mode 100644
index 000000000..cd75a04bd
--- /dev/null
+++ b/apps/sample/src/components/CalView/CalCheckDappDrawer.tsx
@@ -0,0 +1,169 @@
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import {
+ Button,
+ Divider,
+ Flex,
+ Icons,
+ InfiniteLoader,
+} from "@ledgerhq/react-ui";
+
+import { Block } from "@/components/Block";
+import {
+ CommandForm,
+ type ValueSelector,
+} from "@/components/CommandsView/CommandForm";
+import { type FieldType } from "@/hooks/useForm";
+import { useCalConfig } from "@/providers/SignerEthProvider";
+
+import { CalAvailabilityResponseComponent } from "./CalAvailabilityResponse";
+import {
+ checkContractAvailability,
+ type Descriptor,
+} from "./CalNetworkDataSource";
+
+export type CalCheckDappDrawerProps<
+ _,
+ Input extends Record | void,
+> = {
+ title: string;
+ description: string;
+ initialValues: Input;
+ validateValues?: (args: Input) => boolean;
+ valueSelector?: ValueSelector;
+};
+
+type Response = {
+ searchAddress: string;
+ date: Date;
+ responseType: string;
+ result: Descriptor[]; // Store the result from the API
+ loading: false;
+ id: number;
+};
+
+export function CalCheckDappDrawer<
+ Output,
+ Input extends Record,
+>(props: CalCheckDappDrawerProps) {
+ const { initialValues, valueSelector, validateValues } = props;
+
+ const nonce = useRef(-1);
+ const [values, setValues] = useState (initialValues);
+ const [valuesInvalid, setValuesInvalid] = useState(false);
+ const [responses, setResponses] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const { calConfig } = useCalConfig();
+ const handleClickExecute = useCallback(() => {
+ setLoading(true);
+ const id = ++nonce.current;
+
+ const fetchData = async () => {
+ try {
+ console.log("Trigger Request to checkContractAvailability");
+
+ const response = await checkContractAvailability(
+ values.smartContractAddress.toString(),
+ calConfig.url,
+ calConfig.branch,
+ );
+
+ setResponses((prev) => [
+ ...prev,
+ {
+ searchAddress: values.smartContractAddress.toString(),
+ date: new Date(),
+ responseType: response.responseType,
+ result: response.descriptors, // Store the result from the API
+ loading: false,
+ id,
+ },
+ ]);
+
+ setLoading(false);
+ } catch (_error) {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [values]);
+
+ const handleClickClear = useCallback(() => {
+ setResponses([]);
+ }, []);
+
+ const responseBoxRef = useRef(null);
+
+ useEffect(() => {
+ if (responseBoxRef.current) {
+ responseBoxRef.current.scrollTop = responseBoxRef.current.scrollHeight;
+ }
+ }, [responses]);
+
+ useEffect(() => {
+ if (validateValues) {
+ setValuesInvalid(!validateValues(values));
+ }
+ }, [validateValues, values]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+ loading ? :
+ }
+ >
+ Check Availability
+
+
+
+
+
+ {responses.slice().map((response, key) => (
+
+ ))}
+
+
+ Clear responses
+
+
+ >
+ );
+}
diff --git a/apps/sample/src/components/CalView/CalNetworkDataSource.ts b/apps/sample/src/components/CalView/CalNetworkDataSource.ts
index 506030b34..b8db79382 100644
--- a/apps/sample/src/components/CalView/CalNetworkDataSource.ts
+++ b/apps/sample/src/components/CalView/CalNetworkDataSource.ts
@@ -156,7 +156,7 @@ async function fetchRequest(
endpoint: string,
branch: string,
): Promise {
- const path = `${endpoint}/v1/dapps?ref=branch%3A${branch}&output=${output}&chain_id=1&contracts=${contractName}`;
+ const path = `${endpoint}/dapps?ref=branch%3A${branch}&output=${output}&chain_id=1&contracts=${contractName}`;
const response = await fetch(path);
if (!response.ok) {
diff --git a/apps/sample/src/components/CalView/CalSettingsDrawer.tsx b/apps/sample/src/components/CalView/CalSettingsDrawer.tsx
new file mode 100644
index 000000000..0f7f31682
--- /dev/null
+++ b/apps/sample/src/components/CalView/CalSettingsDrawer.tsx
@@ -0,0 +1,88 @@
+import React, { useCallback, useState } from "react";
+import { type ContextModuleCalConfig } from "@ledgerhq/context-module";
+import { Button, Divider, Flex } from "@ledgerhq/react-ui";
+
+import { Block } from "@/components/Block";
+import {
+ CommandForm,
+ type ValueSelector,
+} from "@/components/CommandsView/CommandForm";
+import { type FieldType } from "@/hooks/useForm";
+import { useCalConfig } from "@/providers/SignerEthProvider";
+
+type CalSettingsDrawerProps = {
+ onClose: () => void;
+};
+
+export function CalSettingsDrawer({ onClose }: CalSettingsDrawerProps) {
+ const { calConfig, setCalConfig } = useCalConfig();
+ const [values, setValues] = useState>(calConfig);
+ const valueSelector: ValueSelector = {
+ mode: [
+ { label: "Production", value: "prod" },
+ { label: "Testing", value: "test" },
+ ],
+ branch: [
+ { label: "Main", value: "main" },
+ { label: "Next", value: "next" },
+ { label: "Demo", value: "demo" },
+ ],
+ };
+ const labelSelector: Record = {
+ url: "CAL URL",
+ mode: "Mode",
+ branch: "Branch reference",
+ };
+
+ const onSettingsUpdate = useCallback(() => {
+ const { url, mode, branch } = values;
+ const isMode = (test: unknown): test is "prod" | "test" =>
+ test === "prod" || test === "test";
+ const isBranch = (test: unknown): test is "main" | "next" | "demo" =>
+ test === "main" || test === "next" || test === "demo";
+
+ console.log("Updating settings", values);
+ if (!url || typeof url !== "string" || !url.startsWith("http")) {
+ console.error("Invalid CAL URL", url);
+ return;
+ }
+
+ if (!mode || !isMode(mode)) {
+ console.error("Invalid mode", mode);
+ return;
+ }
+
+ if (!branch || !isBranch(branch)) {
+ console.error("Invalid branch reference", branch);
+ return;
+ }
+
+ const newSettings: ContextModuleCalConfig = {
+ url,
+ mode,
+ branch,
+ };
+
+ setCalConfig(newSettings);
+ onClose();
+ }, [onClose, setCalConfig, values]);
+
+ return (
+
+
+
+
+
+
+
+ Update settings
+
+
+
+ );
+}
diff --git a/apps/sample/src/components/CalView/index.tsx b/apps/sample/src/components/CalView/index.tsx
index 1c9c60dff..6875670f1 100644
--- a/apps/sample/src/components/CalView/index.tsx
+++ b/apps/sample/src/components/CalView/index.tsx
@@ -1,221 +1,84 @@
-import React, { useCallback, useEffect, useRef, useState } from "react";
-import {
- Button,
- Divider,
- Flex,
- Grid,
- Icons,
- InfiniteLoader,
-} from "@ledgerhq/react-ui";
+import React, { useCallback, useState } from "react";
+import { Grid } from "@ledgerhq/react-ui";
-import { Block } from "@/components/Block";
import { ClickableListItem } from "@/components/ClickableListItem";
-import {
- CommandForm,
- ValueSelector,
-} from "@/components/CommandsView/CommandForm";
import { PageWithHeader } from "@/components/PageWithHeader";
import { StyledDrawer } from "@/components/StyledDrawer";
-import { FieldType } from "@/hooks/useForm";
-import { CalAvailabilityResponseComponent } from "./CalAvailabilityResponse";
-import { checkContractAvailability, Descriptor } from "./CalNetworkDataSource";
+import { CalCheckDappDrawer } from "./CalCheckDappDrawer";
+import { CalSettingsDrawer } from "./CalSettingsDrawer";
-const CAL_SERVICE_ENTRIES = [
- {
- title: "Check dApp availability",
- description: "Check dApp availability in Crypto Asset List",
- },
-];
-
-export type CalActionProps<
- _,
- Input extends Record | void,
-> = {
- title: string;
- description: string;
- initialValues: Input;
- validateValues?: (args: Input) => boolean;
- valueSelector?: ValueSelector;
-};
-
-type Response = {
- searchAddress: string;
- date: Date;
- responseType: string;
- result: Descriptor[]; // Store the result from the API
- loading: false;
- id: number;
-};
-
-export function CalActionDrawer<
- Output,
- Input extends Record,
->(props: CalActionProps) {
- const { initialValues, valueSelector, validateValues } = props;
-
- const nonce = useRef(-1);
- const [values, setValues] = useState (initialValues);
- const [valuesInvalid, setValuesInvalid] = useState(false);
- const [responses, setResponses] = useState([]);
- const [loading, setLoading] = useState(false);
- const handleClickExecute = useCallback(() => {
- setLoading(true);
- const id = ++nonce.current;
-
- const fetchData = async () => {
- try {
- console.log("Trigger Request to checkContractAvailability");
-
- const response = await checkContractAvailability(
- values.smartContractAddress.toString(),
- values.calUrl.toString(),
- values.branch.toString(),
- );
-
- setResponses((prev) => [
- ...prev,
- {
- searchAddress: values.smartContractAddress.toString(),
- date: new Date(),
- responseType: response.responseType,
- result: response.descriptors, // Store the result from the API
- loading: false,
- id,
- },
- ]);
-
- setLoading(false);
- } catch (_error) {
- setLoading(false);
- }
- };
-
- fetchData();
- }, [values]);
-
- const handleClickClear = useCallback(() => {
- setResponses([]);
+export const CalView = () => {
+ const [isCheckDappOpen, setIsCheckDappOpen] = useState(false);
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false);
+ const openCheckDapp = useCallback(() => {
+ setIsCheckDappOpen(true);
}, []);
- const responseBoxRef = useRef(null);
-
- useEffect(() => {
- if (responseBoxRef.current) {
- responseBoxRef.current.scrollTop = responseBoxRef.current.scrollHeight;
- }
- }, [responses]);
-
- useEffect(() => {
- if (validateValues) {
- setValuesInvalid(!validateValues(values));
- }
- }, [validateValues, values]);
-
- return (
- <>
-
-
-
-
-
-
-
- loading ? :
- }
- >
- Check Availability
-
-
-
-
-
- {responses.slice().map((response, key) => (
-
- ))}
-
-
- Clear responses
-
-
- >
- );
-}
-
-export const CalView = () => {
- const [isOpen, setIsOpen] = useState(false);
- const openDrawer = useCallback(() => {
- setIsOpen(true);
+ const openSettings = useCallback(() => {
+ setIsSettingsOpen(true);
}, []);
- const closeDrawer = useCallback(() => {
- setIsOpen(false);
+ const closeDrawers = useCallback(() => {
+ setIsCheckDappOpen(false);
+ setIsSettingsOpen(false);
}, []);
- const title = "Check dApp availability";
- const description = "Check descriptor availability on the CAL";
+ const entries = [
+ {
+ title: "Settings",
+ description: "Settings for the Crypto Asset List",
+ onClick: openSettings,
+ },
+ {
+ title: "Check dApp availability",
+ description: "Check dApp availability in Crypto Asset List",
+ onClick: openCheckDapp,
+ },
+ ];
+
+ const pageTitle = "Check dApp availability";
+ const pageDescription = "Check descriptor availability on the CAL";
return (
-
- {CAL_SERVICE_ENTRIES.map(({ title, description }) => (
+
+ {entries.map(({ title, description, onClick }) => (
))}
-
+
+
+
);
};
diff --git a/apps/sample/src/components/ClickableListItem.tsx b/apps/sample/src/components/ClickableListItem.tsx
index 5c2ce4b04..8b4237b81 100644
--- a/apps/sample/src/components/ClickableListItem.tsx
+++ b/apps/sample/src/components/ClickableListItem.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { Flex, Icons, Text } from "@ledgerhq/react-ui";
+import { type BaseStyledProps } from "@ledgerhq/react-ui/components/styled";
import styled from "styled-components";
const ListItemWrapper = styled(Flex)`
@@ -12,12 +13,14 @@ const ListItemWrapper = styled(Flex)`
cursor: pointer;
`;
-export const ClickableListItem: React.FC<{
- title: string;
- description: string;
- onClick(): void;
- icon?: React.ReactNode;
-}> = ({ title, description, onClick, icon }) => {
+export const ClickableListItem: React.FC<
+ {
+ title: string;
+ description: string;
+ onClick(): void;
+ icon?: React.ReactNode;
+ } & BaseStyledProps
+> = ({ title, description, onClick, icon, ...styleProps }) => {
return (
{icon}
diff --git a/apps/sample/src/components/CommandsView/Command.tsx b/apps/sample/src/components/CommandsView/Command.tsx
index c773c7c61..06311b8b8 100644
--- a/apps/sample/src/components/CommandsView/Command.tsx
+++ b/apps/sample/src/components/CommandsView/Command.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from "react";
import {
- CommandResult,
+ type CommandResult,
isSuccessCommandResult,
} from "@ledgerhq/device-management-kit";
import { Button, Flex, Icons, InfiniteLoader } from "@ledgerhq/react-ui";
@@ -8,10 +8,10 @@ import { Button, Flex, Icons, InfiniteLoader } from "@ledgerhq/react-ui";
import { Block } from "@/components/Block";
import { ClickableListItem } from "@/components/ClickableListItem";
import { StyledDrawer } from "@/components/StyledDrawer";
-import { FieldType } from "@/hooks/useForm";
+import { type FieldType } from "@/hooks/useForm";
-import { CommandForm, ValueSelector } from "./CommandForm";
-import { CommandResponse, CommandResponseProps } from "./CommandResponse";
+import { CommandForm, type ValueSelector } from "./CommandForm";
+import { CommandResponse, type CommandResponseProps } from "./CommandResponse";
export type CommandProps<
CommandArgs extends Record | void,
@@ -79,6 +79,13 @@ export function Command<
];
});
})
+ .catch((error) => {
+ setLoading(false);
+ setResponses((prev) => [
+ ...prev.slice(0, -1),
+ { args: values, date: new Date(), loading: false, response: error },
+ ]);
+ })
.finally(() => {
setLoading(false);
});
@@ -111,7 +118,7 @@ export function Command<
title={title}
description={description}
>
-
+
loading ? :
}
+ data-testid="CTA_send-device-command"
>
Send
@@ -135,6 +143,7 @@ export function Command<
rowGap={4}
flex={1}
overflowY="scroll"
+ data-testid="box_device-commands-responses"
>
{responses.map(({ args, date, response, loading }, index) => (
= Record<
string,
@@ -26,11 +26,13 @@ export function CommandForm>({
initialValues,
onChange,
valueSelector,
+ labelSelector,
disabled,
}: {
initialValues: Args;
onChange: (values: Args) => void;
valueSelector?: ValueSelector;
+ labelSelector?: Record;
disabled?: boolean;
}) {
const { formValues, setFormValue } = useForm(initialValues);
@@ -53,7 +55,7 @@ export function CommandForm>({
>
{typeof value === "boolean" ? null : (
- {key}
+ {labelSelector && labelSelector[key] ? labelSelector[key] : key}
)}
{valueSelector?.[key] ? (
@@ -69,13 +71,15 @@ export function CommandForm>({
/>
) : typeof value === "boolean" ? (
- setFormValue(key, !value)}
- disabled={disabled}
- label={key}
- />
+
+ setFormValue(key, !value)}
+ disabled={disabled}
+ label={key}
+ />
+
) : typeof value === "string" ? (
>({
placeholder={key}
onChange={(newVal) => setFormValue(key, newVal)}
disabled={disabled}
+ data-testid={`input-text_${key}`}
/>
) : (
setFormValue(key, newVal ?? 0)}
+ onChange={(newVal) =>
+ setFormValue(key, parseInt(newVal.toString(), 10) ?? 0)
+ }
type="number"
disabled={disabled}
/>
diff --git a/apps/sample/src/components/CommandsView/CommandResponse.tsx b/apps/sample/src/components/CommandsView/CommandResponse.tsx
index fee3f4c6c..3f7584dae 100644
--- a/apps/sample/src/components/CommandsView/CommandResponse.tsx
+++ b/apps/sample/src/components/CommandsView/CommandResponse.tsx
@@ -1,11 +1,11 @@
import React from "react";
import {
- CommandResult,
+ type CommandResult,
isSuccessCommandResult,
} from "@ledgerhq/device-management-kit";
import { Flex, InfiniteLoader, Text, Tooltip } from "@ledgerhq/react-ui";
-import { FieldType } from "@/hooks/useForm";
+import { type FieldType } from "@/hooks/useForm";
export type CommandResponseProps = {
args: Record;
diff --git a/apps/sample/src/components/CommandsView/index.tsx b/apps/sample/src/components/CommandsView/index.tsx
index bde4df4e2..2e76861c0 100644
--- a/apps/sample/src/components/CommandsView/index.tsx
+++ b/apps/sample/src/components/CommandsView/index.tsx
@@ -1,34 +1,34 @@
import React, { useMemo } from "react";
import {
+ BatteryStatusType,
CloseAppCommand,
GetAppAndVersionCommand,
- GetAppAndVersionResponse,
- GetBatteryStatusArgs,
+ type GetAppAndVersionResponse,
+ type GetBatteryStatusArgs,
GetBatteryStatusCommand,
- GetBatteryStatusResponse,
+ type GetBatteryStatusResponse,
GetOsVersionCommand,
- GetOsVersionResponse,
- ListAppsArgs,
+ type GetOsVersionResponse,
+ type ListAppsArgs,
ListAppsCommand,
- ListAppsErrorCodes,
- ListAppsResponse,
- OpenAppArgs,
+ type ListAppsErrorCodes,
+ type ListAppsResponse,
+ type OpenAppArgs,
OpenAppCommand,
- OpenAppErrorCodes,
+ type OpenAppErrorCodes,
} from "@ledgerhq/device-management-kit";
-import { BatteryStatusType } from "@ledgerhq/device-management-kit/src/api/command/os/GetBatteryStatusCommand.js";
import { Grid } from "@ledgerhq/react-ui";
import { PageWithHeader } from "@/components/PageWithHeader";
-import { useSdk } from "@/providers/DeviceSdkProvider";
+import { useDmk } from "@/providers/DeviceManagementKitProvider";
-import { Command, CommandProps } from "./Command";
+import { Command, type CommandProps } from "./Command";
import { getValueSelectorFromEnum } from "./CommandForm";
export const CommandsView: React.FC<{ sessionId: string }> = ({
sessionId: selectedSessionId,
}) => {
- const sdk = useSdk();
+ const dmk = useDmk();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const commands: CommandProps[] = useMemo(
@@ -38,7 +38,7 @@ export const CommandsView: React.FC<{ sessionId: string }> = ({
description: "List all apps on the device",
sendCommand: ({ isContinue }) => {
const command = new ListAppsCommand({ isContinue });
- return sdk.sendCommand({
+ return dmk.sendCommand({
sessionId: selectedSessionId,
command,
});
@@ -54,7 +54,7 @@ export const CommandsView: React.FC<{ sessionId: string }> = ({
description: "Launch an app on the device",
sendCommand: ({ appName }) => {
const command = new OpenAppCommand({ appName });
- return sdk.sendCommand({
+ return dmk.sendCommand({
sessionId: selectedSessionId,
command,
});
@@ -67,7 +67,7 @@ export const CommandsView: React.FC<{ sessionId: string }> = ({
description: "Close the currently open app",
sendCommand: () => {
const command = new CloseAppCommand();
- return sdk.sendCommand({
+ return dmk.sendCommand({
sessionId: selectedSessionId,
command,
});
@@ -79,7 +79,7 @@ export const CommandsView: React.FC<{ sessionId: string }> = ({
description: "Get the currently open app and its version",
sendCommand: () => {
const command = new GetAppAndVersionCommand();
- return sdk.sendCommand({
+ return dmk.sendCommand({
sessionId: selectedSessionId,
command,
});
@@ -91,7 +91,7 @@ export const CommandsView: React.FC<{ sessionId: string }> = ({
description: "Get the OS version of the device",
sendCommand: () => {
const command = new GetOsVersionCommand();
- return sdk.sendCommand({
+ return dmk.sendCommand({
sessionId: selectedSessionId,
command,
});
@@ -103,7 +103,7 @@ export const CommandsView: React.FC<{ sessionId: string }> = ({
description: "Get the battery status of the device",
sendCommand: ({ statusType }) => {
const command = new GetBatteryStatusCommand({ statusType });
- return sdk.sendCommand({
+ return dmk.sendCommand({
sessionId: selectedSessionId,
command,
});
@@ -116,7 +116,7 @@ export const CommandsView: React.FC<{ sessionId: string }> = ({
},
} satisfies CommandProps,
],
- [selectedSessionId, sdk],
+ [selectedSessionId, dmk],
);
return (
diff --git a/apps/sample/src/components/Device/StatusText.tsx b/apps/sample/src/components/Device/StatusText.tsx
index cd39121f5..6c033a63c 100644
--- a/apps/sample/src/components/Device/StatusText.tsx
+++ b/apps/sample/src/components/Device/StatusText.tsx
@@ -1,6 +1,6 @@
import { DeviceStatus } from "@ledgerhq/device-management-kit";
import { Text } from "@ledgerhq/react-ui";
-import styled, { DefaultTheme } from "styled-components";
+import styled, { type DefaultTheme } from "styled-components";
const getColorFromState = ({
state,
diff --git a/apps/sample/src/components/Device/index.tsx b/apps/sample/src/components/Device/index.tsx
index 09f7f2f1b..580dd8a34 100644
--- a/apps/sample/src/components/Device/index.tsx
+++ b/apps/sample/src/components/Device/index.tsx
@@ -1,13 +1,21 @@
import React from "react";
import {
- ConnectionType,
+ type ConnectionType,
DeviceModelId,
- DeviceSessionId,
+ type DeviceSessionId,
} from "@ledgerhq/device-management-kit";
-import { Box, DropdownGeneric, Flex, Icons, Text } from "@ledgerhq/react-ui";
-import styled, { DefaultTheme } from "styled-components";
+import {
+ Box,
+ Button,
+ DropdownGeneric,
+ Flex,
+ Icons,
+ Text,
+} from "@ledgerhq/react-ui";
+import styled, { type DefaultTheme } from "styled-components";
import { useDeviceSessionState } from "@/hooks/useDeviceSessionState";
+import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider";
import { StatusText } from "./StatusText";
@@ -15,6 +23,10 @@ const Root = styled(Flex).attrs({ p: 5, mb: 8, borderRadius: 2 })`
background: ${({ theme }: { theme: DefaultTheme }) =>
theme.colors.neutral.c30};
align-items: center;
+ border: ${({ active, theme }: { theme: DefaultTheme; active: boolean }) =>
+ `1px solid ${active ? theme.colors.success.c40 : "transparent"}`};
+ cursor: ${({ active }: { active: boolean }) =>
+ active ? "normal" : "pointer"};
`;
const IconContainer = styled(Flex).attrs({ p: 4, mr: 3, borderRadius: 100 })`
@@ -41,6 +53,7 @@ type DeviceProps = {
sessionId: DeviceSessionId;
model: DeviceModelId;
onDisconnect: () => Promise;
+ onSelect: () => void;
};
function getIconComponent(model: DeviceModelId) {
@@ -61,21 +74,31 @@ export const Device: React.FC = ({
type,
model,
onDisconnect,
+ onSelect,
sessionId,
}) => {
const sessionState = useDeviceSessionState(sessionId);
+ const {
+ state: { selectedId },
+ } = useDeviceSessionsContext();
const IconComponent = getIconComponent(model);
+ const isActive = selectedId === sessionId;
return (
-
+
- {name}
+
+ {name}
+
{sessionState && (
<>
-
+
{sessionState.deviceStatus}
âĸ
@@ -86,14 +109,57 @@ export const Device: React.FC = ({
-
-
+
+
+
+
+ Disconnect
+
+
+
+
+
+
+ );
+};
+
+type AvailableDeviceProps = {
+ model: DeviceModelId;
+ name: string;
+ type: ConnectionType;
+ connected: boolean;
+ onConnect: () => void;
+};
+
+export const AvailableDevice: React.FC = ({
+ model,
+ name,
+ type,
+ onConnect,
+ connected,
+}) => {
+ const IconComponent = getIconComponent(model);
+ return (
+
+
+
+
+
+ {name}
+
- Disconnect
+ {type}
-
-
-
+
+
+
+ Connect
+
);
};
diff --git a/apps/sample/src/components/DeviceActionsView/AllDeviceActions.tsx b/apps/sample/src/components/DeviceActionsView/AllDeviceActions.tsx
index 962b791f4..dea6ed349 100644
--- a/apps/sample/src/components/DeviceActionsView/AllDeviceActions.tsx
+++ b/apps/sample/src/components/DeviceActionsView/AllDeviceActions.tsx
@@ -1,44 +1,44 @@
import React from "react";
import { useMemo } from "react";
import {
- GetDeviceStatusDAError,
- GetDeviceStatusDAInput,
- GetDeviceStatusDAIntermediateValue,
- GetDeviceStatusDAOutput,
+ type GetDeviceStatusDAError,
+ type GetDeviceStatusDAInput,
+ type GetDeviceStatusDAIntermediateValue,
+ type GetDeviceStatusDAOutput,
GetDeviceStatusDeviceAction,
- GoToDashboardDAError,
- GoToDashboardDAInput,
- GoToDashboardDAIntermediateValue,
- GoToDashboardDAOutput,
+ type GoToDashboardDAError,
+ type GoToDashboardDAInput,
+ type GoToDashboardDAIntermediateValue,
+ type GoToDashboardDAOutput,
GoToDashboardDeviceAction,
- ListAppsDAError,
- ListAppsDAInput,
- ListAppsDAIntermediateValue,
- ListAppsDAOutput,
+ type ListAppsDAError,
+ type ListAppsDAInput,
+ type ListAppsDAIntermediateValue,
+ type ListAppsDAOutput,
ListAppsDeviceAction,
- ListAppsWithMetadataDAError,
- ListAppsWithMetadataDAInput,
- ListAppsWithMetadataDAIntermediateValue,
- ListAppsWithMetadataDAOutput,
+ type ListAppsWithMetadataDAError,
+ type ListAppsWithMetadataDAInput,
+ type ListAppsWithMetadataDAIntermediateValue,
+ type ListAppsWithMetadataDAOutput,
ListAppsWithMetadataDeviceAction,
- OpenAppDAError,
- OpenAppDAInput,
- OpenAppDAIntermediateValue,
- OpenAppDAOutput,
+ type OpenAppDAError,
+ type OpenAppDAInput,
+ type OpenAppDAIntermediateValue,
+ type OpenAppDAOutput,
OpenAppDeviceAction,
} from "@ledgerhq/device-management-kit";
-import { useSdk } from "@/providers/DeviceSdkProvider";
+import { useDmk } from "@/providers/DeviceManagementKitProvider";
import { DeviceActionsList, UNLOCK_TIMEOUT } from "./DeviceActionsList";
-import { DeviceActionProps } from "./DeviceActionTester";
+import { type DeviceActionProps } from "./DeviceActionTester";
export const AllDeviceActions: React.FC<{ sessionId: string }> = ({
sessionId,
}) => {
- const sdk = useSdk();
+ const dmk = useDmk();
- const deviceModelId = sdk.getConnectedDevice({
+ const deviceModelId = dmk.getConnectedDevice({
sessionId,
}).modelId;
@@ -49,17 +49,17 @@ export const AllDeviceActions: React.FC<{ sessionId: string }> = ({
title: "Open app",
description:
"Perform all the actions necessary to open an app on the device",
- executeDeviceAction: ({ appName }, inspect) => {
+ executeDeviceAction: ({ appName, unlockTimeout }, inspect) => {
const deviceAction = new OpenAppDeviceAction({
- input: { appName },
+ input: { appName, unlockTimeout },
inspect,
});
- return sdk.executeDeviceAction({
+ return dmk.executeDeviceAction({
sessionId,
deviceAction,
});
},
- initialValues: { appName: "" },
+ initialValues: { appName: "", unlockTimeout: UNLOCK_TIMEOUT },
deviceModelId,
} satisfies DeviceActionProps<
OpenAppDAOutput,
@@ -76,7 +76,7 @@ export const AllDeviceActions: React.FC<{ sessionId: string }> = ({
input: { unlockTimeout },
inspect,
});
- return sdk.executeDeviceAction({
+ return dmk.executeDeviceAction({
sessionId,
deviceAction,
});
@@ -92,12 +92,12 @@ export const AllDeviceActions: React.FC<{ sessionId: string }> = ({
{
title: "Go to dashboard",
description: "Navigate to the dashboard",
- executeDeviceAction: (_, inspect) => {
+ executeDeviceAction: ({ unlockTimeout }, inspect) => {
const deviceAction = new GoToDashboardDeviceAction({
- input: { unlockTimeout: UNLOCK_TIMEOUT },
+ input: { unlockTimeout },
inspect,
});
- return sdk.executeDeviceAction({
+ return dmk.executeDeviceAction({
sessionId,
deviceAction,
});
@@ -113,12 +113,12 @@ export const AllDeviceActions: React.FC<{ sessionId: string }> = ({
{
title: "List apps",
description: "List all applications installed on the device",
- executeDeviceAction: (_, inspect) => {
+ executeDeviceAction: ({ unlockTimeout }, inspect) => {
const deviceAction = new ListAppsDeviceAction({
- input: { unlockTimeout: UNLOCK_TIMEOUT },
+ input: { unlockTimeout },
inspect,
});
- return sdk.executeDeviceAction({
+ return dmk.executeDeviceAction({
sessionId,
deviceAction,
});
@@ -135,12 +135,12 @@ export const AllDeviceActions: React.FC<{ sessionId: string }> = ({
title: "List apps with metadata",
description:
"List all applications installed on the device with additional metadata",
- executeDeviceAction: (_, inspect) => {
+ executeDeviceAction: ({ unlockTimeout }, inspect) => {
const deviceAction = new ListAppsWithMetadataDeviceAction({
- input: { unlockTimeout: UNLOCK_TIMEOUT },
+ input: { unlockTimeout },
inspect,
});
- return sdk.executeDeviceAction({
+ return dmk.executeDeviceAction({
sessionId,
deviceAction,
});
@@ -154,7 +154,7 @@ export const AllDeviceActions: React.FC<{ sessionId: string }> = ({
ListAppsWithMetadataDAIntermediateValue
>,
],
- [deviceModelId, sdk, sessionId],
+ [deviceModelId, dmk, sessionId],
);
return (
diff --git a/apps/sample/src/components/DeviceActionsView/DeviceActionResponse.tsx b/apps/sample/src/components/DeviceActionsView/DeviceActionResponse.tsx
index 39032d106..e143bd473 100644
--- a/apps/sample/src/components/DeviceActionsView/DeviceActionResponse.tsx
+++ b/apps/sample/src/components/DeviceActionsView/DeviceActionResponse.tsx
@@ -8,7 +8,7 @@ import {
import { Flex, Icons, Tag, Text, Tooltip } from "@ledgerhq/react-ui";
import styled from "styled-components";
-import { FieldType } from "@/hooks/useForm";
+import { type FieldType } from "@/hooks/useForm";
export type DeviceActionResponseProps = {
args: Record;
diff --git a/apps/sample/src/components/DeviceActionsView/DeviceActionTester.tsx b/apps/sample/src/components/DeviceActionsView/DeviceActionTester.tsx
index 2fd722438..ea78a83d7 100644
--- a/apps/sample/src/components/DeviceActionsView/DeviceActionTester.tsx
+++ b/apps/sample/src/components/DeviceActionsView/DeviceActionTester.tsx
@@ -2,10 +2,10 @@ import React from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import type {
DeviceActionIntermediateValue,
+ DmkError,
ExecuteDeviceActionReturnType,
- SdkError,
} from "@ledgerhq/device-management-kit";
-import { DeviceModelId } from "@ledgerhq/device-management-kit";
+import { type DeviceModelId } from "@ledgerhq/device-management-kit";
import {
Button,
Divider,
@@ -22,20 +22,20 @@ import { Block } from "@/components/Block";
import { ClickableListItem } from "@/components/ClickableListItem";
import {
CommandForm,
- ValueSelector,
+ type ValueSelector,
} from "@/components/CommandsView/CommandForm";
-import { FieldType } from "@/hooks/useForm";
+import { type FieldType } from "@/hooks/useForm";
import {
DeviceActionResponse,
- DeviceActionResponseProps,
+ type DeviceActionResponseProps,
} from "./DeviceActionResponse";
import { DeviceActionUI } from "./DeviceActionUI";
export type DeviceActionProps<
Output,
Input extends Record | void,
- Error extends SdkError,
+ Error extends DmkError,
IntermediateValue extends DeviceActionIntermediateValue,
> = {
title: string;
@@ -85,7 +85,7 @@ const BoxHeader: React.FC<{ children: string; hint: string }> = ({
export function DeviceActionTester<
Output,
Input extends Record,
- Error extends SdkError,
+ Error extends DmkError,
IntermediateValue extends DeviceActionIntermediateValue,
>(props: DeviceActionProps) {
const {
@@ -196,7 +196,7 @@ export function DeviceActionTester<
return (
-
+
Device Action input
loading ? :
}
+ data-testid="CTA_send-device-action"
>
Execute
@@ -264,6 +265,7 @@ export function DeviceActionTester<
overflowY="scroll"
height="100%"
flex={1}
+ data-testid="box_device-commands-responses"
>
{responses.map((response, index, arr) => {
const isLatest = index === arr.length - 1;
@@ -273,7 +275,10 @@ export function DeviceActionTester<
key={`${response.date.toISOString()}-index-${index}`}
>
- {isLatest ? null : }
+
+ {/** if I just unmount it, all dividers are glitching out */}
+
+
);
})}
@@ -295,7 +300,7 @@ export function DeviceActionTester<
export function DeviceActionRow<
Output,
Input extends Record,
- Error extends SdkError,
+ Error extends DmkError,
IntermediateValue extends DeviceActionIntermediateValue,
>(
props: DeviceActionProps & {
diff --git a/apps/sample/src/components/DeviceActionsView/DeviceActionUI.tsx b/apps/sample/src/components/DeviceActionsView/DeviceActionUI.tsx
index faeb74e65..e20141661 100644
--- a/apps/sample/src/components/DeviceActionsView/DeviceActionUI.tsx
+++ b/apps/sample/src/components/DeviceActionsView/DeviceActionUI.tsx
@@ -34,7 +34,7 @@ import * as ContinueOnLedgerStaxDark from "./lotties/stax/04_STAX_DARK_CONTINUE_
import * as SignTransactionStaxDark from "./lotties/stax/05_STAX_DARK_SIGN_TRANSACTION.json";
import * as FrontViewStaxDark from "./lotties/stax/06_STAX_DARK_FRONT_VIEW.json";
import {
- DeviceActionResponseProps,
+ type DeviceActionResponseProps,
deviceActionStatusToColor,
} from "./DeviceActionResponse";
diff --git a/apps/sample/src/components/DeviceActionsView/DeviceActionsList.tsx b/apps/sample/src/components/DeviceActionsView/DeviceActionsList.tsx
index fe8db18d9..aa17e982d 100644
--- a/apps/sample/src/components/DeviceActionsView/DeviceActionsList.tsx
+++ b/apps/sample/src/components/DeviceActionsView/DeviceActionsList.tsx
@@ -6,7 +6,7 @@ import styled from "styled-components";
import { PageWithHeader } from "@/components/PageWithHeader";
import {
- DeviceActionProps,
+ type DeviceActionProps,
DeviceActionRow,
DeviceActionTester,
} from "./DeviceActionTester";
diff --git a/apps/sample/src/components/Header/index.tsx b/apps/sample/src/components/Header/index.tsx
index 83fbd02eb..b0e746020 100644
--- a/apps/sample/src/components/Header/index.tsx
+++ b/apps/sample/src/components/Header/index.tsx
@@ -1,6 +1,19 @@
-import React from "react";
-import { Flex, Icons } from "@ledgerhq/react-ui";
-import styled, { DefaultTheme } from "styled-components";
+import React, { useCallback, useEffect, useState } from "react";
+import { BuiltinTransports } from "@ledgerhq/device-management-kit";
+import { FlipperPluginManager } from "@ledgerhq/device-management-kit-flipper-plugin-client";
+import {
+ Button,
+ Divider,
+ DropdownGeneric,
+ Flex,
+ Icons,
+ Input,
+ Switch,
+ Text,
+} from "@ledgerhq/react-ui";
+import styled, { type DefaultTheme } from "styled-components";
+
+import { useDmkConfigContext } from "@/providers/DmkConfig";
const Root = styled(Flex).attrs({ py: 3, px: 10, gridGap: 8 })`
color: ${({ theme }: { theme: DefaultTheme }) => theme.colors.neutral.c90};
@@ -13,21 +26,119 @@ const Actions = styled(Flex)`
align-items: center;
flex: 1 0 0;
`;
+
const IconBox = styled(Flex).attrs({ p: 3 })`
cursor: pointer;
align-items: center;
opacity: 0.7;
`;
-export const Header = () => (
-
-
-
-
-
-
-
-
-
-
-);
+const UrlInput = styled(Input)`
+ align-items: center;
+`;
+
+export const Header = () => {
+ const {
+ dispatch,
+ state: { transport, mockServerUrl },
+ } = useDmkConfigContext();
+ const onToggleMockServer = useCallback(() => {
+ dispatch({
+ type: "set_transport",
+ payload: {
+ transport:
+ transport === BuiltinTransports.MOCK_SERVER
+ ? BuiltinTransports.USB
+ : BuiltinTransports.MOCK_SERVER,
+ },
+ });
+ }, [dispatch, transport]);
+ const [mockServerStateUrl, setMockServerStateUrl] =
+ useState(mockServerUrl);
+ const mockServerEnabled = transport === BuiltinTransports.MOCK_SERVER;
+
+ const validateServerUrl = useCallback(
+ () =>
+ dispatch({
+ type: "set_mock_server_url",
+ payload: { mockServerUrl: mockServerStateUrl },
+ }),
+ [dispatch, mockServerStateUrl],
+ );
+
+ const onClickConnectFlipperClient = useCallback(() => {
+ /**
+ * This is useful in case the Flipper server is started after the app and
+ * we want to connect to it without reloading the app, to keep the app state
+ * and the logs.
+ * */
+ FlipperPluginManager.getInstance().attemptInitialization();
+ }, []);
+
+ const [flipperClientConnected, setFlipperClientConnected] =
+ useState(false);
+
+ useEffect(() => {
+ const subscription = FlipperPluginManager.getInstance()
+ .observeIsConnected()
+ .subscribe((connected: boolean) => {
+ setFlipperClientConnected(connected);
+ });
+ return () => {
+ subscription.unsubscribe();
+ };
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ Mock server:
+
+
+
+
+ {mockServerEnabled && (
+ setMockServerStateUrl(url)}
+ renderRight={() => (
+
+
+
+
+
+ )}
+ />
+ )}
+
+
+ Flipper ({flipperClientConnected ? "Connected" : "Disconnected"}):
+
+
+ Connect Flipper client
+
+
+
+
+
+ );
+};
diff --git a/apps/sample/src/components/MainView/ConnectDeviceActions.tsx b/apps/sample/src/components/MainView/ConnectDeviceActions.tsx
new file mode 100644
index 000000000..2334173ee
--- /dev/null
+++ b/apps/sample/src/components/MainView/ConnectDeviceActions.tsx
@@ -0,0 +1,98 @@
+import React, { useCallback } from "react";
+import {
+ BuiltinTransports,
+ type DmkError,
+} from "@ledgerhq/device-management-kit";
+import { Button, Flex } from "@ledgerhq/react-ui";
+import styled from "styled-components";
+
+import { useDmk } from "@/providers/DeviceManagementKitProvider";
+import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider";
+import { useDmkConfigContext } from "@/providers/DmkConfig";
+
+type ConnectDeviceActionsProps = {
+ onError: (error: DmkError | null) => void;
+};
+
+const ConnectButton = styled(Button).attrs({ mx: 3 })``;
+
+export const ConnectDeviceActions = ({
+ onError,
+}: ConnectDeviceActionsProps) => {
+ const {
+ state: { transport },
+ } = useDmkConfigContext();
+ const { dispatch: dispatchDeviceSession } = useDeviceSessionsContext();
+ const dmk = useDmk();
+
+ const onSelectDeviceClicked = useCallback(
+ (selectedTransport: BuiltinTransports) => {
+ onError(null);
+ dmk.startDiscovering({ transport: selectedTransport }).subscribe({
+ next: (device) => {
+ dmk
+ .connect({ device })
+ .then((sessionId) => {
+ console.log(
+ `đĻ Response from connect: ${JSON.stringify(sessionId)} đ`,
+ );
+ dispatchDeviceSession({
+ type: "add_session",
+ payload: {
+ sessionId,
+ connectedDevice: dmk.getConnectedDevice({ sessionId }),
+ },
+ });
+ })
+ .catch((error) => {
+ onError(error);
+ console.error(`Error from connection or get-version`, error);
+ });
+ },
+ error: (error) => {
+ console.error(error);
+ },
+ });
+ },
+ [dispatchDeviceSession, onError, dmk],
+ );
+
+ // This implementation gives the impression that working with the mock server
+ // is a special case, when in fact it's just a transport like the others
+ // TODO: instead of toggling between mock & regular config, we should
+ // just have a menu to select the active transports (where the active menu)
+ // and this here should be a list of one buttons for each active transport
+ // also we should not have a different appearance when the mock server is enabled
+ // we should just display the list of active transports somewhere in the sidebar, discreetly
+
+ return transport === BuiltinTransports.MOCK_SERVER ? (
+ onSelectDeviceClicked(BuiltinTransports.MOCK_SERVER)}
+ variant="main"
+ backgroundColor="main"
+ size="large"
+ data-testid="CTA_select-device"
+ >
+ Select a device
+
+ ) : (
+
+ onSelectDeviceClicked(BuiltinTransports.USB)}
+ variant="main"
+ backgroundColor="main"
+ size="large"
+ >
+ Select a USB device
+
+ onSelectDeviceClicked(BuiltinTransports.BLE)}
+ variant="main"
+ backgroundColor="main"
+ size="large"
+ >
+ Select a BLE device
+
+
+ );
+};
diff --git a/apps/sample/src/components/MainView/index.tsx b/apps/sample/src/components/MainView/index.tsx
index 5a62bfc05..45aa638fb 100644
--- a/apps/sample/src/components/MainView/index.tsx
+++ b/apps/sample/src/components/MainView/index.tsx
@@ -1,10 +1,10 @@
-import React, { useCallback, useEffect } from "react";
-import { Button, Flex, Text } from "@ledgerhq/react-ui";
+import React, { useEffect, useState } from "react";
+import { type DmkError } from "@ledgerhq/device-management-kit";
+import { Badge, Flex, Icon, Notification, Text } from "@ledgerhq/react-ui";
import Image from "next/image";
-import styled, { DefaultTheme } from "styled-components";
+import styled, { type DefaultTheme } from "styled-components";
-import { useSdk } from "@/providers/DeviceSdkProvider";
-import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider";
+import { ConnectDeviceActions } from "./ConnectDeviceActions";
const Root = styled(Flex)`
flex: 1;
@@ -12,6 +12,11 @@ const Root = styled(Flex)`
align-items: center;
flex-direction: column;
`;
+const ErrorNotification = styled(Notification)`
+ position: absolute;
+ bottom: 10px;
+ width: 70%;
+`;
const Description = styled(Text).attrs({ my: 6 })`
color: ${({ theme }: { theme: DefaultTheme }) => theme.colors.neutral.c70};
@@ -22,44 +27,21 @@ const NanoLogo = styled(Image).attrs({ mb: 8 })`
`;
export const MainView: React.FC = () => {
- const sdk = useSdk();
- const { dispatch } = useDeviceSessionsContext();
-
- // Example starting the discovery on a user action
- const onSelectDeviceClicked = useCallback(() => {
- sdk.startDiscovering().subscribe({
- next: (device) => {
- sdk
- .connect({ deviceId: device.id })
- .then((sessionId) => {
- console.log(
- `đĻ Response from connect: ${JSON.stringify(sessionId)} đ`,
- );
- dispatch({
- type: "add_session",
- payload: {
- sessionId,
- connectedDevice: sdk.getConnectedDevice({ sessionId }),
- },
- });
- })
- .catch((error) => {
- console.error(`Error from connection or get-version`, error);
- });
- },
- error: (error) => {
- console.error(error);
- },
- });
- }, [sdk, dispatch]);
+ const [connectionError, setConnectionError] = useState(null);
useEffect(() => {
+ let timeoutId: NodeJS.Timeout;
+ if (connectionError) {
+ timeoutId = setTimeout(() => {
+ setConnectionError(null);
+ }, 3000);
+ }
return () => {
- // Example cleaning up the discovery
- sdk.stopDiscovering();
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
};
- }, [sdk]);
-
+ }, [connectionError]);
return (
{
Use this application to test Ledger hardware device features.
-
- Select a device
-
+
+ {connectionError && (
+ }
+ />
+ }
+ hasBackground
+ title="Error"
+ description={
+ connectionError.message ||
+ (connectionError.originalError as Error | undefined)?.message
+ }
+ />
+ )}
);
};
diff --git a/apps/sample/src/components/Menu/index.tsx b/apps/sample/src/components/Menu/index.tsx
index def37f53e..e8f8979ce 100644
--- a/apps/sample/src/components/Menu/index.tsx
+++ b/apps/sample/src/components/Menu/index.tsx
@@ -1,8 +1,11 @@
import React from "react";
+import { BuiltinTransports } from "@ledgerhq/device-management-kit";
import { Flex, Icons, Link } from "@ledgerhq/react-ui";
import { useRouter } from "next/navigation";
import styled from "styled-components";
+import { useDmkConfigContext } from "@/providers/DmkConfig";
+
const MenuItem = styled(Flex).attrs({ p: 3, pl: 5 })`
align-items: center;
`;
@@ -15,6 +18,9 @@ const MenuTitle = styled(Link).attrs({
export const Menu: React.FC = () => {
const router = useRouter();
+ const {
+ state: { transport },
+ } = useDmkConfigContext();
return (
<>
@@ -24,17 +30,30 @@ export const Menu: React.FC = () => {
- router.push("/commands")}>Commands
+ router.push("/commands")}
+ >
+ Commands
+
- router.push("device-actions")}>
+ router.push("device-actions")}
+ >
Device actions
- router.push("/apdu")}>APDU
+ router.push("/apdu")}
+ >
+ APDU
+
@@ -42,12 +61,28 @@ export const Menu: React.FC = () => {
- router.push("/keyring")}>Keyrings
+ router.push("/signer")}
+ >
+ Signers
+
router.push("/cal")}>Crypto Assets
+ {transport === BuiltinTransports.MOCK_SERVER && (
+
+
+ router.push("/mock")}
+ >
+ Mock Settings
+
+
+ )}
>
);
};
diff --git a/apps/sample/src/components/MockView/MockDeviceDrawer.tsx b/apps/sample/src/components/MockView/MockDeviceDrawer.tsx
new file mode 100644
index 000000000..1176e5453
--- /dev/null
+++ b/apps/sample/src/components/MockView/MockDeviceDrawer.tsx
@@ -0,0 +1,210 @@
+import React, { useCallback, useEffect, useState } from "react";
+import {
+ type Mock,
+ type MockClient,
+ type Session,
+} from "@ledgerhq/device-transport-kit-mock-client";
+import { Button, Divider, Flex, Input, Text } from "@ledgerhq/react-ui";
+import styled from "styled-components";
+
+import { MockItem } from "@/components/MockView/MockItem";
+import { StyledDrawer } from "@/components/StyledDrawer";
+
+type MockDeviceDrawerProps = {
+ currentSession: Session | null;
+ isOpen: boolean;
+ onClose: () => void;
+ client: MockClient;
+ onDeviceDeleted: () => void;
+};
+
+const MockButton = styled(Button).attrs({
+ variant: "main",
+ color: "neutral.c00",
+ mx: 5,
+})``;
+
+const inputContainerProps = { style: { borderRadius: 4 } };
+
+export const MockDeviceDrawer: React.FC = ({
+ currentSession,
+ isOpen,
+ onClose,
+ client,
+ onDeviceDeleted,
+}) => {
+ const [mocks, setMocks] = useState([]);
+ const [currentPrefix, setCurrentPrefix] = useState("b001");
+ const [currentResponse, setCurrentResponse] = useState("6700");
+ const [editMockIndex, setEditMockIndex] = useState(-1);
+
+ const fetchMocks = useCallback(
+ async (session: Session) => {
+ try {
+ const response = await client.getMocks(session.id);
+ setMocks(response);
+ } catch (error) {
+ console.error(error);
+ }
+ },
+ [client],
+ );
+ const sendMock = useCallback(
+ async (prefix: string, response: string) => {
+ if (!currentSession) {
+ return;
+ }
+ try {
+ const resp = await client.addMock(currentSession.id, prefix, response);
+ setEditMockIndex(-1);
+ if (!resp) {
+ console.log("Failed to add the mock");
+ } else {
+ await fetchMocks(currentSession);
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ },
+ [currentSession, client, fetchMocks],
+ );
+ const handleAddMockClick = useCallback(async () => {
+ if (!currentSession) {
+ return;
+ }
+ await sendMock(currentPrefix, currentResponse);
+ }, [currentPrefix, currentResponse, currentSession, sendMock]);
+
+ const handleRemoveMocksClick = async () => {
+ if (!currentSession) {
+ return;
+ }
+ try {
+ const response = await client.deleteMocks(currentSession.id);
+ if (!response) {
+ console.log("Failed to delete the mocks");
+ } else {
+ fetchMocks(currentSession).catch(console.error);
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const handleRemoveDeviceClick = useCallback(
+ async (sessionId: string) => {
+ try {
+ const response = await client.disconnect(sessionId);
+ if (!response) {
+ console.log("Failed to disconnect device");
+ } else {
+ onDeviceDeleted();
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ },
+ [client, onDeviceDeleted],
+ );
+
+ useEffect(() => {
+ if (isOpen && currentSession) {
+ fetchMocks(currentSession);
+ }
+ }, [isOpen, currentSession, fetchMocks]);
+
+ return (
+
+
+
+
+
+ Prefix
+
+
+ Response
+
+
+
+
+
+ {mocks.map((mock, index) => (
+ setEditMockIndex(index)}
+ onSubmit={sendMock}
+ />
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ Add
+
+
+
+
+
+
+ Remove all mocks
+
+
+ currentSession ? handleRemoveDeviceClick(currentSession.id) : null
+ }
+ >
+ Remove device
+
+
+
+
+ );
+};
diff --git a/apps/sample/src/components/MockView/MockItem.tsx b/apps/sample/src/components/MockView/MockItem.tsx
new file mode 100644
index 000000000..78c3668a1
--- /dev/null
+++ b/apps/sample/src/components/MockView/MockItem.tsx
@@ -0,0 +1,83 @@
+import React, { useState } from "react";
+import { type Mock } from "@ledgerhq/device-transport-kit-mock-client";
+import { Button, Flex, Icons, Input, Text } from "@ledgerhq/react-ui";
+
+type MockItemProps = {
+ mock: Mock;
+ editable: boolean;
+ onEdit: () => void;
+ onSubmit: (prefix: string, response: string) => void;
+};
+
+export const MockItem: React.FC = ({
+ mock,
+ editable,
+ onEdit,
+ onSubmit,
+}) => {
+ const [currentResponse, setCurrentResponse] = useState(mock.response);
+ const [currentPrefix, setCurrentPrefix] = useState(mock.prefix);
+
+ return (
+
+ {editable ? (
+ <>
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+
+ {mock.prefix}
+
+
+ {mock.response}
+
+ >
+ )}
+
+ (editable ? : )}
+ onClick={
+ editable ? () => onSubmit(currentPrefix, currentResponse) : onEdit
+ }
+ />
+
+
+ );
+};
diff --git a/apps/sample/src/components/MockView/index.tsx b/apps/sample/src/components/MockView/index.tsx
new file mode 100644
index 000000000..085ac28c3
--- /dev/null
+++ b/apps/sample/src/components/MockView/index.tsx
@@ -0,0 +1,96 @@
+import React, { useCallback, useEffect, useState } from "react";
+import { type Session } from "@ledgerhq/device-transport-kit-mock-client";
+import { Button, Flex, Text } from "@ledgerhq/react-ui";
+import styled from "styled-components";
+
+import { ClickableListItem } from "@/components/ClickableListItem";
+import { MockDeviceDrawer } from "@/components/MockView/MockDeviceDrawer";
+import { PageWithHeader } from "@/components/PageWithHeader";
+import { useMockClient } from "@/hooks/useMockClient";
+import { useDmkConfigContext } from "@/providers/DmkConfig";
+
+const MockButton = styled(Button).attrs({
+ variant: "main",
+ color: "neutral.c00",
+ mx: 5,
+})``;
+
+export const MockView: React.FC = () => {
+ const [sessions, setSessions] = useState([]);
+
+ const [currentSession, setCurrentSession] = useState(null);
+ const [drawerVisible, setDrawerVisibility] = useState(false);
+
+ const {
+ state: { mockServerUrl },
+ } = useDmkConfigContext();
+
+ const client = useMockClient(mockServerUrl);
+
+ const fetchSessions = useCallback(async () => {
+ try {
+ const response = await client.getConnected();
+ setSessions(response);
+ setCurrentSession(null);
+ } catch (error) {
+ console.error(error);
+ }
+ }, [client]);
+
+ const handleSessionClick = useCallback(async (session: Session) => {
+ try {
+ setCurrentSession(session);
+ setDrawerVisibility(true);
+ } catch (error) {
+ console.error(error);
+ }
+ }, []);
+
+ const handleRemoveDevicesClick = async () => {
+ try {
+ const response = await client.disconnectAll();
+ if (!response) {
+ console.log("Failed to disconnect all devices");
+ } else {
+ fetchSessions().catch(console.error);
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ useEffect(() => {
+ fetchSessions().catch(console.error);
+ }, [fetchSessions]);
+
+ return (
+
+
+
+ {sessions.map((session) => (
+ handleSessionClick(session)}
+ my={2}
+ />
+ ))}
+
+
+
+ Remove all devices
+
+
+
+
+ setDrawerVisibility(false)}
+ onDeviceDeleted={fetchSessions}
+ currentSession={currentSession}
+ client={client}
+ />
+
+ );
+};
diff --git a/apps/sample/src/components/PageWithHeader.tsx b/apps/sample/src/components/PageWithHeader.tsx
index b451b6869..472a3d6e5 100644
--- a/apps/sample/src/components/PageWithHeader.tsx
+++ b/apps/sample/src/components/PageWithHeader.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { Breadcrumb, Divider, Flex } from "@ledgerhq/react-ui";
-import { Props as BreadCrumbProps } from "@ledgerhq/react-ui/components/navigation/Breadcrumb/index";
+import { type Props as BreadCrumbProps } from "@ledgerhq/react-ui/components/navigation/Breadcrumb/index";
import styled from "styled-components";
const Root = styled(Flex).attrs({ mx: 15, mb: 5 })`
diff --git a/apps/sample/src/components/SessionIdWrapper.tsx b/apps/sample/src/components/SessionIdWrapper.tsx
index 21e913713..7a0514711 100644
--- a/apps/sample/src/components/SessionIdWrapper.tsx
+++ b/apps/sample/src/components/SessionIdWrapper.tsx
@@ -40,5 +40,5 @@ export const SessionIdWrapper: React.FC<{
);
}
- return ;
+ return ;
};
diff --git a/apps/sample/src/components/Sidebar/index.tsx b/apps/sample/src/components/Sidebar/index.tsx
index 4d225a878..427cc7854 100644
--- a/apps/sample/src/components/Sidebar/index.tsx
+++ b/apps/sample/src/components/Sidebar/index.tsx
@@ -1,19 +1,33 @@
"use client";
import React, { useCallback, useEffect, useState } from "react";
+import { BuiltinTransports } from "@ledgerhq/device-management-kit";
import { Box, Flex, IconsLegacy, Link, Text } from "@ledgerhq/react-ui";
import { useRouter } from "next/navigation";
-import styled, { DefaultTheme } from "styled-components";
+import styled, { type DefaultTheme } from "styled-components";
+import { AvailableDevices } from "@/components/AvailableDevices";
import { Device } from "@/components/Device";
import { Menu } from "@/components/Menu";
-import { useExportLogsCallback, useSdk } from "@/providers/DeviceSdkProvider";
+import {
+ useDmk,
+ useExportLogsCallback,
+} from "@/providers/DeviceManagementKitProvider";
import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider";
+import { useDmkConfigContext } from "@/providers/DmkConfig";
const Root = styled(Flex).attrs({ py: 8, px: 6 })`
flex-direction: column;
width: 280px;
- background-color: ${({ theme }: { theme: DefaultTheme }) =>
- theme.colors.background.drawer};
+ background-color: ${({
+ theme,
+ mockServerEnabled,
+ }: {
+ theme: DefaultTheme;
+ mockServerEnabled: boolean;
+ }) =>
+ mockServerEnabled
+ ? theme.colors.constant.purple
+ : theme.colors.background.drawer};
`;
const Subtitle = styled(Text).attrs({ mb: 5 })``;
@@ -35,37 +49,40 @@ const VersionText = styled(Text)`
export const Sidebar: React.FC = () => {
const [version, setVersion] = useState("");
- const sdk = useSdk();
+ const dmk = useDmk();
const exportLogs = useExportLogsCallback();
const {
state: { deviceById, selectedId },
dispatch,
} = useDeviceSessionsContext();
+ const {
+ state: { transport },
+ } = useDmkConfigContext();
useEffect(() => {
- sdk
+ dmk
.getVersion()
.then((v) => setVersion(v))
.catch((error: unknown) => {
console.error(new Error(String(error)));
setVersion("");
});
- }, [sdk]);
+ }, [dmk]);
const onDeviceDisconnect = useCallback(
async (sessionId: string) => {
try {
- await sdk.disconnect({ sessionId });
+ await dmk.disconnect({ sessionId });
dispatch({ type: "remove_session", payload: { sessionId } });
} catch (e) {
console.error(e);
}
},
- [dispatch, sdk],
+ [dispatch, dmk],
);
const router = useRouter();
return (
-
+
router.push("/")}
mb={8}
@@ -75,24 +92,28 @@ export const Sidebar: React.FC = () => {
}}
>
Ledger Device Management Kit
+ {transport === BuiltinTransports.MOCK_SERVER && (MOCKED) }
-
- SDK Version: {version ? version : "Loading..."}
-
-
- Device
-
- {Object.entries(deviceById).map(([sessionId, device]) => (
- onDeviceDisconnect(sessionId)}
- />
- ))}
+
+ Device sessions ({Object.values(deviceById).length})
+
+
+ {Object.entries(deviceById).map(([sessionId, device]) => (
+
+ dispatch({ type: "select_session", payload: { sessionId } })
+ }
+ onDisconnect={() => onDeviceDisconnect(sessionId)}
+ />
+ ))}
+
+
Menu
@@ -107,10 +128,9 @@ export const Sidebar: React.FC = () => {
>
Share logs
-
- Ledger Device Management Kit version {version}
+
+ Ledger Device Management Kit{"\n"}version {version}
- App version 0.1
);
diff --git a/apps/sample/src/components/KeyringEthView/index.tsx b/apps/sample/src/components/SignerEthView/index.tsx
similarity index 61%
rename from apps/sample/src/components/KeyringEthView/index.tsx
rename to apps/sample/src/components/SignerEthView/index.tsx
index 67eb4867d..0aa711ea3 100644
--- a/apps/sample/src/components/KeyringEthView/index.tsx
+++ b/apps/sample/src/components/SignerEthView/index.tsx
@@ -1,73 +1,33 @@
-import React, { useCallback, useMemo } from "react";
+import React, { useMemo } from "react";
import {
- ContextModuleBuilder,
- ContextModuleCalConfig,
- ContextModuleConfig,
-} from "@ledgerhq/context-module";
-import {
- GetAddressDAError,
- GetAddressDAIntermediateValue,
- GetAddressDAOutput,
- KeyringEthBuilder,
- SignPersonalMessageDAError,
- SignPersonalMessageDAIntermediateValue,
- SignPersonalMessageDAOutput,
- SignTransactionDAError,
- SignTransactionDAIntermediateValue,
- SignTransactionDAOutput,
- SignTypedDataDAError,
- SignTypedDataDAIntermediateValue,
- SignTypedDataDAOutput,
- TypedData,
+ type GetAddressDAError,
+ type GetAddressDAIntermediateValue,
+ type GetAddressDAOutput,
+ type SignPersonalMessageDAError,
+ type SignPersonalMessageDAIntermediateValue,
+ type SignPersonalMessageDAOutput,
+ type SignTransactionDAError,
+ type SignTransactionDAIntermediateValue,
+ type SignTransactionDAOutput,
+ type SignTypedDataDAError,
+ type SignTypedDataDAIntermediateValue,
+ type SignTypedDataDAOutput,
+ type TypedData,
} from "@ledgerhq/device-signer-kit-ethereum";
import { ethers } from "ethers";
import { DeviceActionsList } from "@/components/DeviceActionsView/DeviceActionsList";
-import { DeviceActionProps } from "@/components/DeviceActionsView/DeviceActionTester";
-import { useSdk } from "@/providers/DeviceSdkProvider";
-
-const DEFAULT_CAL_URL = "https://crypto-assets-service.api.ledger.com/v1";
-const DEFAULT_CAL_BRANCH_REF = "main";
-
-const isBranchRef = (
- branchRef: string,
-): branchRef is ContextModuleCalConfig["branch"] => {
- return ["next", "main", "demo"].includes(branchRef);
-};
+import { type DeviceActionProps } from "@/components/DeviceActionsView/DeviceActionTester";
+import { useDmk } from "@/providers/DeviceManagementKitProvider";
+import { useSignerEth } from "@/providers/SignerEthProvider";
-export const KeyringEthView: React.FC<{ sessionId: string }> = ({
+export const SignerEthView: React.FC<{ sessionId: string }> = ({
sessionId,
}) => {
- const sdk = useSdk();
-
- const getKeyringEth = useCallback(
- (options?: {
- test: boolean;
- calUrl: string;
- branchRef: ContextModuleCalConfig["branch"];
- }) => {
- if (!options) {
- return new KeyringEthBuilder({ sdk, sessionId }).build();
- }
+ const dmk = useDmk();
+ const signer = useSignerEth();
- const builder = new ContextModuleBuilder();
- const calUrl = options.calUrl.length ? options.calUrl : "";
- const mode = options.test ? "test" : "prod";
- const config: ContextModuleConfig = {
- cal: { branch: options.branchRef, url: calUrl, mode },
- };
- console.log(
- `Using context module configuration: ${JSON.stringify(config)}`,
- );
- const contextModule = builder.withConfig(config).build();
- return new KeyringEthBuilder({ sdk, sessionId })
- .withContextModule(contextModule)
- .build();
- },
- [sdk, sessionId],
- );
-
- const deviceModelId = sdk.getConnectedDevice({
+ const deviceModelId = dmk.getConnectedDevice({
sessionId,
}).modelId;
@@ -83,7 +43,10 @@ export const KeyringEthView: React.FC<{ sessionId: string }> = ({
checkOnDevice,
returnChainCode,
}) => {
- return getKeyringEth().getAddress(derivationPath, {
+ if (!signer) {
+ throw new Error("Signer not initialized");
+ }
+ return signer.getAddress(derivationPath, {
checkOnDevice,
returnChainCode,
});
@@ -109,7 +72,10 @@ export const KeyringEthView: React.FC<{ sessionId: string }> = ({
description:
"Perform all the actions necessary to sign a message with the device",
executeDeviceAction: ({ derivationPath, message }) => {
- return getKeyringEth().signMessage(derivationPath, message);
+ if (!signer) {
+ throw new Error("Signer not initialized");
+ }
+ return signer.signMessage(derivationPath, message);
},
initialValues: {
derivationPath: "44'/60'/0'/0/0",
@@ -134,7 +100,10 @@ export const KeyringEthView: React.FC<{ sessionId: string }> = ({
transaction,
recipientDomain,
}) => {
- return getKeyringEth().signTransaction(
+ if (!signer) {
+ throw new Error("Signer not initialized");
+ }
+ return signer.signTransaction(
derivationPath,
ethers.Transaction.from(transaction),
{ domain: recipientDomain },
@@ -160,33 +129,19 @@ export const KeyringEthView: React.FC<{ sessionId: string }> = ({
title: "Sign typed message",
description:
"Perform all the actions necessary to sign a typed message on the device",
- executeDeviceAction: ({
- derivationPath,
- message,
- test,
- calUrl,
- branchRef,
- }) => {
+ executeDeviceAction: ({ derivationPath, message }) => {
const typedData = JSON.parse(message) as TypedData;
- return getKeyringEth({ test, calUrl, branchRef }).signTypedData(
- derivationPath,
- typedData,
- );
+
+ if (!signer) {
+ throw new Error("Signer not initialized");
+ }
+ return signer.signTypedData(derivationPath, typedData);
},
initialValues: {
derivationPath: "44'/60'/0'/0/0",
message: `{"domain":{"name":"USD Coin","verifyingContract":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","chainId":1,"version":"2"},"primaryType":"Permit","message":{"deadline":1718992051,"nonce":0,"spender":"0x111111125421ca6dc452d289314280a0f8842a65","owner":"0x6cbcd73cd8e8a42844662f0a0e76d7f79afd933d","value":"115792089237316195423570985008687907853269984665640564039457584007913129639935"},"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]}}`,
- test: false,
- calUrl: DEFAULT_CAL_URL,
- branchRef: DEFAULT_CAL_BRANCH_REF,
},
- validateValues: ({ message, calUrl, branchRef }) => {
- if (calUrl.length > 0 && !calUrl.startsWith("http")) {
- return false;
- }
- if (branchRef.length > 0 && !isBranchRef(branchRef)) {
- return false;
- }
+ validateValues: ({ message }) => {
try {
const parsedData = JSON.parse(message);
if (
@@ -213,18 +168,15 @@ export const KeyringEthView: React.FC<{ sessionId: string }> = ({
{
derivationPath: string;
message: string;
- test: boolean;
- calUrl: string;
- branchRef: ContextModuleCalConfig["branch"];
},
SignTypedDataDAError,
SignTypedDataDAIntermediateValue
>,
],
- [deviceModelId, getKeyringEth],
+ [deviceModelId, signer],
);
return (
-
+
);
};
diff --git a/apps/sample/src/components/SignerSolanaView/index.tsx b/apps/sample/src/components/SignerSolanaView/index.tsx
new file mode 100644
index 000000000..60d628fe3
--- /dev/null
+++ b/apps/sample/src/components/SignerSolanaView/index.tsx
@@ -0,0 +1,139 @@
+import React, { useMemo } from "react";
+import {
+ base64StringToBuffer,
+ isBase64String,
+} from "@ledgerhq/device-management-kit";
+import {
+ type GetAddressDAError,
+ type GetAddressDAIntermediateValue,
+ type GetAddressDAOutput,
+ type GetAppConfigurationDAError,
+ type GetAppConfigurationDAIntermediateValue,
+ type GetAppConfigurationDAOutput,
+ SignerSolanaBuilder,
+ type SignMessageDAError,
+ type SignMessageDAIntermediateValue,
+ type SignMessageDAOutput,
+ type SignTransactionDAError,
+ type SignTransactionDAIntermediateValue,
+ type SignTransactionDAOutput,
+} from "@ledgerhq/device-signer-kit-solana";
+
+import { DeviceActionsList } from "@/components/DeviceActionsView/DeviceActionsList";
+import { type DeviceActionProps } from "@/components/DeviceActionsView/DeviceActionTester";
+import { useDmk } from "@/providers/DeviceManagementKitProvider";
+
+const DEFAULT_DERIVATION_PATH = "44'/501'/0'/0'";
+
+export const SignerSolanaView: React.FC<{ sessionId: string }> = ({
+ sessionId,
+}) => {
+ const dmk = useDmk();
+ const signer = new SignerSolanaBuilder({ dmk, sessionId }).build();
+
+ const deviceModelId = dmk.getConnectedDevice({
+ sessionId,
+ }).modelId;
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const deviceActions: DeviceActionProps[] = useMemo(
+ () => [
+ {
+ title: "Get address",
+ description:
+ "Perform all the actions necessary to get a Solana address from the device",
+ executeDeviceAction: ({ derivationPath, checkOnDevice }) => {
+ return signer.getAddress(derivationPath, {
+ checkOnDevice,
+ });
+ },
+ initialValues: {
+ derivationPath: DEFAULT_DERIVATION_PATH,
+ checkOnDevice: false,
+ },
+ deviceModelId,
+ } satisfies DeviceActionProps<
+ GetAddressDAOutput,
+ {
+ derivationPath: string;
+ checkOnDevice?: boolean;
+ },
+ GetAddressDAError,
+ GetAddressDAIntermediateValue
+ >,
+ {
+ title: "Sign Transaction",
+ description:
+ "Perform all the actions necessary to sign a Solana transaction with the device",
+ executeDeviceAction: ({ derivationPath, transaction }) => {
+ const serializedTransaction =
+ base64StringToBuffer(transaction) ?? new Uint8Array();
+ return signer.signTransaction(
+ derivationPath,
+ serializedTransaction,
+ {},
+ );
+ },
+ initialValues: {
+ derivationPath: DEFAULT_DERIVATION_PATH,
+ transaction: "",
+ },
+ deviceModelId,
+ validateValues: ({ transaction }) =>
+ isBase64String(transaction) && transaction.length > 0,
+ } satisfies DeviceActionProps<
+ SignTransactionDAOutput,
+ {
+ derivationPath: string;
+ transaction: string;
+ },
+ SignTransactionDAError,
+ SignTransactionDAIntermediateValue
+ >,
+ {
+ title: "Sign off chain message",
+ description:
+ "Perform all the actions necessary to sign a solana off-chain message from the device",
+ executeDeviceAction: ({ derivationPath, message }) => {
+ if (!signer) {
+ throw new Error("Signer not initialized");
+ }
+ return signer.signMessage(derivationPath, message);
+ },
+ initialValues: {
+ derivationPath: DEFAULT_DERIVATION_PATH,
+ message: "Hello World",
+ },
+ deviceModelId,
+ } satisfies DeviceActionProps<
+ SignMessageDAOutput,
+ {
+ derivationPath: string;
+ message: string;
+ },
+ SignMessageDAError,
+ SignMessageDAIntermediateValue
+ >,
+ {
+ title: "Get app configuration",
+ description:
+ "Perform all the actions necessary to get the Solana app configuration from the device",
+ executeDeviceAction: () => {
+ return signer.getAppConfiguration();
+ },
+ initialValues: {},
+ deviceModelId,
+ } satisfies DeviceActionProps<
+ GetAppConfigurationDAOutput,
+ Record,
+ GetAppConfigurationDAError,
+ GetAppConfigurationDAIntermediateValue
+ >,
+ ],
+ [deviceModelId, signer],
+ );
+
+ return (
+
+ );
+};
diff --git a/apps/sample/src/components/KeyringView/index.tsx b/apps/sample/src/components/SignerView/index.tsx
similarity index 56%
rename from apps/sample/src/components/KeyringView/index.tsx
rename to apps/sample/src/components/SignerView/index.tsx
index 7db7536b7..3e5ef4b6c 100644
--- a/apps/sample/src/components/KeyringView/index.tsx
+++ b/apps/sample/src/components/SignerView/index.tsx
@@ -1,39 +1,44 @@
import React from "react";
-import { CryptoIcons, Grid } from "@ledgerhq/react-ui/index";
+import { CryptoIcons, Grid } from "@ledgerhq/react-ui";
import { useRouter } from "next/navigation";
import { PageWithHeader } from "@/components//PageWithHeader";
import { ClickableListItem } from "@/components/ClickableListItem";
-const SUPPORTED_KEYRINGS = [
+const SUPPORTED_SIGNERS = [
{
title: "Ethereum",
- description: "Access EVM compatible keyring functionality",
+ description: "Access EVM compatible signer functionality",
icon: ,
},
{
title: "Bitcoin",
- description: "Access Bitcoin keyring functionality",
+ description: "Access Bitcoin signer functionality",
icon: ,
},
+ {
+ title: "Solana",
+ description: "Access Solana signer functionality",
+ icon: ,
+ },
];
-export const KeyringView = () => {
+export const SignerView = () => {
const router = useRouter();
return (
-
+
- {SUPPORTED_KEYRINGS.map(({ title, description, icon }) => (
+ {SUPPORTED_SIGNERS.map(({ title, description, icon }) => (
{
- router.push(`/keyring/${title.toLowerCase()}`);
+ router.push(`/signer/${title.toLowerCase()}`);
}}
icon={icon}
/>
diff --git a/apps/sample/src/components/StyledDrawer.tsx b/apps/sample/src/components/StyledDrawer.tsx
index 6acab3bc0..4c91f63b1 100644
--- a/apps/sample/src/components/StyledDrawer.tsx
+++ b/apps/sample/src/components/StyledDrawer.tsx
@@ -12,16 +12,16 @@ const DescriptionText = styled(Text).attrs({
export const StyledDrawer: React.FC<{
title: string;
- description: string;
big: boolean;
isOpen: boolean;
onClose(): void;
children: React.ReactNode;
+ description?: string;
}> = ({ title, description, big, isOpen, onClose, children }) => {
return (
- {description}
+ {description && {description} }
{children}
diff --git a/apps/sample/src/hooks/useAvailableDevices.tsx b/apps/sample/src/hooks/useAvailableDevices.tsx
new file mode 100644
index 000000000..2a78ef7b7
--- /dev/null
+++ b/apps/sample/src/hooks/useAvailableDevices.tsx
@@ -0,0 +1,45 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+import { type DiscoveredDevice } from "@ledgerhq/device-management-kit";
+import { type Subscription } from "rxjs";
+
+import { useDmk } from "@/providers/DeviceManagementKitProvider";
+import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider";
+
+type AvailableDevice = DiscoveredDevice & { connected: boolean };
+
+export function useAvailableDevices(): AvailableDevice[] {
+ const dmk = useDmk();
+ const [discoveredDevices, setDiscoveredDevices] = useState<
+ DiscoveredDevice[]
+ >([]);
+ const { state: deviceSessionsState } = useDeviceSessionsContext();
+
+ const subscription = useRef(null);
+ useEffect(() => {
+ if (!subscription.current) {
+ subscription.current = dmk.listenToKnownDevices().subscribe((devices) => {
+ setDiscoveredDevices(devices);
+ });
+ }
+ return () => {
+ if (subscription.current) {
+ setDiscoveredDevices([]);
+ subscription.current.unsubscribe();
+ subscription.current = null;
+ }
+ };
+ }, [dmk]);
+
+ const result = useMemo(
+ () =>
+ discoveredDevices.map((device) => ({
+ ...device,
+ connected: Object.values(deviceSessionsState.deviceById).some(
+ (connectedDevice) => connectedDevice.id === device.id,
+ ),
+ })),
+ [discoveredDevices, deviceSessionsState],
+ );
+
+ return result;
+}
diff --git a/apps/sample/src/hooks/useDeviceSessionState.ts b/apps/sample/src/hooks/useDeviceSessionState.ts
index ae6c4ace5..03236e543 100644
--- a/apps/sample/src/hooks/useDeviceSessionState.ts
+++ b/apps/sample/src/hooks/useDeviceSessionState.ts
@@ -1,22 +1,22 @@
import { useEffect, useState } from "react";
import {
- DeviceSessionId,
- DeviceSessionState,
+ type DeviceSessionId,
+ type DeviceSessionState,
DeviceStatus,
} from "@ledgerhq/device-management-kit";
-import { useSdk } from "@/providers/DeviceSdkProvider";
+import { useDmk } from "@/providers/DeviceManagementKitProvider";
import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider";
export function useDeviceSessionState(sessionId: DeviceSessionId) {
- const sdk = useSdk();
+ const dmk = useDmk();
const [deviceSessionState, setDeviceSessionState] =
useState();
const { dispatch } = useDeviceSessionsContext();
useEffect(() => {
if (sessionId) {
- const subscription = sdk
+ const subscription = dmk
.getDeviceSessionState({
sessionId,
})
@@ -32,7 +32,7 @@ export function useDeviceSessionState(sessionId: DeviceSessionId) {
subscription.unsubscribe();
};
}
- }, [sessionId, sdk, dispatch]);
+ }, [sessionId, dmk, dispatch]);
return deviceSessionState;
}
diff --git a/apps/sample/src/hooks/useHasChanged.tsx b/apps/sample/src/hooks/useHasChanged.tsx
new file mode 100644
index 000000000..e3f90aaf4
--- /dev/null
+++ b/apps/sample/src/hooks/useHasChanged.tsx
@@ -0,0 +1,13 @@
+import { useRef } from "react";
+
+/**
+ * A custom hook that returns whether the value has changed since the last render.
+ * @param value The value to compare against the previous render.
+ * @returns A boolean indicating whether the value has changed since the last render.
+ */
+export function useHasChanged(value: T): boolean {
+ const ref = useRef(value);
+ const hasChanged = ref.current !== value;
+ ref.current = value;
+ return hasChanged;
+}
diff --git a/apps/sample/src/hooks/useMockClient.ts b/apps/sample/src/hooks/useMockClient.ts
new file mode 100644
index 000000000..58560561a
--- /dev/null
+++ b/apps/sample/src/hooks/useMockClient.ts
@@ -0,0 +1,12 @@
+import { useEffect, useState } from "react";
+import { MockClient } from "@ledgerhq/device-transport-kit-mock-client";
+
+export const useMockClient = (url: string): MockClient => {
+ const [client, setClient] = useState(new MockClient(url));
+
+ useEffect(() => {
+ setClient(new MockClient(url));
+ }, [url]);
+
+ return client;
+};
diff --git a/apps/sample/src/hooks/usePrevious.ts b/apps/sample/src/hooks/usePrevious.ts
new file mode 100644
index 000000000..6b37c7f6a
--- /dev/null
+++ b/apps/sample/src/hooks/usePrevious.ts
@@ -0,0 +1,13 @@
+import { useRef } from "react";
+
+/**
+ * A custom hook that returns the previous value of the provided value.
+ * @param value The value to compare against the previous render.
+ * @returns The previous value of the provided value.
+ */
+export function usePrevious(value: T) {
+ const ref = useRef();
+ const previousValue = ref.current;
+ ref.current = value;
+ return previousValue;
+}
diff --git a/apps/sample/src/providers/DeviceManagementKitProvider/index.tsx b/apps/sample/src/providers/DeviceManagementKitProvider/index.tsx
new file mode 100644
index 000000000..f5ce22a18
--- /dev/null
+++ b/apps/sample/src/providers/DeviceManagementKitProvider/index.tsx
@@ -0,0 +1,98 @@
+import React, { useCallback, useEffect, useState } from "react";
+import { createContext, type PropsWithChildren, useContext } from "react";
+import {
+ BuiltinTransports,
+ ConsoleLogger,
+ type DeviceManagementKit,
+ DeviceManagementKitBuilder,
+ WebLogsExporterLogger,
+} from "@ledgerhq/device-management-kit";
+import { FlipperDmkLogger } from "@ledgerhq/device-management-kit-flipper-plugin-client";
+
+import { useHasChanged } from "@/hooks/useHasChanged";
+import { useDmkConfigContext } from "@/providers/DmkConfig";
+
+const DmkContext = createContext(null);
+const LogsExporterContext = createContext(null);
+
+function buildDefaultDmk(logsExporter: WebLogsExporterLogger) {
+ return new DeviceManagementKitBuilder()
+ .addTransport(BuiltinTransports.USB)
+ .addTransport(BuiltinTransports.BLE)
+ .addLogger(new ConsoleLogger())
+ .addLogger(logsExporter)
+ .addLogger(new FlipperDmkLogger())
+ .build();
+}
+
+function buildMockDmk(url: string, logsExporter: WebLogsExporterLogger) {
+ return new DeviceManagementKitBuilder()
+ .addTransport(BuiltinTransports.MOCK_SERVER)
+ .addLogger(new ConsoleLogger())
+ .addLogger(logsExporter)
+ .addLogger(new FlipperDmkLogger())
+ .addConfig({ mockUrl: url })
+ .build();
+}
+
+export const DmkProvider: React.FC = ({ children }) => {
+ const {
+ state: { transport, mockServerUrl },
+ } = useDmkConfigContext();
+
+ const mockServerEnabled = transport === BuiltinTransports.MOCK_SERVER;
+ const [state, setState] = useState(() => {
+ const logsExporter = new WebLogsExporterLogger();
+ const dmk = mockServerEnabled
+ ? buildMockDmk(mockServerUrl, logsExporter)
+ : buildDefaultDmk(logsExporter);
+ return { dmk, logsExporter };
+ });
+
+ const mockServerEnabledChanged = useHasChanged(mockServerEnabled);
+ const mockServerUrlChanged = useHasChanged(mockServerUrl);
+
+ if (mockServerEnabledChanged || mockServerUrlChanged) {
+ setState(({ logsExporter }) => {
+ return {
+ dmk: mockServerEnabled
+ ? buildMockDmk(mockServerUrl, logsExporter)
+ : buildDefaultDmk(logsExporter),
+ logsExporter,
+ };
+ });
+ }
+
+ useEffect(() => {
+ return () => {
+ state.dmk.close();
+ };
+ }, [state.dmk]);
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export const useDmk = (): DeviceManagementKit => {
+ const dmk = useContext(DmkContext);
+ if (dmk === null)
+ throw new Error("useDmk must be used within a DmkContext.Provider");
+ return dmk;
+};
+
+export function useExportLogsCallback() {
+ const logsExporter = useContext(LogsExporterContext);
+ if (logsExporter === null) {
+ throw new Error(
+ "useExportLogsCallback must be used within LogsExporterContext.Provider",
+ );
+ }
+ return useCallback(() => {
+ logsExporter.exportLogsToJSON();
+ }, [logsExporter]);
+}
diff --git a/apps/sample/src/providers/DeviceSdkProvider/index.tsx b/apps/sample/src/providers/DeviceSdkProvider/index.tsx
deleted file mode 100644
index a0d5ee961..000000000
--- a/apps/sample/src/providers/DeviceSdkProvider/index.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React, { useCallback } from "react";
-import { createContext, PropsWithChildren, useContext } from "react";
-import {
- ConsoleLogger,
- DeviceSdk,
- DeviceSdkBuilder,
- WebLogsExporterLogger,
-} from "@ledgerhq/device-management-kit";
-
-const webLogsExporterLogger = new WebLogsExporterLogger();
-
-export const sdk = new DeviceSdkBuilder()
- .addLogger(new ConsoleLogger())
- .addLogger(webLogsExporterLogger)
- .build();
-
-const SdkContext = createContext(sdk);
-
-export const SdkProvider: React.FC = ({ children }) => {
- return {children} ;
-};
-
-export const useSdk = (): DeviceSdk => {
- return useContext(SdkContext);
-};
-
-export function useExportLogsCallback() {
- return useCallback(() => {
- webLogsExporterLogger.exportLogsToJSON();
- }, []);
-}
diff --git a/apps/sample/src/providers/DeviceSessionsProvider/index.tsx b/apps/sample/src/providers/DeviceSessionsProvider/index.tsx
index 82354d2a2..ed0a8fb5d 100644
--- a/apps/sample/src/providers/DeviceSessionsProvider/index.tsx
+++ b/apps/sample/src/providers/DeviceSessionsProvider/index.tsx
@@ -1,16 +1,23 @@
-import React, { Context, createContext, useContext, useReducer } from "react";
+import React, {
+ type Context,
+ createContext,
+ useContext,
+ useEffect,
+ useReducer,
+} from "react";
+import { useHasChanged } from "@/hooks/useHasChanged";
+import { useDmk } from "@/providers/DeviceManagementKitProvider";
import {
- AddSessionAction,
+ type DeviceSessionsAction,
DeviceSessionsInitialState,
deviceSessionsReducer,
- DeviceSessionsState,
- RemoveSessionAction,
+ type DeviceSessionsState,
} from "@/reducers/deviceSessions";
type DeviceSessionsContextType = {
state: DeviceSessionsState;
- dispatch: (value: AddSessionAction | RemoveSessionAction) => void;
+ dispatch: (value: DeviceSessionsAction) => void;
};
const DeviceSessionsContext: Context =
@@ -22,11 +29,34 @@ const DeviceSessionsContext: Context =
export const DeviceSessionsProvider: React.FC = ({
children,
}) => {
+ const dmk = useDmk();
const [state, dispatch] = useReducer(
deviceSessionsReducer,
DeviceSessionsInitialState,
);
+ const dmkHasChanged = useHasChanged(dmk);
+ if (dmkHasChanged) {
+ dispatch({ type: "remove_all_sessions" });
+ }
+
+ useEffect(() => {
+ const subscription = dmk
+ .listenToConnectedDevice()
+ .subscribe((connectedDevice) => {
+ dispatch({
+ type: "add_session",
+ payload: {
+ sessionId: connectedDevice.sessionId,
+ connectedDevice,
+ },
+ });
+ });
+ return () => {
+ subscription.unsubscribe();
+ };
+ }, [dmk]);
+
return (
{children}
diff --git a/apps/sample/src/providers/DmkConfig/index.tsx b/apps/sample/src/providers/DmkConfig/index.tsx
new file mode 100644
index 000000000..60b668e66
--- /dev/null
+++ b/apps/sample/src/providers/DmkConfig/index.tsx
@@ -0,0 +1,34 @@
+import React from "react";
+import { createContext, useContext, useReducer } from "react";
+
+import {
+ type DmkConfigAction,
+ DmkConfigInitialState,
+ dmkConfigReducer,
+ type DmkConfigState,
+} from "@/reducers/dmkConfig";
+
+type DmkConfigContextType = {
+ state: DmkConfigState;
+ dispatch: (value: DmkConfigAction) => void;
+};
+
+const DmkConfigContext = createContext({
+ state: DmkConfigInitialState,
+ dispatch: () => null,
+});
+
+export const DmkConfigProvider: React.FC = ({
+ children,
+}) => {
+ const [state, dispatch] = useReducer(dmkConfigReducer, DmkConfigInitialState);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useDmkConfigContext = () =>
+ useContext(DmkConfigContext);
diff --git a/apps/sample/src/providers/SignerEthProvider/index.tsx b/apps/sample/src/providers/SignerEthProvider/index.tsx
new file mode 100644
index 000000000..d5da929bf
--- /dev/null
+++ b/apps/sample/src/providers/SignerEthProvider/index.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import React, {
+ createContext,
+ type PropsWithChildren,
+ useContext,
+ useEffect,
+ useState,
+} from "react";
+import {
+ ContextModuleBuilder,
+ type ContextModuleCalConfig,
+} from "@ledgerhq/context-module";
+import {
+ type SignerEth,
+ SignerEthBuilder,
+} from "@ledgerhq/device-signer-kit-ethereum";
+
+import { useDmk } from "@/providers/DeviceManagementKitProvider";
+import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider";
+
+type SignerEthContextType = {
+ signer: SignerEth | null;
+ calConfig: ContextModuleCalConfig;
+ setCalConfig: (cal: ContextModuleCalConfig) => void;
+};
+
+const initialState: SignerEthContextType = {
+ signer: null,
+ calConfig: {
+ url: "https://crypto-assets-service.api.ledger.com/v1",
+ mode: "prod",
+ branch: "main",
+ },
+ setCalConfig: () => {},
+};
+
+const SignerEthContext = createContext(initialState);
+
+export const SignerEthProvider: React.FC = ({
+ children,
+}) => {
+ const dmk = useDmk();
+ const {
+ state: { selectedId: sessionId },
+ } = useDeviceSessionsContext();
+
+ const [signer, setSigner] = useState(null);
+ const [calConfig, setCalConfig] = useState(
+ initialState.calConfig,
+ );
+
+ useEffect(() => {
+ if (!sessionId || !dmk) {
+ setSigner(null);
+ return;
+ }
+
+ const contextModule = new ContextModuleBuilder()
+ .withConfig({ cal: calConfig })
+ .build();
+ const newSigner = new SignerEthBuilder({ dmk, sessionId })
+ .withContextModule(contextModule)
+ .build();
+ setSigner(newSigner);
+ }, [calConfig, dmk, sessionId]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useSignerEth = (): SignerEth | null => {
+ return useContext(SignerEthContext).signer;
+};
+
+export const useCalConfig = () => {
+ const { calConfig, setCalConfig } = useContext(SignerEthContext);
+ return { calConfig, setCalConfig };
+};
diff --git a/apps/sample/src/reducers/deviceSessions.ts b/apps/sample/src/reducers/deviceSessions.ts
index d6e2d580b..25b55777b 100644
--- a/apps/sample/src/reducers/deviceSessions.ts
+++ b/apps/sample/src/reducers/deviceSessions.ts
@@ -1,7 +1,7 @@
-import { Reducer } from "react";
+import { type Reducer } from "react";
import {
- ConnectedDevice,
- DeviceSessionId,
+ type ConnectedDevice,
+ type DeviceSessionId,
} from "@ledgerhq/device-management-kit";
export type DeviceSessionsState = {
@@ -9,16 +9,31 @@ export type DeviceSessionsState = {
deviceById: Record;
};
-export type AddSessionAction = {
+type AddSessionAction = {
type: "add_session";
payload: { sessionId: DeviceSessionId; connectedDevice: ConnectedDevice };
};
-export type RemoveSessionAction = {
+type RemoveSessionAction = {
type: "remove_session";
payload: { sessionId: DeviceSessionId };
};
+type RemoveAllSessionsAction = {
+ type: "remove_all_sessions";
+};
+
+export type DeviceSessionsAction =
+ | AddSessionAction
+ | RemoveSessionAction
+ | SelectSessionAction
+ | RemoveAllSessionsAction;
+
+export type SelectSessionAction = {
+ type: "select_session";
+ payload: { sessionId: DeviceSessionId };
+};
+
export const DeviceSessionsInitialState: DeviceSessionsState = {
selectedId: undefined,
deviceById: {},
@@ -26,8 +41,10 @@ export const DeviceSessionsInitialState: DeviceSessionsState = {
export const deviceSessionsReducer: Reducer<
DeviceSessionsState,
- AddSessionAction | RemoveSessionAction
+ DeviceSessionsAction
> = (state, action) => {
+ const sessionsCount = Object.keys(state.deviceById).length;
+
switch (action.type) {
case "add_session":
return {
@@ -43,7 +60,18 @@ export const deviceSessionsReducer: Reducer<
return {
...state,
- selectedId: undefined,
+ selectedId:
+ sessionsCount > 0
+ ? Object.keys(state.deviceById)[sessionsCount - 1]
+ : undefined,
+ };
+ case "remove_all_sessions":
+ return DeviceSessionsInitialState;
+
+ case "select_session":
+ return {
+ ...state,
+ selectedId: action.payload.sessionId,
};
default:
return state;
diff --git a/apps/sample/src/reducers/dmkConfig.ts b/apps/sample/src/reducers/dmkConfig.ts
new file mode 100644
index 000000000..fad34525c
--- /dev/null
+++ b/apps/sample/src/reducers/dmkConfig.ts
@@ -0,0 +1,51 @@
+import { type Reducer } from "react";
+import { BuiltinTransports } from "@ledgerhq/device-management-kit";
+
+export type DmkConfigState = {
+ mockServerUrl: string;
+ transport: BuiltinTransports;
+};
+
+type SetTransportAction = {
+ type: "set_transport";
+ payload: {
+ transport: BuiltinTransports;
+ };
+};
+
+type SetMockServerUrlAction = {
+ type: "set_mock_server_url";
+ payload: {
+ mockServerUrl: string;
+ };
+};
+
+export type DmkConfigAction = SetTransportAction | SetMockServerUrlAction;
+
+export const DmkConfigInitialState: DmkConfigState = {
+ mockServerUrl: "http://127.0.0.1:8080/",
+ transport:
+ (process.env.Dmk_CONFIG_TRANSPORT as BuiltinTransports) ||
+ BuiltinTransports.USB,
+};
+
+export const dmkConfigReducer: Reducer = (
+ state,
+ action,
+) => {
+ switch (action.type) {
+ case "set_transport":
+ return {
+ ...state,
+ transport: action.payload.transport,
+ };
+ case "set_mock_server_url":
+ return {
+ ...state,
+ mockServerUrl: action.payload.mockServerUrl,
+ };
+
+ default:
+ return state;
+ }
+};
diff --git a/apps/sample/src/styles/globalstyles.tsx b/apps/sample/src/styles/globalstyles.tsx
index 487e64123..d7db8e5e7 100644
--- a/apps/sample/src/styles/globalstyles.tsx
+++ b/apps/sample/src/styles/globalstyles.tsx
@@ -11,4 +11,13 @@ export const GlobalStyle = createGlobalStyle`
margin: 0;
background-color: #000000;
}
+ body {
+ user-select: none;
+ }
+ .no-scrollbar {
+ &::-webkit-scrollbar {
+ width: 0px;
+ height: 0px;
+ }
+ }
`;
diff --git a/apps/sample/src/utils/pipes.ts b/apps/sample/src/utils/pipes.ts
new file mode 100644
index 000000000..802701872
--- /dev/null
+++ b/apps/sample/src/utils/pipes.ts
@@ -0,0 +1,14 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+export const asyncPipe =
+ (...fns: Array<(arg: any) => any>) =>
+ (input: any): Promise =>
+ fns.reduce(
+ (chain, func) => Promise.resolve(chain).then(func),
+ Promise.resolve(input),
+ );
+
+export const pipe =
+ (...fns: Array<(arg: any) => any>) =>
+ (input: any): any =>
+ fns.reduce((chain, func) => func(chain), input);
diff --git a/apps/sample/tsconfig.eslint.json b/apps/sample/tsconfig.eslint.json
index 022d7d734..ae4346431 100644
--- a/apps/sample/tsconfig.eslint.json
+++ b/apps/sample/tsconfig.eslint.json
@@ -4,8 +4,8 @@
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
- "eslint.config.mjs",
"next.config.js",
- "postcss.config.js"
+ "postcss.config.js",
+ "eslint.config.mjs"
]
}
diff --git a/apps/sample/tsconfig.json b/apps/sample/tsconfig.json
index f9c79624a..96f3bd3df 100644
--- a/apps/sample/tsconfig.json
+++ b/apps/sample/tsconfig.json
@@ -1,5 +1,5 @@
{
- "extends": "@ledgerhq/tsconfig-dsdk/web",
+ "extends": "@ledgerhq/tsconfig-dsdk/tsconfig.web",
"compilerOptions": {
"plugins": [
{
@@ -11,6 +11,12 @@
},
"strictNullChecks": true
},
- "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ "eslint.config.mjs",
+ "next-env.d.ts",
+ ".next/types/**/*.ts"
+ ],
"exclude": ["node_modules"]
}
diff --git a/danger/dangerfile-ci.ts b/danger/dangerfile-ci.ts
index ddd61df7f..ede9d8389 100644
--- a/danger/dangerfile-ci.ts
+++ b/danger/dangerfile-ci.ts
@@ -8,11 +8,17 @@ import {
checkIfBot,
isFork,
} from "./helpers";
+import { exit } from "process";
const author = getAuthor(danger);
console.log("PR Actor:", author);
-checkIfBot(danger.github.pr.user);
+const isBot = checkIfBot(danger.github.pr.user);
+
+if (isBot) {
+ console.log("PR Actor is a bot, skipping checks...");
+ exit(0);
+}
const results: boolean[] = [];
diff --git a/danger/helpers.ts b/danger/helpers.ts
index b046f3bee..c6e8ee868 100644
--- a/danger/helpers.ts
+++ b/danger/helpers.ts
@@ -11,7 +11,7 @@ import { execSync } from "child_process";
type FailFn = (message: MarkdownString, file?: string, line?: number) => void;
type MessageFn = (
message: MarkdownString,
- opts?: { file?: string; line?: number; icon?: MarkdownString }
+ opts?: { file?: string; line?: number; icon?: MarkdownString },
) => void;
export const BRANCH_PREFIX = [
@@ -29,11 +29,7 @@ export const BRANCH_PREFIX = [
"refactor",
];
-export const checkIfBot = (user: GitHubPRDSL["user"]) => {
- if (user.type === "Bot") {
- exit(0);
- }
-};
+export const checkIfBot = (user: GitHubPRDSL["user"]) => user.type === "Bot";
export const getAuthor = (danger: DangerDSLType) => {
if (danger.github) {
@@ -48,13 +44,13 @@ export const isFork = (pr: GitHubPRDSL) => pr?.head?.repo?.fork ?? false;
const Branch = (
danger: DangerDSLType,
fail: FailFn,
- isFork: boolean = false
+ isFork: boolean = false,
) => ({
regex: isFork
? new RegExp(`^(${BRANCH_PREFIX.join("|")})\/.+`, "i")
: new RegExp(
`^(${BRANCH_PREFIX.join("|")})\/((dsdk)-[0-9]+|no-issue|issue-[0-9]+)\-.+`,
- "i"
+ "i",
),
getBranch: () => {
@@ -103,7 +99,7 @@ Please fix the PR branch name to match the convention, see [CONTRIBUTING.md](htt
export const checkBranches = (
danger: DangerDSLType,
fail: FailFn,
- fork: boolean = false
+ fork: boolean = false,
) => {
const config = Branch(danger, fail, fork);
const currentBranch = config.getBranch();
@@ -119,7 +115,7 @@ export const checkBranches = (
const Commits = (
danger: DangerDSLType,
fail: FailFn,
- fork: boolean = false
+ fork: boolean = false,
) => ({
regex: /^.+\(([a-z]+\-?){1,}\): [A-Z].*/,
@@ -150,7 +146,7 @@ Example: \`đ (scope): My feature\`\
const currentBranch = Branch(danger, fail, fork).getBranch();
return execSync(
- `git log origin/develop..${currentBranch} --pretty=format:%s`
+ `git log origin/develop..${currentBranch} --pretty=format:%s`,
)
.toString()
.split("\n");
@@ -160,14 +156,14 @@ Example: \`đ (scope): My feature\`\
export const checkCommits = (
danger: DangerDSLType,
fail: FailFn,
- fork: boolean = false
+ fork: boolean = false,
) => {
const config = Commits(danger, fail, fork);
const branchCommits = config.getCommits();
console.log("Branch commits:", branchCommits);
const wrongCommits = branchCommits.filter(
- (commit) => !config.regex.test(commit)
+ (commit) => !config.regex.test(commit),
);
if (wrongCommits.length > 0) {
@@ -229,7 +225,7 @@ Please fix the PR title to match the convention, see [CONTRIBUTING.md](https://g
export const checkTitle = (
danger: DangerDSLType,
fail: FailFn,
- fork: boolean = false
+ fork: boolean = false,
) => {
const config = Title(danger, fail, fork);
if (!config.regex.test(danger.github.pr.title)) {
@@ -247,7 +243,7 @@ export const checkChangesets = (danger: DangerDSLType, message: MessageFn) => {
`\ No changeset file found in the PR. Please add a changeset file.`,
{
icon: "â ī¸",
- }
+ },
);
return false;
}
diff --git a/package.json b/package.json
index 84423648c..c65361396 100644
--- a/package.json
+++ b/package.json
@@ -2,8 +2,9 @@
"name": "@ledgerhq/device-sdk",
"version": "1.0.0",
"private": true,
- "license": "MIT",
+ "license": "Apache-2.0",
"scripts": {
+ "clean": "rimraf -g **/.turbo **/.next **/coverage",
"build": "turbo run build",
"build:libs": "turbo run build --filter=./packages/**",
"dev": "turbo run dev",
@@ -15,13 +16,17 @@
"test:coverage": "turbo run test:coverage",
"typecheck": "turbo run typecheck",
"health-check": "turbo run health-check --output-logs=errors-only --continue",
- "core": "pnpm --filter @ledgerhq/device-management-kit",
+ "dmk": "pnpm --filter @ledgerhq/device-management-kit",
"context-module": "pnpm --filter @ledgerhq/context-module",
- "keyring-btc": "pnpm --filter @ledgerhq/keyring-btc",
- "keyring-eth": "pnpm --filter @ledgerhq/device-signer-kit-ethereum",
+ "signer-btc": "pnpm --filter @ledgerhq/device-signer-kit-btc",
+ "signer-eth": "pnpm --filter @ledgerhq/device-signer-kit-ethereum",
+ "signer-solana": "pnpm --filter @ledgerhq/device-signer-kit-solana",
+ "mock-client": "pnpm --filter @ledgerhq/device-transport-kit-mock-client",
"trusted-apps": "pnpm --filter @ledgerhq/device-sdk-trusted-apps",
"ui": "pnpm --filter @ledgerhq/device-sdk-ui",
- "sample": "pnpm --filter @ledgerhq/device-sdk-sample",
+ "flipper": "pnpm --filter @ledgerhq/device-management-kit-flipper-plugin-client",
+ "sample": "pnpm --filter @ledgerhq/device-management-kit-sample",
+ "doc": "pnpm --filter @ledgerhq/ledger-dmk-docs",
"bump": "changeset version",
"prerelease": "pnpm recursive exec -- pnpm pack",
"release": "changeset publish",
@@ -39,20 +44,22 @@
"@types/node": "^22.7.5",
"concurrently": "^9.0.1",
"danger": "^12.3.3",
- "eslint": "9.12.0",
- "gitmoji-cli": "^9.4.0",
+ "esbuild": "^0.24.0",
+ "esbuild-node-externals": "^1.14.0",
+ "eslint": "9.14.0",
+ "gitmoji-cli": "^9.5.0",
"hygen": "^6.2.11",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"rimraf": "^6.0.1",
"ts-jest": "^29.2.5",
"tsc-alias": "^1.8.10",
- "turbo": "^2.1.2",
- "typescript": "^5.6.2",
- "zx": "^8.1.8"
+ "turbo": "^2.2.3",
+ "typescript": "^5.6.3",
+ "zx": "^8.1.9"
},
"engines": {
"node": ">=20"
},
- "packageManager": "pnpm@9.10.0"
+ "packageManager": "pnpm@9.13.2"
}
diff --git a/packages/config/eslint/CHANGELOG.md b/packages/config/eslint/CHANGELOG.md
new file mode 100644
index 000000000..7623da85a
--- /dev/null
+++ b/packages/config/eslint/CHANGELOG.md
@@ -0,0 +1,7 @@
+# @ledgerhq/eslint-config-dsdk
+
+## 0.0.2
+
+### Patch Changes
+
+- [#438](https://github.com/LedgerHQ/device-sdk-ts/pull/438) [`d6273ed`](https://github.com/LedgerHQ/device-sdk-ts/commit/d6273ed00b61d273ebc42bd5dfa16ce4c5641af5) Thanks [@valpinkman](https://github.com/valpinkman)! - Update license to Apache-2.0
diff --git a/packages/config/eslint/index.js b/packages/config/eslint/index.js
index 3993b89e3..9fee7e78b 100644
--- a/packages/config/eslint/index.js
+++ b/packages/config/eslint/index.js
@@ -1,5 +1,3 @@
-// @ts-check
-
import globals from "globals";
import js from "@eslint/js";
import tseslint from "typescript-eslint";
@@ -13,6 +11,7 @@ export default [
{
ignores: [
".*.js",
+ ".*.mjs",
"coverage/*",
"_templates/*",
"lib/*",
@@ -95,6 +94,13 @@ export default [
message: "Prefer named exports",
},
],
+ "@typescript-eslint/consistent-type-imports": [
+ "warn",
+ {
+ prefer: "type-imports",
+ fixStyle: "inline-type-imports",
+ },
+ ],
"no-void": "off",
"no-restricted-imports": [
"error",
diff --git a/packages/config/eslint/package.json b/packages/config/eslint/package.json
index a9b122aab..15fea5f3f 100644
--- a/packages/config/eslint/package.json
+++ b/packages/config/eslint/package.json
@@ -1,7 +1,7 @@
{
"name": "@ledgerhq/eslint-config-dsdk",
- "license": "MIT",
- "version": "0.0.1",
+ "license": "Apache-2.0",
+ "version": "0.0.2",
"private": true,
"description": "Base ESLint configuration for LedgerHQ DSDK projects",
"main": "index.js",
@@ -10,13 +10,15 @@
],
"type": "module",
"devDependencies": {
- "@eslint/compat": "^1.2.0",
- "@eslint/js": "9.12.0",
+ "@eslint/compat": "^1.2.2",
+ "@eslint/js": "9.14.0",
+ "@types/eslint__js": "^8.42.3",
+ "@eslint/core": "^0.8.0",
"eslint-plugin-prettier": "^5.2.1",
- "eslint-plugin-react": "^7.37.1",
- "eslint-plugin-react-hooks": "^4.6.2",
+ "eslint-plugin-react": "^7.37.2",
+ "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-simple-import-sort": "12.1.1",
- "globals": "15.10.0",
- "typescript-eslint": "8.8.0"
+ "globals": "15.11.0",
+ "typescript-eslint": "8.13.0"
}
}
diff --git a/packages/config/jest/CHANGELOG.md b/packages/config/jest/CHANGELOG.md
new file mode 100644
index 000000000..14e4fd6bf
--- /dev/null
+++ b/packages/config/jest/CHANGELOG.md
@@ -0,0 +1,7 @@
+# @ledgerhq/jest-config-dsdk
+
+## 1.0.1
+
+### Patch Changes
+
+- [#438](https://github.com/LedgerHQ/device-sdk-ts/pull/438) [`d6273ed`](https://github.com/LedgerHQ/device-sdk-ts/commit/d6273ed00b61d273ebc42bd5dfa16ce4c5641af5) Thanks [@valpinkman](https://github.com/valpinkman)! - Update license to Apache-2.0
diff --git a/packages/config/jest/package.json b/packages/config/jest/package.json
index e2601b633..874cdadd1 100644
--- a/packages/config/jest/package.json
+++ b/packages/config/jest/package.json
@@ -1,6 +1,7 @@
{
"name": "@ledgerhq/jest-config-dsdk",
- "version": "1.0.0",
+ "license": "Apache-2.0",
+ "version": "1.0.1",
"main": "jest-preset.js",
"types": "node_modules/ts-jest/dist/index.d.ts",
"private": true,
diff --git a/packages/config/prettier/CHANGELOG.md b/packages/config/prettier/CHANGELOG.md
new file mode 100644
index 000000000..8ce55407e
--- /dev/null
+++ b/packages/config/prettier/CHANGELOG.md
@@ -0,0 +1,7 @@
+# @ledgerhq/prettier-config-dsdk
+
+## 0.0.2
+
+### Patch Changes
+
+- [#438](https://github.com/LedgerHQ/device-sdk-ts/pull/438) [`d6273ed`](https://github.com/LedgerHQ/device-sdk-ts/commit/d6273ed00b61d273ebc42bd5dfa16ce4c5641af5) Thanks [@valpinkman](https://github.com/valpinkman)! - Update license to Apache-2.0
diff --git a/packages/config/prettier/package.json b/packages/config/prettier/package.json
index 96ea807d4..52a147ceb 100644
--- a/packages/config/prettier/package.json
+++ b/packages/config/prettier/package.json
@@ -1,7 +1,7 @@
{
"name": "@ledgerhq/prettier-config-dsdk",
- "license": "MIT",
- "version": "0.0.1",
+ "license": "Apache-2.0",
+ "version": "0.0.2",
"private": true,
"files": [
"index.js"
diff --git a/packages/config/typescript/CHANGELOG.md b/packages/config/typescript/CHANGELOG.md
new file mode 100644
index 000000000..6cb2a9648
--- /dev/null
+++ b/packages/config/typescript/CHANGELOG.md
@@ -0,0 +1,7 @@
+# @ledgerhq/tsconfig-dsdk
+
+## 1.0.1
+
+### Patch Changes
+
+- [#438](https://github.com/LedgerHQ/device-sdk-ts/pull/438) [`d6273ed`](https://github.com/LedgerHQ/device-sdk-ts/commit/d6273ed00b61d273ebc42bd5dfa16ce4c5641af5) Thanks [@valpinkman](https://github.com/valpinkman)! - Update license to Apache-2.0
diff --git a/packages/config/typescript/package.json b/packages/config/typescript/package.json
index e30796add..16b374b49 100644
--- a/packages/config/typescript/package.json
+++ b/packages/config/typescript/package.json
@@ -1,13 +1,14 @@
{
"name": "@ledgerhq/tsconfig-dsdk",
- "version": "1.0.0",
+ "license": "Apache-2.0",
+ "version": "1.0.1",
"private": true,
"files": [
- "sdk.json",
- "web.json"
+ "tsconfig.sdk.json",
+ "tsconfig.web.json"
],
"devDependencies": {
- "@tsconfig/recommended": "^1.0.7",
+ "@tsconfig/recommended": "^1.0.8",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0"
}
diff --git a/packages/config/typescript/sdk.json b/packages/config/typescript/tsconfig.sdk.json
similarity index 87%
rename from packages/config/typescript/sdk.json
rename to packages/config/typescript/tsconfig.sdk.json
index 73c7da687..bb6ad643c 100644
--- a/packages/config/typescript/sdk.json
+++ b/packages/config/typescript/tsconfig.sdk.json
@@ -1,15 +1,19 @@
{
"extends": "@tsconfig/recommended/tsconfig.json",
"compilerOptions": {
- "target": "esnext",
- "lib": ["esnext", "dom"],
- "sourceMap": true,
+ "allowUnreachableCode": false,
+ "allowUnusedLabels": false,
+ "checkJs": false,
"declaration": true,
"declarationMap": true,
- "skipLibCheck": false,
- "strict": true,
- "allowUnusedLabels": false,
- "allowUnreachableCode": false,
+ "emitDecoratorMetadata": true,
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+ "lib": ["esnext", "dom", "dom.iterable"],
+ "module": "esnext",
+ "moduleResolution": "bundler",
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
@@ -17,15 +21,11 @@
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
- "isolatedModules": true,
- "checkJs": true,
- "esModuleInterop": true,
- "forceConsistentCasingInFileNames": true,
- "experimentalDecorators": true,
+ "skipLibCheck": false,
+ "sourceMap": true,
+ "strict": true,
"stripInternal": true,
- "emitDecoratorMetadata": true,
- "moduleResolution": "bundler",
- "module": "esnext"
+ "target": "esnext"
},
"exclude": ["node_modules"]
}
diff --git a/packages/config/typescript/web.json b/packages/config/typescript/tsconfig.web.json
similarity index 100%
rename from packages/config/typescript/web.json
rename to packages/config/typescript/tsconfig.web.json
diff --git a/packages/core/src/api/DeviceSdkBuilder.ts b/packages/core/src/api/DeviceSdkBuilder.ts
deleted file mode 100644
index 12b38e942..000000000
--- a/packages/core/src/api/DeviceSdkBuilder.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { DEFAULT_MANAGER_API_BASE_URL } from "@internal/manager-api/model/Const";
-
-import { LoggerSubscriberService } from "./logger-subscriber/service/LoggerSubscriberService";
-import { DeviceSdk } from "./DeviceSdk";
-import { SdkConfig } from "./SdkConfig";
-
-/**
- * Builder for the `DeviceSdk` class.
- *
- * @example
- * ```
- * const sdk = new LedgerDeviceSdkBuilder()
- * .setStub(false)
- * .addLogger(myLogger)
- * .build();
- * ```
- */
-export class LedgerDeviceSdkBuilder {
- private stub = false;
- private readonly loggers: LoggerSubscriberService[] = [];
- private config: SdkConfig = {
- managerApiUrl: DEFAULT_MANAGER_API_BASE_URL,
- };
-
- build(): DeviceSdk {
- return new DeviceSdk({
- stub: this.stub,
- loggers: this.loggers,
- config: this.config,
- });
- }
-
- setStub(stubbed: boolean): LedgerDeviceSdkBuilder {
- this.stub = stubbed;
- return this;
- }
-
- /**
- * Add a logger to the SDK that will receive its logs
- */
- addLogger(logger: LoggerSubscriberService): LedgerDeviceSdkBuilder {
- this.loggers.push(logger);
- return this;
- }
-
- addConfig(config: SdkConfig): LedgerDeviceSdkBuilder {
- this.config = {
- ...this.config,
- ...config,
- };
- return this;
- }
-}
diff --git a/packages/core/src/api/SdkConfig.ts b/packages/core/src/api/SdkConfig.ts
deleted file mode 100644
index a910a5f2b..000000000
--- a/packages/core/src/api/SdkConfig.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export type SdkConfig = {
- managerApiUrl: string;
-};
diff --git a/packages/core/src/api/logger-subscriber/service/LoggerSubscriberService.ts b/packages/core/src/api/logger-subscriber/service/LoggerSubscriberService.ts
deleted file mode 100644
index a4a9000eb..000000000
--- a/packages/core/src/api/logger-subscriber/service/LoggerSubscriberService.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { LogLevel } from "@api/logger-subscriber/model/LogLevel";
-import { LogSubscriberOptions } from "@api/logger-subscriber/model/LogSubscriberOptions";
-
-/**
- * Logger subscriber service.
- *
- * Implement this interface and use `LedgerDeviceSdkBuilder.addLogger` to
- * receive logs from the SDK.
- */
-export interface LoggerSubscriberService {
- log(level: LogLevel, message: string, options: LogSubscriberOptions): void;
-}
diff --git a/packages/core/src/api/usb/model/DiscoveredDevice.ts b/packages/core/src/api/usb/model/DiscoveredDevice.ts
deleted file mode 100644
index ca7fd41d5..000000000
--- a/packages/core/src/api/usb/model/DiscoveredDevice.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { DeviceId, DeviceModel } from "@api/device/DeviceModel";
-
-/**
- * A discovered device.
- */
-export type DiscoveredDevice = {
- readonly id: DeviceId;
- readonly deviceModel: DeviceModel;
-};
diff --git a/packages/core/src/internal/config/service/ConfigService.ts b/packages/core/src/internal/config/service/ConfigService.ts
deleted file mode 100644
index ca7a9cc9b..000000000
--- a/packages/core/src/internal/config/service/ConfigService.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Config } from "@internal/config/model/Config";
-
-export interface ConfigService {
- getSdkConfig(): Promise;
-}
diff --git a/packages/core/src/internal/config/use-case/GetSdkVersionUseCase.test.ts b/packages/core/src/internal/config/use-case/GetSdkVersionUseCase.test.ts
deleted file mode 100644
index d16f097ac..000000000
--- a/packages/core/src/internal/config/use-case/GetSdkVersionUseCase.test.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { GetSdkVersionUseCase } from "./GetSdkVersionUseCase";
-
-const getSdkConfigMock = jest.fn();
-
-let usecase: GetSdkVersionUseCase;
-describe("GetSdkVersionUseCase", () => {
- beforeEach(() => {
- getSdkConfigMock.mockClear();
- const configService = {
- getSdkConfig: getSdkConfigMock,
- };
-
- usecase = new GetSdkVersionUseCase(configService);
- });
-
- it("should return the sdk version", async () => {
- getSdkConfigMock.mockResolvedValue({
- name: "DeviceSDK",
- version: "1.0.0",
- });
- expect(await usecase.getSdkVersion()).toBe("1.0.0");
- });
-});
diff --git a/packages/core/src/internal/device-model/data/DeviceModelDataSource.ts b/packages/core/src/internal/device-model/data/DeviceModelDataSource.ts
deleted file mode 100644
index fb56a8f46..000000000
--- a/packages/core/src/internal/device-model/data/DeviceModelDataSource.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { DeviceModelId } from "@api/device/DeviceModel";
-import { InternalDeviceModel } from "@internal/device-model/model/DeviceModel";
-
-/**
- * Source of truth for the device models
- */
-export interface DeviceModelDataSource {
- getAllDeviceModels(): InternalDeviceModel[];
-
- getDeviceModel(params: { id: DeviceModelId }): InternalDeviceModel;
-
- filterDeviceModels(
- params: Partial,
- ): InternalDeviceModel[];
-}
diff --git a/packages/core/src/internal/device-session/model/DeviceSessionRefresher.test.ts b/packages/core/src/internal/device-session/model/DeviceSessionRefresher.test.ts
deleted file mode 100644
index 75819b7ce..000000000
--- a/packages/core/src/internal/device-session/model/DeviceSessionRefresher.test.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { Left, Right } from "purify-ts";
-
-import { CommandResultFactory } from "@api/command/model/CommandResult";
-import {
- GetAppAndVersionCommand,
- GetAppAndVersionResponse,
-} from "@api/command/os/GetAppAndVersionCommand";
-import { DeviceStatus } from "@api/device/DeviceStatus";
-import { ApduResponse } from "@api/device-session/ApduResponse";
-import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
-
-import { DeviceSessionRefresher } from "./DeviceSessionRefresher";
-
-const mockSendApduFn = jest.fn().mockResolvedValue(Right({} as ApduResponse));
-const mockUpdateStateFn = jest.fn().mockImplementation(() => undefined);
-
-jest.useFakeTimers();
-
-describe("DeviceSessionRefresher", () => {
- let deviceSessionRefresher: DeviceSessionRefresher;
- let logger: LoggerPublisherService;
-
- beforeEach(() => {
- jest
- .spyOn(GetAppAndVersionCommand.prototype, "parseResponse")
- .mockReturnValueOnce(
- CommandResultFactory({
- data: {
- name: "testAppName",
- } as GetAppAndVersionResponse,
- }),
- );
- logger = new DefaultLoggerPublisherService(
- [],
- "DeviceSessionRefresherTest",
- );
- deviceSessionRefresher = new DeviceSessionRefresher(
- {
- refreshInterval: 1000,
- deviceStatus: DeviceStatus.CONNECTED,
- sendApduFn: mockSendApduFn,
- updateStateFn: mockUpdateStateFn,
- },
- logger,
- );
- });
-
- afterEach(() => {
- deviceSessionRefresher.stop();
- jest.clearAllMocks();
- });
-
- it("should poll by calling sendApduFn", () => {
- jest.advanceTimersByTime(1000);
- expect(mockSendApduFn).toHaveBeenCalledTimes(1);
-
- jest.advanceTimersByTime(1000);
- expect(mockSendApduFn).toHaveBeenCalledTimes(2);
- });
-
- it("should not poll when device is busy", () => {
- deviceSessionRefresher.setDeviceStatus(DeviceStatus.BUSY);
-
- jest.advanceTimersByTime(1000);
-
- expect(mockSendApduFn).not.toHaveBeenCalled();
- });
-
- it("should not poll when device is disconnected", () => {
- deviceSessionRefresher.setDeviceStatus(DeviceStatus.NOT_CONNECTED);
-
- jest.advanceTimersByTime(1000);
-
- expect(mockSendApduFn).not.toHaveBeenCalled();
- });
-
- it("should update device session state by calling updateStateFn", async () => {
- jest.advanceTimersByTime(1000);
-
- expect(await mockSendApduFn()).toEqual(Right({}));
- expect(mockUpdateStateFn).toHaveBeenCalled();
- });
-
- it("should not update device session state with failed polling response", async () => {
- mockSendApduFn.mockResolvedValueOnce(Left("error"));
- const spy = jest.spyOn(logger, "error");
-
- jest.advanceTimersByTime(1000);
- await mockSendApduFn();
-
- expect(mockUpdateStateFn).not.toHaveBeenCalled();
- expect(spy).toHaveBeenCalled();
- });
-
- it("should stop the refresher when device is disconnected", () => {
- const spy = jest.spyOn(deviceSessionRefresher, "stop");
- deviceSessionRefresher.setDeviceStatus(DeviceStatus.NOT_CONNECTED);
- expect(spy).toHaveBeenCalledTimes(1);
- });
-
- it("should not throw error if stop is called on a stopped refresher", () => {
- deviceSessionRefresher.stop();
- expect(() => deviceSessionRefresher.stop()).not.toThrow();
- });
-});
diff --git a/packages/core/src/internal/device-session/model/DeviceSessionRefresher.ts b/packages/core/src/internal/device-session/model/DeviceSessionRefresher.ts
deleted file mode 100644
index fc2370167..000000000
--- a/packages/core/src/internal/device-session/model/DeviceSessionRefresher.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import { injectable } from "inversify";
-import { Either } from "purify-ts";
-import { filter, interval, map, Subscription, switchMap } from "rxjs";
-
-import { isSuccessCommandResult } from "@api/command/model/CommandResult";
-import { GetAppAndVersionCommand } from "@api/command/os/GetAppAndVersionCommand";
-import { DeviceStatus } from "@api/device/DeviceStatus";
-import { ApduResponse } from "@api/device-session/ApduResponse";
-import {
- DeviceSessionState,
- DeviceSessionStateType,
-} from "@api/device-session/DeviceSessionState";
-import { SdkError } from "@api/Error";
-import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
-
-/**
- * The arguments for the DeviceSessionRefresher.
- */
-export type DeviceSessionRefresherArgs = {
- /**
- * The refresh interval in milliseconds.
- */
- refreshInterval: number;
-
- /**
- * The current device status when the refresher is created.
- */
- deviceStatus: Exclude;
-
- /**
- * The function used to send APDU commands to the device.
- */
- sendApduFn: (rawApdu: Uint8Array) => Promise>;
-
- /**
- * Callback that updates the state of the device session with
- * polling response.
- * @param callback - A function that will take the previous state and return the new state.
- * @returns void
- */
- updateStateFn(
- callback: (state: DeviceSessionState) => DeviceSessionState,
- ): void;
-};
-
-/**
- * The session refresher that periodically sends a command to refresh the session.
- */
-@injectable()
-export class DeviceSessionRefresher {
- private readonly _logger: LoggerPublisherService;
- private readonly _getAppAndVersionCommand = new GetAppAndVersionCommand();
- private _deviceStatus: DeviceStatus;
- private _subscription: Subscription;
-
- constructor(
- {
- refreshInterval,
- deviceStatus,
- sendApduFn,
- updateStateFn,
- }: DeviceSessionRefresherArgs,
- logger: LoggerPublisherService,
- ) {
- this._deviceStatus = deviceStatus;
- this._logger = logger;
- this._subscription = interval(refreshInterval)
- .pipe(
- filter(
- () =>
- ![DeviceStatus.BUSY, DeviceStatus.NOT_CONNECTED].includes(
- this._deviceStatus,
- ),
- ),
- switchMap(() => {
- const rawApdu = this._getAppAndVersionCommand.getApdu().getRawApdu();
- return sendApduFn(rawApdu);
- }),
- map((resp) =>
- resp.caseOf({
- Left: (error) => {
- this._logger.error("Error in sending APDU when polling", {
- data: { error },
- });
- return null;
- },
- Right: (data: ApduResponse) => {
- try {
- return this._getAppAndVersionCommand.parseResponse(data);
- } catch (error) {
- this._logger.error("Error in parsing APDU response", {
- data: { error },
- });
- return null;
- }
- },
- }),
- ),
- filter((parsedResponse) => parsedResponse !== null),
- )
- .subscribe((parsedResponse) => {
- if (!isSuccessCommandResult(parsedResponse)) {
- return;
- }
- // `batteryStatus` and `firmwareVersion` are not available in the polling response.
- updateStateFn((state) => ({
- ...state,
- sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel,
- deviceStatus: this._deviceStatus,
- currentApp: parsedResponse.data,
- installedApps: "installedApps" in state ? state.installedApps : [],
- }));
- });
- }
-
- /**
- * Maintain a device status to prevent sending APDU when the device is busy.
- *
- * @param {DeviceStatus} deviceStatus - The new device status.
- */
- setDeviceStatus(deviceStatus: DeviceStatus) {
- if (deviceStatus === DeviceStatus.NOT_CONNECTED) {
- this.stop();
- }
- this._deviceStatus = deviceStatus;
- }
-
- /**
- * Stops the session refresher.
- * The refresher will no longer send commands to refresh the session.
- */
- stop() {
- if (!this._subscription || this._subscription.closed) {
- return;
- }
- this._subscription.unsubscribe();
- }
-}
diff --git a/packages/core/src/internal/device-session/service/ApduReceiverService.ts b/packages/core/src/internal/device-session/service/ApduReceiverService.ts
deleted file mode 100644
index 7185891e8..000000000
--- a/packages/core/src/internal/device-session/service/ApduReceiverService.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Either, Maybe } from "purify-ts";
-
-import { ApduResponse } from "@api/device-session/ApduResponse";
-import { SdkError } from "@api/Error";
-
-export interface ApduReceiverService {
- handleFrame(apdu: Uint8Array): Either>;
-}
diff --git a/packages/core/src/internal/device-session/service/DeviceSessionService.ts b/packages/core/src/internal/device-session/service/DeviceSessionService.ts
deleted file mode 100644
index ca486fa5d..000000000
--- a/packages/core/src/internal/device-session/service/DeviceSessionService.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Either } from "purify-ts";
-
-import { SdkError } from "@api/Error";
-import { DeviceSession } from "@internal/device-session/model/DeviceSession";
-
-export interface DeviceSessionService {
- addDeviceSession(deviceSession: DeviceSession): DeviceSessionService;
- getDeviceSessionById(sessionId: string): Either;
- getDeviceSessionByDeviceId(deviceId: string): Either;
- removeDeviceSession(sessionId: string): DeviceSessionService;
- getDeviceSessions(): DeviceSession[];
-}
diff --git a/packages/core/src/internal/discovery/di/discoveryTypes.ts b/packages/core/src/internal/discovery/di/discoveryTypes.ts
deleted file mode 100644
index 220c352aa..000000000
--- a/packages/core/src/internal/discovery/di/discoveryTypes.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export const discoveryTypes = {
- StartDiscoveringUseCase: Symbol.for("StartDiscoveringUseCase"),
- StopDiscoveringUseCase: Symbol.for("StopDiscoveringUseCase"),
- ConnectUseCase: Symbol.for("ConnectUseCase"),
- DisconnectUseCase: Symbol.for("DisconnectUseCase"),
-};
diff --git a/packages/core/src/internal/discovery/use-case/StartDiscoveringUseCase.ts b/packages/core/src/internal/discovery/use-case/StartDiscoveringUseCase.ts
deleted file mode 100644
index 8e6fd73ca..000000000
--- a/packages/core/src/internal/discovery/use-case/StartDiscoveringUseCase.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { inject, injectable } from "inversify";
-import { map, Observable } from "rxjs";
-
-import { DeviceModel } from "@api/device/DeviceModel";
-import { DiscoveredDevice } from "@api/types";
-import { usbDiTypes } from "@internal/usb/di/usbDiTypes";
-import { InternalDiscoveredDevice } from "@internal/usb/model/InternalDiscoveredDevice";
-import type { UsbHidTransport } from "@internal/usb/transport/UsbHidTransport";
-
-/**
- * Starts discovering devices connected via USB HID (BLE not implemented yet).
- *
- * For the WebHID implementation, this use-case needs to be called as a result of an user interaction (button "click" event for ex).
- */
-@injectable()
-export class StartDiscoveringUseCase {
- constructor(
- @inject(usbDiTypes.UsbHidTransport)
- private usbHidTransport: UsbHidTransport,
- // Later: @inject(usbDiTypes.BleTransport) private bleTransport: BleTransport,
- ) {}
-
- execute(): Observable {
- return this.usbHidTransport.startDiscovering().pipe(
- map((data: InternalDiscoveredDevice) => {
- const deviceModel = new DeviceModel({
- id: data.id,
- model: data.deviceModel.id,
- name: data.deviceModel.productName,
- });
- return {
- id: data.id,
- deviceModel,
- };
- }),
- );
- }
-}
diff --git a/packages/core/src/internal/discovery/use-case/StopDiscoveringUseCase.ts b/packages/core/src/internal/discovery/use-case/StopDiscoveringUseCase.ts
deleted file mode 100644
index 56e30c29a..000000000
--- a/packages/core/src/internal/discovery/use-case/StopDiscoveringUseCase.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { inject, injectable } from "inversify";
-
-import { usbDiTypes } from "@internal/usb/di/usbDiTypes";
-import type { UsbHidTransport } from "@internal/usb/transport/UsbHidTransport";
-
-/**
- * Stops discovering devices connected via USB HID (and later BLE).
- */
-@injectable()
-export class StopDiscoveringUseCase {
- constructor(
- @inject(usbDiTypes.UsbHidTransport)
- private usbHidTransport: UsbHidTransport,
- // Later: @inject(usbDiTypes.BleTransport) private bleTransport: BleTransport,
- ) {}
-
- execute(): void {
- return this.usbHidTransport.stopDiscovering();
- }
-}
diff --git a/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts b/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts
deleted file mode 100644
index e5a90491e..000000000
--- a/packages/core/src/internal/manager-api/data/ManagerApiDataSource.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { EitherAsync } from "purify-ts";
-
-import { HttpFetchApiError } from "@internal/manager-api/model/Errors";
-import { Application } from "@internal/manager-api/model/ManagerApiType";
-
-export interface ManagerApiDataSource {
- getAppsByHash(
- hashes: string[],
- ): EitherAsync>;
-}
diff --git a/packages/core/src/internal/manager-api/service/ManagerApiService.ts b/packages/core/src/internal/manager-api/service/ManagerApiService.ts
deleted file mode 100644
index 466702f05..000000000
--- a/packages/core/src/internal/manager-api/service/ManagerApiService.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { EitherAsync } from "purify-ts";
-
-import { ListAppsResponse } from "@api/command/os/ListAppsCommand";
-import { HttpFetchApiError } from "@internal/manager-api/model/Errors";
-import { Application } from "@internal/manager-api/model/ManagerApiType";
-
-export interface ManagerApiService {
- getAppsByHash(
- apps: ListAppsResponse,
- ): EitherAsync>;
-}
diff --git a/packages/core/src/internal/usb/data/UsbHidConfig.ts b/packages/core/src/internal/usb/data/UsbHidConfig.ts
deleted file mode 100644
index 5b486479f..000000000
--- a/packages/core/src/internal/usb/data/UsbHidConfig.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// [SHOULD] Move it to device-model module
-export const LEDGER_VENDOR_ID = 0x2c97;
-export const FRAME_SIZE = 64;
-export const RECONNECT_DEVICE_TIMEOUT = 2000;
diff --git a/packages/core/src/internal/usb/di/usbDiTypes.ts b/packages/core/src/internal/usb/di/usbDiTypes.ts
deleted file mode 100644
index 0f40ecfdd..000000000
--- a/packages/core/src/internal/usb/di/usbDiTypes.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export const usbDiTypes = {
- UsbHidTransport: Symbol.for("UsbHidTransport"),
- UsbHidDeviceConnectionFactory: Symbol.for("UsbHidDeviceConnectionFactory"),
- GetConnectedDeviceUseCase: Symbol.for("GetConnectedDeviceUseCase"),
-};
diff --git a/packages/core/src/internal/usb/di/usbModule.ts b/packages/core/src/internal/usb/di/usbModule.ts
deleted file mode 100644
index bc3267faa..000000000
--- a/packages/core/src/internal/usb/di/usbModule.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { ContainerModule } from "inversify";
-
-import { UsbHidDeviceConnectionFactory } from "@internal/usb/service/UsbHidDeviceConnectionFactory";
-import { WebUsbHidTransport } from "@internal/usb/transport/WebUsbHidTransport";
-import { GetConnectedDeviceUseCase } from "@internal/usb/use-case/GetConnectedDeviceUseCase";
-import { StubUseCase } from "@root/src/di.stub";
-
-import { usbDiTypes } from "./usbDiTypes";
-
-type FactoryProps = {
- stub: boolean;
-};
-
-export const usbModuleFactory = ({ stub = false }: FactoryProps) =>
- new ContainerModule((bind, _unbind, _isBound, rebind) => {
- // The transport needs to be a singleton to keep the internal states of the devices
- bind(usbDiTypes.UsbHidTransport).to(WebUsbHidTransport).inSingletonScope();
-
- // UsbHidDeviceConnectionFactory
- bind(usbDiTypes.UsbHidDeviceConnectionFactory).to(
- UsbHidDeviceConnectionFactory,
- );
-
- // GetConnectedDeviceUseCase
- bind(usbDiTypes.GetConnectedDeviceUseCase).to(GetConnectedDeviceUseCase);
-
- if (stub) {
- rebind(usbDiTypes.GetConnectedDeviceUseCase).to(StubUseCase);
- }
- });
diff --git a/packages/core/src/internal/usb/model/Errors.ts b/packages/core/src/internal/usb/model/Errors.ts
deleted file mode 100644
index f566dc0b5..000000000
--- a/packages/core/src/internal/usb/model/Errors.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { SdkError } from "@api/Error";
-
-export type PromptDeviceAccessError =
- | UsbHidTransportNotSupportedError
- | NoAccessibleDeviceError;
-
-export type ConnectError = UnknownDeviceError | OpeningConnectionError;
-
-class GeneralSdkError implements SdkError {
- _tag = "GeneralSdkError";
- originalError?: unknown;
- constructor(err?: unknown) {
- if (err instanceof Error) {
- this.originalError = err;
- } else {
- this.originalError = new Error(String(err));
- }
- }
-}
-
-export class DeviceNotRecognizedError extends GeneralSdkError {
- override readonly _tag = "DeviceNotRecognizedError";
- constructor(readonly err?: unknown) {
- super(err);
- }
-}
-
-export class NoAccessibleDeviceError extends GeneralSdkError {
- override readonly _tag = "NoAccessibleDeviceError";
- constructor(readonly err?: unknown) {
- super(err);
- }
-}
-
-export class OpeningConnectionError extends GeneralSdkError {
- override readonly _tag = "ConnectionOpeningError";
- constructor(readonly err?: unknown) {
- super(err);
- }
-}
-
-export class UnknownDeviceError extends GeneralSdkError {
- override readonly _tag = "UnknownDeviceError";
- constructor(readonly err?: unknown) {
- super(err);
- }
-}
-
-export class UsbHidTransportNotSupportedError extends GeneralSdkError {
- override readonly _tag = "UsbHidTransportNotSupportedError";
- constructor(readonly err?: unknown) {
- super(err);
- }
-}
-
-export class SendApduConcurrencyError extends GeneralSdkError {
- override readonly _tag = "SendApduConcurrencyError";
- constructor(readonly err?: unknown) {
- super(err);
- }
-}
-
-export class DisconnectError extends GeneralSdkError {
- override readonly _tag = "DisconnectError";
- constructor(readonly err?: unknown) {
- super(err);
- }
-}
-
-export class ReconnectionFailedError extends GeneralSdkError {
- override readonly _tag = "ReconnectionFailedError";
- constructor(readonly err?: unknown) {
- super(err);
- }
-}
diff --git a/packages/core/src/internal/usb/transport/DeviceConnection.ts b/packages/core/src/internal/usb/transport/DeviceConnection.ts
deleted file mode 100644
index 30aa65f8a..000000000
--- a/packages/core/src/internal/usb/transport/DeviceConnection.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Either } from "purify-ts";
-
-import { ApduResponse } from "@api/device-session/ApduResponse";
-import { SdkError } from "@api/Error";
-
-export type SendApduFnType = (
- apdu: Uint8Array,
- triggersDisconnection?: boolean,
-) => Promise>;
-
-export interface DeviceConnection {
- sendApdu: SendApduFnType;
-}
diff --git a/packages/core/src/internal/usb/transport/UsbHidDeviceConnection.test.ts b/packages/core/src/internal/usb/transport/UsbHidDeviceConnection.test.ts
deleted file mode 100644
index 49f7af8a4..000000000
--- a/packages/core/src/internal/usb/transport/UsbHidDeviceConnection.test.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-import { Left, Right } from "purify-ts";
-
-import { ApduReceiverService } from "@internal/device-session/service/ApduReceiverService";
-import { ApduSenderService } from "@internal/device-session/service/ApduSenderService";
-import { defaultApduReceiverServiceStubBuilder } from "@internal/device-session/service/DefaultApduReceiverService.stub";
-import { defaultApduSenderServiceStubBuilder } from "@internal/device-session/service/DefaultApduSenderService.stub";
-import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
-import { ReconnectionFailedError } from "@internal/usb/model/Errors";
-import { hidDeviceStubBuilder } from "@internal/usb/model/HIDDevice.stub";
-import { UsbHidDeviceConnection } from "@internal/usb/transport/UsbHidDeviceConnection";
-
-jest.useFakeTimers();
-
-const RESPONSE_LOCKED_DEVICE = new Uint8Array([
- 0xaa, 0xaa, 0x05, 0x00, 0x00, 0x00, 0x02, 0x55, 0x15, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-]);
-
-const RESPONSE_SUCCESS = new Uint8Array([
- 0xaa, 0xaa, 0x05, 0x00, 0x00, 0x00, 0x02, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-]);
-
-/**
- * Flushes all pending promises
- */
-const flushPromises = () =>
- new Promise(jest.requireActual("timers").setImmediate);
-
-describe("UsbHidDeviceConnection", () => {
- let device: HIDDevice;
- let apduSender: ApduSenderService;
- let apduReceiver: ApduReceiverService;
- const logger = (tag: string) => new DefaultLoggerPublisherService([], tag);
-
- beforeEach(() => {
- device = hidDeviceStubBuilder();
- apduSender = defaultApduSenderServiceStubBuilder(undefined, logger);
- apduReceiver = defaultApduReceiverServiceStubBuilder(undefined, logger);
- });
-
- it("should get device", () => {
- // given
- const connection = new UsbHidDeviceConnection(
- { device, apduSender, apduReceiver },
- logger,
- );
- // when
- const cDevice = connection.device;
- // then
- expect(cDevice).toStrictEqual(device);
- });
-
- it("should send APDU through hid report", () => {
- // given
- const connection = new UsbHidDeviceConnection(
- { device, apduSender, apduReceiver },
- logger,
- );
- // when
- connection.sendApdu(new Uint8Array(0));
- // then
- expect(device.sendReport).toHaveBeenCalled();
- });
-
- it("should receive APDU through hid report", async () => {
- // given
- device.sendReport = jest.fn(() =>
- Promise.resolve(
- device.oninputreport!({
- type: "inputreport",
- data: new DataView(Uint8Array.from(RESPONSE_SUCCESS).buffer),
- } as HIDInputReportEvent),
- ),
- );
- const connection = new UsbHidDeviceConnection(
- { device, apduSender, apduReceiver },
- logger,
- );
- // when
- const response = await connection.sendApdu(Uint8Array.from([]));
- // then
- expect(response).toEqual(
- Right({
- statusCode: new Uint8Array([0x90, 0x00]),
- data: new Uint8Array([]),
- }),
- );
- });
-
- test("sendApdu(whatever, true) should wait for reconnection before resolving if the response is a success", async () => {
- // given
- device.sendReport = jest.fn(() =>
- Promise.resolve(
- device.oninputreport!({
- type: "inputreport",
- data: new DataView(Uint8Array.from(RESPONSE_SUCCESS).buffer),
- } as HIDInputReportEvent),
- ),
- );
- const connection = new UsbHidDeviceConnection(
- { device, apduSender, apduReceiver },
- logger,
- );
-
- let hasResolved = false;
- const responsePromise = connection
- .sendApdu(Uint8Array.from([]), true)
- .then((response) => {
- hasResolved = true;
- return response;
- });
-
- // before reconnecting
- await flushPromises();
- expect(hasResolved).toBe(false);
-
- // when reconnecting
- connection.device = device;
- await flushPromises();
- expect(hasResolved).toBe(true);
-
- const response = await responsePromise;
-
- expect(response).toEqual(
- Right({
- statusCode: new Uint8Array([0x90, 0x00]),
- data: new Uint8Array([]),
- }),
- );
- });
-
- test("sendApdu(whatever, true) should not wait for reconnection if the response is not a success", async () => {
- // given
- device.sendReport = jest.fn(() =>
- Promise.resolve(
- device.oninputreport!({
- type: "inputreport",
- data: new DataView(Uint8Array.from(RESPONSE_LOCKED_DEVICE).buffer),
- } as HIDInputReportEvent),
- ),
- );
- const connection = new UsbHidDeviceConnection(
- { device, apduSender, apduReceiver },
- logger,
- );
-
- // when
- const response = await connection.sendApdu(Uint8Array.from([]), true);
-
- // then
- expect(response).toEqual(
- Right({
- statusCode: new Uint8Array([0x55, 0x15]),
- data: new Uint8Array([]),
- }),
- );
- });
-
- test("sendApdu(whatever, true) should return an error if the device gets disconnected while waiting for reconnection", async () => {
- // given
- device.sendReport = jest.fn(() =>
- Promise.resolve(
- device.oninputreport!({
- type: "inputreport",
- data: new DataView(Uint8Array.from(RESPONSE_SUCCESS).buffer),
- } as HIDInputReportEvent),
- ),
- );
- const connection = new UsbHidDeviceConnection(
- { device, apduSender, apduReceiver },
- logger,
- );
-
- const responsePromise = connection.sendApdu(Uint8Array.from([]), true);
-
- // when disconnecting
- connection.disconnect();
- await flushPromises();
-
- // then
- const response = await responsePromise;
- expect(response).toEqual(Left(new ReconnectionFailedError()));
- });
-});
diff --git a/packages/core/src/internal/usb/transport/UsbHidDeviceConnection.ts b/packages/core/src/internal/usb/transport/UsbHidDeviceConnection.ts
deleted file mode 100644
index 4011ab127..000000000
--- a/packages/core/src/internal/usb/transport/UsbHidDeviceConnection.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-import { inject } from "inversify";
-import { Either, Left, Maybe, Right } from "purify-ts";
-import { Subject } from "rxjs";
-
-import { CommandUtils } from "@api/command/utils/CommandUtils";
-import { ApduResponse } from "@api/device-session/ApduResponse";
-import { SdkError } from "@api/Error";
-import { ApduReceiverService } from "@internal/device-session/service/ApduReceiverService";
-import { ApduSenderService } from "@internal/device-session/service/ApduSenderService";
-import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes";
-import type { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
-import { ReconnectionFailedError } from "@internal/usb/model/Errors";
-
-import { DeviceConnection } from "./DeviceConnection";
-
-type UsbHidDeviceConnectionConstructorArgs = {
- device: HIDDevice;
- apduSender: ApduSenderService;
- apduReceiver: ApduReceiverService;
-};
-
-export class UsbHidDeviceConnection implements DeviceConnection {
- private _device: HIDDevice;
- private readonly _apduSender: ApduSenderService;
- private readonly _apduReceiver: ApduReceiverService;
- private _sendApduSubject: Subject;
- private readonly _logger: LoggerPublisherService;
- private _settleReconnectionPromise: Maybe<{
- resolve(): void;
- reject(err: SdkError): void;
- }> = Maybe.zero();
-
- constructor(
- { device, apduSender, apduReceiver }: UsbHidDeviceConnectionConstructorArgs,
- @inject(loggerTypes.LoggerPublisherServiceFactory)
- loggerServiceFactory: (tag: string) => LoggerPublisherService,
- ) {
- this._apduSender = apduSender;
- this._apduReceiver = apduReceiver;
- this._sendApduSubject = new Subject();
- this._logger = loggerServiceFactory("UsbHidDeviceConnection");
- this._device = device;
- this._device.oninputreport = (event) => this.receiveHidInputReport(event);
- }
-
- public get device() {
- return this._device;
- }
-
- public set device(device: HIDDevice) {
- this._device = device;
- this._device.oninputreport = (event) => this.receiveHidInputReport(event);
-
- this._settleReconnectionPromise.ifJust(() => {
- this.reconnected();
- });
- }
-
- async sendApdu(
- apdu: Uint8Array,
- triggersDisconnection?: boolean,
- ): Promise> {
- this._sendApduSubject = new Subject();
-
- this._logger.debug("Sending APDU", {
- data: { apdu },
- tag: "apdu-sender",
- });
-
- const resultPromise = new Promise>(
- (resolve) => {
- this._sendApduSubject.subscribe({
- next: async (r) => {
- if (triggersDisconnection && CommandUtils.isSuccessResponse(r)) {
- const reconnectionRes = await this.setupWaitForReconnection();
- reconnectionRes.caseOf({
- Left: (err) => resolve(Left(err)),
- Right: () => resolve(Right(r)),
- });
- } else {
- resolve(Right(r));
- }
- },
- error: (err) => {
- resolve(Left(err));
- },
- });
- },
- );
-
- const frames = this._apduSender.getFrames(apdu);
- for (const frame of frames) {
- this._logger.debug("Sending Frame", {
- data: { frame: frame.getRawData() },
- });
- try {
- await this._device.sendReport(0, frame.getRawData());
- } catch (error) {
- this._logger.error("Error sending frame", { data: { error } });
- }
- }
-
- return resultPromise;
- }
-
- private receiveHidInputReport(event: HIDInputReportEvent) {
- const data = new Uint8Array(event.data.buffer);
- this._logger.debug("Received Frame", {
- data: { frame: data },
- tag: "apdu-receiver",
- });
- const response = this._apduReceiver.handleFrame(data);
- response.caseOf({
- Right: (maybeApduResponse) => {
- maybeApduResponse.map((apduResponse) => {
- this._logger.debug("Received APDU Response", {
- data: { response: apduResponse },
- });
- this._sendApduSubject.next(apduResponse);
- this._sendApduSubject.complete();
- });
- },
- Left: (err) => {
- this._sendApduSubject.error(err);
- },
- });
- }
-
- private setupWaitForReconnection(): Promise> {
- return new Promise>((resolve) => {
- this._settleReconnectionPromise = Maybe.of({
- resolve: () => resolve(Right(undefined)),
- reject: (error: SdkError) => resolve(Left(error)),
- });
- });
- }
-
- private reconnected() {
- this._settleReconnectionPromise.ifJust((promise) => {
- promise.resolve();
- this._settleReconnectionPromise = Maybe.zero();
- });
- }
-
- public disconnect() {
- this._settleReconnectionPromise.ifJust((promise) => {
- promise.reject(new ReconnectionFailedError());
- this._settleReconnectionPromise = Maybe.zero();
- });
- }
-}
diff --git a/packages/core/src/internal/usb/transport/UsbHidTransport.ts b/packages/core/src/internal/usb/transport/UsbHidTransport.ts
deleted file mode 100644
index dba438c28..000000000
--- a/packages/core/src/internal/usb/transport/UsbHidTransport.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Either } from "purify-ts";
-import { Observable } from "rxjs";
-
-import { DeviceId } from "@api/device/DeviceModel";
-import { SdkError } from "@api/Error";
-import { ConnectError } from "@internal/usb/model/Errors";
-import { InternalConnectedDevice } from "@internal/usb/model/InternalConnectedDevice";
-import { InternalDiscoveredDevice } from "@internal/usb/model/InternalDiscoveredDevice";
-
-export type DisconnectHandler = (deviceId: DeviceId) => void;
-
-/**
- * Transport interface representing a USB HID communication
- */
-export interface UsbHidTransport {
- isSupported(): boolean;
-
- startDiscovering(): Observable;
-
- stopDiscovering(): void;
-
- /**
- * Enables communication with the device by connecting to it.
- *
- * @param params containing
- * - id: the device id from the DTO discovered device
- */
- connect(params: {
- deviceId: DeviceId;
- onDisconnect: DisconnectHandler;
- }): Promise>;
-
- disconnect(params: {
- connectedDevice: InternalConnectedDevice;
- }): Promise>;
-}
diff --git a/packages/core/src/internal/usb/transport/WebUsbHidTransport.ts b/packages/core/src/internal/usb/transport/WebUsbHidTransport.ts
deleted file mode 100644
index 9e3a36f97..000000000
--- a/packages/core/src/internal/usb/transport/WebUsbHidTransport.ts
+++ /dev/null
@@ -1,476 +0,0 @@
-import * as Sentry from "@sentry/minimal";
-import { inject, injectable } from "inversify";
-import { Either, EitherAsync, Left, Maybe, Right } from "purify-ts";
-import { from, Observable, Subscription, switchMap, timer } from "rxjs";
-import { v4 as uuid } from "uuid";
-
-import { DeviceId } from "@api/device/DeviceModel";
-import { SdkError } from "@api/Error";
-import type { DeviceModelDataSource } from "@internal/device-model/data/DeviceModelDataSource";
-import { deviceModelTypes } from "@internal/device-model/di/deviceModelTypes";
-import { InternalDeviceModel } from "@internal/device-model/model/DeviceModel";
-import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes";
-import type { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
-import {
- LEDGER_VENDOR_ID,
- RECONNECT_DEVICE_TIMEOUT,
-} from "@internal/usb/data/UsbHidConfig";
-import { usbDiTypes } from "@internal/usb/di/usbDiTypes";
-import {
- ConnectError,
- DeviceNotRecognizedError,
- DisconnectError,
- NoAccessibleDeviceError,
- OpeningConnectionError,
- type PromptDeviceAccessError,
- UnknownDeviceError,
- UsbHidTransportNotSupportedError,
-} from "@internal/usb/model/Errors";
-import { InternalConnectedDevice } from "@internal/usb/model/InternalConnectedDevice";
-import { InternalDiscoveredDevice } from "@internal/usb/model/InternalDiscoveredDevice";
-import { UsbHidDeviceConnectionFactory } from "@internal/usb/service/UsbHidDeviceConnectionFactory";
-import { UsbHidDeviceConnection } from "@internal/usb/transport/UsbHidDeviceConnection";
-
-import { DisconnectHandler, UsbHidTransport } from "./UsbHidTransport";
-
-// An attempt to manage the state of several devices with one transport. Not final.
-type WebHidInternalDevice = {
- id: DeviceId;
- hidDevice: HIDDevice;
- discoveredDevice: InternalDiscoveredDevice;
-};
-
-@injectable()
-export class WebUsbHidTransport implements UsbHidTransport {
- // Maps uncoupled DiscoveredDevice and WebHID's HIDDevice WebHID
- private _internalDevicesById: Map;
- private _disconnectionHandlersByHidId: Map void>;
- private _deviceConnectionByHidId: Map;
- private _connectionListenersAbortController: AbortController;
- private _logger: LoggerPublisherService;
- private _usbHidDeviceConnectionFactory: UsbHidDeviceConnectionFactory;
- private _deviceDisconnectionTimerSubscription: Maybe =
- Maybe.zero();
-
- constructor(
- @inject(deviceModelTypes.DeviceModelDataSource)
- private deviceModelDataSource: DeviceModelDataSource,
- @inject(loggerTypes.LoggerPublisherServiceFactory)
- loggerServiceFactory: (tag: string) => LoggerPublisherService,
- @inject(usbDiTypes.UsbHidDeviceConnectionFactory)
- usbHidDeviceConnectionFactory: UsbHidDeviceConnectionFactory,
- ) {
- this._internalDevicesById = new Map();
- this._disconnectionHandlersByHidId = new Map();
- this._deviceConnectionByHidId = new Map();
- this._connectionListenersAbortController = new AbortController();
- this._logger = loggerServiceFactory("WebUsbHidTransport");
- this._usbHidDeviceConnectionFactory = usbHidDeviceConnectionFactory;
-
- this.hidApi.map((hidApi) => {
- hidApi.ondisconnect = (event) =>
- this.handleDeviceDisconnectionEvent(event);
- hidApi.onconnect = (event) => this.handleDeviceConnectionEvent(event);
- });
- }
-
- /**
- * Get the WebHID API if supported or error
- * @returns `Either`
- */
- private get hidApi(): Either {
- if (this.isSupported()) {
- return Right(navigator.hid);
- }
-
- return Left(new UsbHidTransportNotSupportedError("WebHID not supported"));
- }
-
- isSupported() {
- try {
- const result = !!navigator?.hid;
- this._logger.debug(`isSupported: ${result}`);
- return result;
- } catch (error) {
- this._logger.error(`isSupported: error`, { data: { error } });
- return false;
- }
- }
-
- /**
- * Currently: as there is no way to uniquely identify a device, we might need to always update the internal mapping
- * of devices when prompting for device access.
- *
- * Also, we cannot trust hidApi.getDevices() as 2 devices of the same models (even on the same USB port) will be recognized
- * as the same devices.
- */
- // private async promptDeviceAccess(): Promise> {
- private async promptDeviceAccess(): Promise<
- Either
- > {
- return EitherAsync.liftEither(this.hidApi)
- .map(async (hidApi) => {
- // `requestDevice` returns an array. but normally the user can select only one device at a time.
- let hidDevices: HIDDevice[] = [];
-
- try {
- hidDevices = await hidApi.requestDevice({
- filters: [{ vendorId: LEDGER_VENDOR_ID }],
- });
- } catch (error) {
- const deviceError = new NoAccessibleDeviceError(error);
- this._logger.error(`promptDeviceAccess: error requesting device`, {
- data: { error },
- });
- Sentry.captureException(deviceError);
- throw deviceError;
- }
-
- this._logger.debug(
- `promptDeviceAccess: hidDevices len ${hidDevices.length}`,
- );
-
- // Granted access to 0 device (by clicking on cancel for ex) results in an error
- if (hidDevices.length === 0) {
- this._logger.warn("No device was selected");
- throw new NoAccessibleDeviceError("No selected device");
- }
-
- const discoveredHidDevices: HIDDevice[] = [];
-
- for (const hidDevice of hidDevices) {
- discoveredHidDevices.push(hidDevice);
-
- this._logger.debug(`promptDeviceAccess: selected device`, {
- data: { hidDevice },
- });
- }
-
- return discoveredHidDevices;
- })
- .run();
- }
-
- /**
- * For WebHID, the client can only discover devices for which the user granted access to.
- *
- * The issue is that once a user grant access to a device of a model/productId A, any other model/productId A device will be accessible.
- * Even if plugged on another USB port.
- * So we cannot rely on the `hid.getDevices` to get the list of accessible devices, because it is not possible to differentiate
- * between 2 devices of the same model.
- * Neither on `connect` and `disconnect` events.
- * We can only rely on the `hid.requestDevice` because it is the user who will select the device that we can access.
- *
- * 2 possible implementations:
- * - only `hid.requestDevice` and return the one selected device
- * - `hid.getDevices` first to get the previously accessible devices, then a `hid.requestDevice` to get any new one
- *
- * [ASK] Should we also subscribe to hid events `connect` and `disconnect` ?
- *
- * [ASK] For the 2nd option: the DiscoveredDevice could have a `isSelected` property ?
- * So the consumer can directly select this device.
- */
- startDiscovering(): Observable {
- this._logger.debug("startDiscovering");
-
- // Logs the connection and disconnection events
- this.startListeningToConnectionEvents();
-
- // There is no unique identifier for the device from the USB/HID connection,
- // so the previously known accessible devices list cannot be trusted.
- this._internalDevicesById.clear();
-
- return from(this.promptDeviceAccess()).pipe(
- switchMap((either) => {
- return either.caseOf({
- Left: (error) => {
- this._logger.error("Error while getting accessible device", {
- data: { error },
- });
- Sentry.captureException(error);
- throw error;
- },
- Right: (hidDevices) => {
- this._logger.info(`Got access to ${hidDevices.length} HID devices`);
-
- const discoveredDevices = hidDevices.map((hidDevice) => {
- const maybeDeviceModel = this.getDeviceModel(hidDevice);
- return maybeDeviceModel.caseOf({
- Just: (deviceModel) => {
- const id = uuid();
-
- const discoveredDevice = {
- id,
- deviceModel,
- };
-
- const internalDevice: WebHidInternalDevice = {
- id,
- hidDevice,
- discoveredDevice,
- };
-
- this._logger.debug(
- `Discovered device ${id} ${discoveredDevice.deviceModel.productName}`,
- );
- this._internalDevicesById.set(id, internalDevice);
-
- return discoveredDevice;
- },
- Nothing: () => {
- // [ASK] Or we just ignore the not recognized device ? And log them
- this._logger.warn(
- `Device not recognized: hidDevice.productId: 0x${hidDevice.productId.toString(16)}`,
- );
- throw new DeviceNotRecognizedError(
- `Device not recognized: hidDevice.productId: 0x${hidDevice.productId.toString(16)}`,
- );
- },
- });
- });
- return from(discoveredDevices);
- },
- });
- }),
- );
- }
-
- stopDiscovering(): void {
- this._logger.debug("stopDiscovering");
-
- this.stopListeningToConnectionEvents();
- }
-
- /**
- * Logs `connect` and `disconnect` events for already accessible devices
- */
- private startListeningToConnectionEvents(): void {
- this._logger.debug("startListeningToConnectionEvents");
-
- this.hidApi.map((hidApi) => {
- hidApi.addEventListener(
- "connect",
- (event) => {
- this._logger.debug("connection event", { data: { event } });
- },
- { signal: this._connectionListenersAbortController.signal },
- );
-
- hidApi.addEventListener(
- "disconnect",
- (event) => {
- this._logger.debug("disconnect event", { data: { event } });
- },
- { signal: this._connectionListenersAbortController.signal },
- );
- });
- }
-
- private stopListeningToConnectionEvents(): void {
- this._logger.debug("stopListeningToConnectionEvents");
- this._connectionListenersAbortController.abort();
- }
-
- /**
- * Connect to a HID USB device and update the internal state of the associated device
- */
- async connect({
- deviceId,
- onDisconnect,
- }: {
- deviceId: DeviceId;
- onDisconnect: DisconnectHandler;
- }): Promise> {
- this._logger.debug("connect", { data: { deviceId } });
-
- const internalDevice = this._internalDevicesById.get(deviceId);
-
- if (!internalDevice) {
- this._logger.error(`Unknown device ${deviceId}`);
- return Left(new UnknownDeviceError(`Unknown device ${deviceId}`));
- }
-
- try {
- await internalDevice.hidDevice.open();
- } catch (error) {
- if (error instanceof DOMException && error.name === "InvalidStateError") {
- this._logger.debug(`Device ${deviceId} is already opened`);
- } else {
- const connectionError = new OpeningConnectionError(error);
- this._logger.debug(`Error while opening device: ${deviceId}`, {
- data: { error },
- });
- Sentry.captureException(connectionError);
- return Left(connectionError);
- }
- }
-
- const {
- discoveredDevice: { deviceModel },
- } = internalDevice;
-
- const deviceConnection = this._usbHidDeviceConnectionFactory.create(
- internalDevice.hidDevice,
- );
- this._deviceConnectionByHidId.set(
- this.getHidUsbProductId(internalDevice.hidDevice),
- deviceConnection,
- );
- const connectedDevice = new InternalConnectedDevice({
- sendApdu: (apdu, triggersDisconnection) =>
- deviceConnection.sendApdu(apdu, triggersDisconnection),
- deviceModel,
- id: deviceId,
- type: "USB",
- });
- this._disconnectionHandlersByHidId.set(
- this.getHidUsbProductId(internalDevice.hidDevice),
- () => {
- this.disconnect({ connectedDevice }).then(() => onDisconnect(deviceId));
- },
- );
- return Right(connectedDevice);
- }
-
- private getDeviceModel(hidDevice: HIDDevice): Maybe {
- const { productId } = hidDevice;
- const matchingModel = this.deviceModelDataSource.getAllDeviceModels().find(
- (deviceModel) =>
- // outside of bootloader mode, the value that we need to identify a device model is the first byte of the actual hidDevice.productId
- deviceModel.usbProductId === productId >> 8 ||
- deviceModel.bootloaderUsbProductId === productId,
- );
- return matchingModel ? Maybe.of(matchingModel) : Maybe.zero();
- }
-
- private getHidUsbProductId(hidDevice: HIDDevice): number {
- return this.getDeviceModel(hidDevice).caseOf({
- Just: (deviceModel) => deviceModel.usbProductId,
- Nothing: () => hidDevice.productId >> 8,
- });
- }
-
- /**
- * Disconnect from a HID USB device and delete its handlers
- */
- async disconnect(params: {
- connectedDevice: InternalConnectedDevice;
- }): Promise> {
- this._logger.debug("disconnect", { data: { connectedDevice: params } });
- const internalDevice = this._internalDevicesById.get(
- params.connectedDevice.id,
- );
-
- if (!internalDevice) {
- this._logger.error(`Unknown device ${params.connectedDevice.id}`);
- return Left(
- new UnknownDeviceError(`Unknown device ${params.connectedDevice.id}`),
- );
- }
-
- const deviceConnection = this._deviceConnectionByHidId.get(
- this.getHidUsbProductId(internalDevice.hidDevice),
- );
-
- deviceConnection?.disconnect();
-
- try {
- const usbProductId = this.getHidUsbProductId(internalDevice.hidDevice);
- this._internalDevicesById.delete(internalDevice.id);
- this._disconnectionHandlersByHidId.delete(usbProductId);
- this._deviceConnectionByHidId.delete(usbProductId);
- await internalDevice.hidDevice.close();
- return Right(void 0);
- } catch (error) {
- return Left(new DisconnectError(error));
- }
- }
-
- /**
- * Type guard to check if the event is a HID connection event
- * @param event
- * @private
- */
- private isHIDConnectionEvent(event: Event): event is HIDConnectionEvent {
- return (
- "device" in event &&
- typeof event.device === "object" &&
- event.device !== null &&
- "productId" in event.device &&
- typeof event.device.productId === "number"
- );
- }
-
- private _handleDisconnection(
- device: HIDDevice,
- callback: (handler: () => void) => void,
- ) {
- const usbProductId = this.getHidUsbProductId(device);
- const maybeDisconnectHandler = Maybe.fromNullable(
- this._disconnectionHandlersByHidId.get(usbProductId),
- );
-
- maybeDisconnectHandler.map(callback);
- }
-
- /**
- * Handle the disconnection event of a HID device
- * @param event
- */
- private handleDeviceDisconnectionEvent(event: Event) {
- if (!this.isHIDConnectionEvent(event)) {
- this._logger.error("Invalid event", { data: { event } });
- return;
- }
-
- this._handleDisconnection(event.device, (handler) => {
- // We start a timer to disconnect the device if the device has a disconnect handler (ie is already connected)
- this._logger.debug(`Start delay of ${RECONNECT_DEVICE_TIMEOUT}ms`);
- this._deviceDisconnectionTimerSubscription = Maybe.of(
- timer(RECONNECT_DEVICE_TIMEOUT).subscribe(() => {
- this._logger.debug("Disconnecting device");
-
- handler();
- this._deviceDisconnectionTimerSubscription = Maybe.zero();
- }),
- );
- });
- }
-
- /**
- * Handle the connection event of a HID device
- * @param event
- */
- private handleDeviceConnectionEvent(event: Event) {
- if (!this.isHIDConnectionEvent(event)) {
- this._logger.error("Invalid event", { data: { event } });
- return;
- }
-
- // If a disconnection blocking timer is running, we stop it and reconnect
- try {
- this._deviceDisconnectionTimerSubscription.map(async (timerSub) => {
- timerSub.unsubscribe();
- const maybeDeviceConnection = Maybe.fromNullable(
- this._deviceConnectionByHidId.get(
- this.getHidUsbProductId(event.device),
- ),
- );
- await event.device.open();
- maybeDeviceConnection.map(
- (dConnection) => (dConnection.device = event.device),
- );
- });
- } catch (error) {
- this._logger.error("Error while reconnecting to device", {
- data: { event, error },
- });
- this._handleDisconnection(event.device, (disconnectHandler) => {
- disconnectHandler();
- this._logger.debug("Disconnecting device", {
- data: { device: event.device },
- });
- });
- }
- }
-}
diff --git a/packages/core/tsconfig.cjs.json b/packages/core/tsconfig.cjs.json
deleted file mode 100644
index 63c80c465..000000000
--- a/packages/core/tsconfig.cjs.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "extends": "./tsconfig.json",
- "exclude": [
- "src/**/*.test.ts",
- "src/**/*.stub.ts",
- "src/**/__mocks__",
- "src/**/__test-utils__",
- "jest.*.ts"
- ],
- "compilerOptions": {
- "module": "nodenext",
- "moduleResolution": "nodenext",
- "outDir": "./lib/cjs",
- "declarationDir": "./lib/cjs"
- }
-}
diff --git a/packages/core/tsconfig.eslint.json b/packages/core/tsconfig.eslint.json
deleted file mode 100644
index db99aa7a0..000000000
--- a/packages/core/tsconfig.eslint.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "extends": "./tsconfig.json",
- "include": ["src", "index.ts", "jest.*.ts", "scripts", "eslint.config.mjs"]
-}
diff --git a/packages/core/tsconfig.esm.json b/packages/core/tsconfig.esm.json
deleted file mode 100644
index 7918341c9..000000000
--- a/packages/core/tsconfig.esm.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "extends": "./tsconfig.json",
- "exclude": [
- "src/**/*.test.ts",
- "src/**/*.stub.ts",
- "src/**/__mocks__",
- "src/**/__test-utils__",
- "jest.*.ts"
- ],
- "compilerOptions": {
- "module": "esnext",
- "outDir": "./lib/esm",
- "declarationDir": "./lib/esm"
- }
-}
diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json
deleted file mode 100644
index 915c92b8b..000000000
--- a/packages/core/tsconfig.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "extends": "@ledgerhq/tsconfig-dsdk/sdk",
- "compilerOptions": {
- "baseUrl": ".",
- "paths": {
- "@api/*": ["src/api/*"],
- "@internal/*": ["src/internal/*"],
- "@root/*": ["./*"]
- },
- "experimentalDecorators": true,
- "resolveJsonModule": true
- },
- "include": ["src", "index.ts", "jest.*.ts"]
-}
diff --git a/packages/core/.prettierignore b/packages/device-management-kit/.prettierignore
similarity index 100%
rename from packages/core/.prettierignore
rename to packages/device-management-kit/.prettierignore
diff --git a/packages/signer/keyring-btc/.prettierrc.js b/packages/device-management-kit/.prettierrc.js
similarity index 100%
rename from packages/signer/keyring-btc/.prettierrc.js
rename to packages/device-management-kit/.prettierrc.js
diff --git a/packages/core/CHANGELOG.md b/packages/device-management-kit/CHANGELOG.md
similarity index 72%
rename from packages/core/CHANGELOG.md
rename to packages/device-management-kit/CHANGELOG.md
index b81da84eb..b600b0678 100644
--- a/packages/core/CHANGELOG.md
+++ b/packages/device-management-kit/CHANGELOG.md
@@ -1,5 +1,38 @@
# @ledgerhq/device-management-kit
+## 0.5.0
+
+### Minor Changes
+
+- [#221](https://github.com/LedgerHQ/device-sdk-ts/pull/221) [`55d62f2`](https://github.com/LedgerHQ/device-sdk-ts/commit/55d62f2dfe9cd979c99fbc8f8aeed7909c653807) Thanks [@jdabbech-ledger](https://github.com/jdabbech-ledger)! - Add BLE support
+
+### Patch Changes
+
+- [#438](https://github.com/LedgerHQ/device-sdk-ts/pull/438) [`d6273ed`](https://github.com/LedgerHQ/device-sdk-ts/commit/d6273ed00b61d273ebc42bd5dfa16ce4c5641af5) Thanks [@valpinkman](https://github.com/valpinkman)! - Update license to Apache-2.0
+
+- [#321](https://github.com/LedgerHQ/device-sdk-ts/pull/321) [`123bec8`](https://github.com/LedgerHQ/device-sdk-ts/commit/123bec87ebd6c23922138c44a397bc72919d88e5) Thanks [@valpinkman](https://github.com/valpinkman)! - Use esbuild to build libraries
+
+- [#477](https://github.com/LedgerHQ/device-sdk-ts/pull/477) [`64e8886`](https://github.com/LedgerHQ/device-sdk-ts/commit/64e88863fd93c7140c32be5c91fde231293be7be) Thanks [@jdabbech-ledger](https://github.com/jdabbech-ledger)! - Replace ListDeviceSessionsUseCase with ListConnectedDevicesUseCase
+
+- [#460](https://github.com/LedgerHQ/device-sdk-ts/pull/460) [`a99fe1b`](https://github.com/LedgerHQ/device-sdk-ts/commit/a99fe1bfd362b6b5f9e8ee2489d285766e06272a) Thanks [@jdabbech-ledger](https://github.com/jdabbech-ledger)! - Rename SDK to DMK
+
+- [`5085f6d`](https://github.com/LedgerHQ/device-sdk-ts/commit/5085f6dd397b5800849e34f593e71fd9c61c0e40) Thanks [@valpinkman](https://github.com/valpinkman)! - Implement basic Flipper client for the Ledger Device Management Kit
+
+- [#487](https://github.com/LedgerHQ/device-sdk-ts/pull/487) [`afaeb64`](https://github.com/LedgerHQ/device-sdk-ts/commit/afaeb64c1fd2643d74ea8a2cc541c450d78c470c) Thanks [@jdabbech-ledger](https://github.com/jdabbech-ledger)! - Add support for nanoS
+
+- [`5085f6d`](https://github.com/LedgerHQ/device-sdk-ts/commit/5085f6dd397b5800849e34f593e71fd9c61c0e40) Thanks [@valpinkman](https://github.com/valpinkman)! - Add mockserver integration with transport
+
+- [#452](https://github.com/LedgerHQ/device-sdk-ts/pull/452) [`9c2daf9`](https://github.com/LedgerHQ/device-sdk-ts/commit/9c2daf90391d5219cfa0f98e500a6f2e1295b454) Thanks [@aussedatlo](https://github.com/aussedatlo)! - Use type keyword when importing type
+
+- [`5085f6d`](https://github.com/LedgerHQ/device-sdk-ts/commit/5085f6dd397b5800849e34f593e71fd9c61c0e40) Thanks [@valpinkman](https://github.com/valpinkman)! - Add unlock timeout input in open app device action
+
+- [#357](https://github.com/LedgerHQ/device-sdk-ts/pull/357) [`629900d`](https://github.com/LedgerHQ/device-sdk-ts/commit/629900d681acdc4398445d4167a70811d041dad4) Thanks [@ofreyssinet-ledger](https://github.com/ofreyssinet-ledger)! - WebHid: Rework reconnection logic & fix sendApdu to wait in case of disconnection
+
+- [#392](https://github.com/LedgerHQ/device-sdk-ts/pull/392) [`bd19f5c`](https://github.com/LedgerHQ/device-sdk-ts/commit/bd19f5c27f5a74dc9d58bd25fb021a260ff5e602) Thanks [@ofreyssinet-ledger](https://github.com/ofreyssinet-ledger)! - New use case listenToKnownDevices
+
+- Updated dependencies [[`d6273ed`](https://github.com/LedgerHQ/device-sdk-ts/commit/d6273ed00b61d273ebc42bd5dfa16ce4c5641af5), [`a99fe1b`](https://github.com/LedgerHQ/device-sdk-ts/commit/a99fe1bfd362b6b5f9e8ee2489d285766e06272a), [`5085f6d`](https://github.com/LedgerHQ/device-sdk-ts/commit/5085f6dd397b5800849e34f593e71fd9c61c0e40)]:
+ - @ledgerhq/device-transport-kit-mock-client@1.0.1
+
## 0.4.0
### Minor Changes
diff --git a/packages/core/README.md b/packages/device-management-kit/README.md
similarity index 100%
rename from packages/core/README.md
rename to packages/device-management-kit/README.md
diff --git a/packages/core/SCRIPTS.MD b/packages/device-management-kit/SCRIPTS.MD
similarity index 100%
rename from packages/core/SCRIPTS.MD
rename to packages/device-management-kit/SCRIPTS.MD
diff --git a/packages/core/_templates/core-module/with-prompt/datasource.ejs.t b/packages/device-management-kit/_templates/core-module/with-prompt/datasource.ejs.t
similarity index 100%
rename from packages/core/_templates/core-module/with-prompt/datasource.ejs.t
rename to packages/device-management-kit/_templates/core-module/with-prompt/datasource.ejs.t
diff --git a/packages/core/_templates/core-module/with-prompt/default-service.ejs.t b/packages/device-management-kit/_templates/core-module/with-prompt/default-service.ejs.t
similarity index 100%
rename from packages/core/_templates/core-module/with-prompt/default-service.ejs.t
rename to packages/device-management-kit/_templates/core-module/with-prompt/default-service.ejs.t
diff --git a/packages/core/_templates/core-module/with-prompt/default-service.test.ejs.t b/packages/device-management-kit/_templates/core-module/with-prompt/default-service.test.ejs.t
similarity index 100%
rename from packages/core/_templates/core-module/with-prompt/default-service.test.ejs.t
rename to packages/device-management-kit/_templates/core-module/with-prompt/default-service.test.ejs.t
diff --git a/packages/core/_templates/core-module/with-prompt/di-module.ejs.t b/packages/device-management-kit/_templates/core-module/with-prompt/di-module.ejs.t
similarity index 100%
rename from packages/core/_templates/core-module/with-prompt/di-module.ejs.t
rename to packages/device-management-kit/_templates/core-module/with-prompt/di-module.ejs.t
diff --git a/packages/core/_templates/core-module/with-prompt/di-module.test.ejs.t b/packages/device-management-kit/_templates/core-module/with-prompt/di-module.test.ejs.t
similarity index 100%
rename from packages/core/_templates/core-module/with-prompt/di-module.test.ejs.t
rename to packages/device-management-kit/_templates/core-module/with-prompt/di-module.test.ejs.t
diff --git a/packages/core/_templates/core-module/with-prompt/di-types.ejs.t b/packages/device-management-kit/_templates/core-module/with-prompt/di-types.ejs.t
similarity index 100%
rename from packages/core/_templates/core-module/with-prompt/di-types.ejs.t
rename to packages/device-management-kit/_templates/core-module/with-prompt/di-types.ejs.t
diff --git a/packages/core/_templates/core-module/with-prompt/gitkeep-model.ejs.t b/packages/device-management-kit/_templates/core-module/with-prompt/gitkeep-model.ejs.t
similarity index 100%
rename from packages/core/_templates/core-module/with-prompt/gitkeep-model.ejs.t
rename to packages/device-management-kit/_templates/core-module/with-prompt/gitkeep-model.ejs.t
diff --git a/packages/core/_templates/core-module/with-prompt/gitkeep-usecase.ejs.t b/packages/device-management-kit/_templates/core-module/with-prompt/gitkeep-usecase.ejs.t
similarity index 100%
rename from packages/core/_templates/core-module/with-prompt/gitkeep-usecase.ejs.t
rename to packages/device-management-kit/_templates/core-module/with-prompt/gitkeep-usecase.ejs.t
diff --git a/packages/core/_templates/core-module/with-prompt/prompt.js b/packages/device-management-kit/_templates/core-module/with-prompt/prompt.js
similarity index 100%
rename from packages/core/_templates/core-module/with-prompt/prompt.js
rename to packages/device-management-kit/_templates/core-module/with-prompt/prompt.js
diff --git a/packages/core/_templates/core-module/with-prompt/service.ejs.t b/packages/device-management-kit/_templates/core-module/with-prompt/service.ejs.t
similarity index 100%
rename from packages/core/_templates/core-module/with-prompt/service.ejs.t
rename to packages/device-management-kit/_templates/core-module/with-prompt/service.ejs.t
diff --git a/packages/core/doc/DeviceActionStateGraph.md b/packages/device-management-kit/doc/DeviceActionStateGraph.md
similarity index 100%
rename from packages/core/doc/DeviceActionStateGraph.md
rename to packages/device-management-kit/doc/DeviceActionStateGraph.md
diff --git a/packages/device-management-kit/eslint.config.mjs b/packages/device-management-kit/eslint.config.mjs
new file mode 100644
index 000000000..5d90c9c2e
--- /dev/null
+++ b/packages/device-management-kit/eslint.config.mjs
@@ -0,0 +1,13 @@
+import config from "@ledgerhq/eslint-config-dsdk";
+
+export default [
+ ...config,
+ {
+ ignores: ["eslint.config.mjs", "scripts/*.mjs"],
+ languageOptions: {
+ parserOptions: {
+ project: "./tsconfig.json",
+ },
+ },
+ },
+];
diff --git a/packages/core/index.ts b/packages/device-management-kit/index.ts
similarity index 100%
rename from packages/core/index.ts
rename to packages/device-management-kit/index.ts
diff --git a/packages/core/jest.config.ts b/packages/device-management-kit/jest.config.ts
similarity index 79%
rename from packages/core/jest.config.ts
rename to packages/device-management-kit/jest.config.ts
index 16fcf65c3..74ed7a246 100644
--- a/packages/core/jest.config.ts
+++ b/packages/device-management-kit/jest.config.ts
@@ -1,5 +1,5 @@
/* eslint no-restricted-syntax: 0 */
-import { JestConfigWithTsJest, pathsToModuleNameMapper } from "ts-jest";
+import { type JestConfigWithTsJest, pathsToModuleNameMapper } from "ts-jest";
import { compilerOptions } from "./tsconfig.json";
@@ -11,6 +11,7 @@ const config: JestConfigWithTsJest = {
preset: "@ledgerhq/jest-config-dsdk",
setupFiles: ["/jest.setup.ts"],
testPathIgnorePatterns: ["/lib/esm", "/lib/cjs"],
+ modulePathIgnorePatterns: ["/lib/esm", "/lib/cjs"],
collectCoverageFrom: [
"src/**/*.ts",
"!src/**/*.stub.ts",
diff --git a/packages/core/jest.setup.ts b/packages/device-management-kit/jest.setup.ts
similarity index 100%
rename from packages/core/jest.setup.ts
rename to packages/device-management-kit/jest.setup.ts
diff --git a/packages/core/package.json b/packages/device-management-kit/package.json
similarity index 61%
rename from packages/core/package.json
rename to packages/device-management-kit/package.json
index bed24a6e3..ff8d999ec 100644
--- a/packages/core/package.json
+++ b/packages/device-management-kit/package.json
@@ -1,35 +1,29 @@
{
"name": "@ledgerhq/device-management-kit",
- "version": "0.4.0",
- "license": "MIT",
+ "version": "0.5.0",
+ "license": "Apache-2.0",
"exports": {
".": {
+ "types": "./lib/types/index.d.ts",
"import": "./lib/esm/index.js",
- "require": "./lib/cjs/index.js",
- "default": "./index.ts",
- "types": "./lib/esm/index.d.ts"
- },
- "./*.js": {
- "import": "./lib/esm/*.js",
- "require": "./lib/cjs/*.js",
- "default": "./*.js",
- "types": "./lib/esm/*.d.ts"
+ "require": "./lib/cjs/index.js"
},
"./*": {
+ "types": "./lib/types/*",
"import": "./lib/esm/*",
- "require": "./lib/cjs/*",
- "default": "./*",
- "types": "./lib/esm/*"
+ "require": "./lib/cjs/*"
}
},
"files": [
- "./lib"
+ "./lib",
+ "package.json"
],
"scripts": {
- "build": "rimraf lib && zx scripts/build.mjs",
- "dev": "concurrently \"pnpm dev:esm\" \"pnpm dev:cjs\"",
- "dev:esm": "concurrently \"tsc --watch -p tsconfig.esm.json\" \"tsc-alias --watch -p tsconfig.esm.json\"",
- "dev:cjs": "concurrently \"tsc --watch -p tsconfig.cjs.json\" \"tsc-alias --watch -p tsconfig.cjs.json\"",
+ "prebuild": "rimraf lib",
+ "build": "pnpm lmdk-build --entryPoints index.ts,src/**/*.ts --tsconfig tsconfig.prod.json",
+ "dev": "concurrently \"pnpm watch:builds\" \"pnpm watch:types\"",
+ "watch:builds": "pnpm lmdk-watch --entryPoints index.ts,src/**/*.ts --tsconfig tsconfig.prod.json",
+ "watch:types": "concurrently \"tsc --watch -p tsconfig.prod.json\" \"tsc-alias --watch -p tsconfig.prod.json\"",
"lint": "eslint",
"lint:fix": "pnpm lint --fix",
"postpack": "find . -name '*.tgz' -exec cp {} ../../dist/ \\; ",
@@ -42,10 +36,11 @@
"module:create": "pnpm hygen core-module with-prompt"
},
"dependencies": {
+ "@ledgerhq/device-transport-kit-mock-client": "workspace:*",
"@sentry/minimal": "^6.19.7",
"@statelyai/inspect": "^0.4.0",
"axios": "^1.7.7",
- "inversify": "^6.0.2",
+ "inversify": "^6.0.3",
"inversify-logger-middleware": "^3.1.0",
"purify-ts": "^2.1.0",
"reflect-metadata": "^0.2.2",
@@ -55,6 +50,7 @@
"xstate": "^5.18.2"
},
"devDependencies": {
+ "@ledgerhq/esbuild-tools": "workspace:*",
"@ledgerhq/eslint-config-dsdk": "workspace:*",
"@ledgerhq/jest-config-dsdk": "workspace:*",
"@ledgerhq/prettier-config-dsdk": "workspace:*",
@@ -62,6 +58,7 @@
"@types/semver": "^7.5.8",
"@types/uuid": "^10.0.0",
"@types/w3c-web-hid": "^1.0.6",
+ "@types/web-bluetooth": "^0.0.20",
"ts-node": "^10.9.2"
}
}
diff --git a/packages/core/scripts/add-module.mjs b/packages/device-management-kit/scripts/add-module.mjs
similarity index 100%
rename from packages/core/scripts/add-module.mjs
rename to packages/device-management-kit/scripts/add-module.mjs
diff --git a/packages/core/src/api/DeviceSdk.test.ts b/packages/device-management-kit/src/api/DeviceManagementKit.test.ts
similarity index 55%
rename from packages/core/src/api/DeviceSdk.test.ts
rename to packages/device-management-kit/src/api/DeviceManagementKit.test.ts
index 6ddb8fde7..33c70a240 100644
--- a/packages/core/src/api/DeviceSdk.test.ts
+++ b/packages/device-management-kit/src/api/DeviceManagementKit.test.ts
@@ -1,98 +1,105 @@
-import { LocalConfigDataSource } from "@internal/config/data/ConfigDataSource";
+import { type interfaces } from "inversify";
+
+import { type LocalConfigDataSource } from "@internal/config/data/ConfigDataSource";
import { StubLocalConfigDataSource } from "@internal/config/data/LocalConfigDataSource.stub";
import { configTypes } from "@internal/config/di/configTypes";
import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes";
import { discoveryTypes } from "@internal/discovery/di/discoveryTypes";
import { sendTypes } from "@internal/send/di/sendTypes";
-import { usbDiTypes } from "@internal/usb/di/usbDiTypes";
import pkg from "@root/package.json";
import { StubUseCase } from "@root/src/di.stub";
import { commandTypes } from "./command/di/commandTypes";
import { ConsoleLogger } from "./logger-subscriber/service/ConsoleLogger";
-import { DeviceSdk } from "./DeviceSdk";
+import { DeviceManagementKit } from "./DeviceManagementKit";
jest.mock("./logger-subscriber/service/ConsoleLogger");
-let sdk: DeviceSdk;
+let dmk: DeviceManagementKit;
let logger: ConsoleLogger;
-describe("DeviceSdk", () => {
+describe("DeviceManagementKit", () => {
describe("clean", () => {
beforeEach(() => {
logger = new ConsoleLogger();
- sdk = new DeviceSdk({
+ dmk = new DeviceManagementKit({
stub: false,
loggers: [logger],
config: {
managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
},
});
});
it("should create an instance", () => {
- expect(sdk).toBeDefined();
- expect(sdk).toBeInstanceOf(DeviceSdk);
+ expect(dmk).toBeDefined();
+ expect(dmk).toBeInstanceOf(DeviceManagementKit);
});
it("should return a clean `version`", async () => {
- expect(await sdk.getVersion()).toBe(pkg.version);
+ expect(await dmk.getVersion()).toBe(pkg.version);
});
it("should have startDiscovery method", () => {
- expect(sdk.startDiscovering).toBeDefined();
+ expect(dmk.startDiscovering).toBeDefined();
});
it("should have stopDiscovery method", () => {
- expect(sdk.stopDiscovering).toBeDefined();
+ expect(dmk.stopDiscovering).toBeDefined();
});
it("should have connect method", () => {
- expect(sdk.connect).toBeDefined();
+ expect(dmk.connect).toBeDefined();
});
it("should have sendApdu method", () => {
- expect(sdk.sendApdu).toBeDefined();
+ expect(dmk.sendApdu).toBeDefined();
});
it("should have getConnectedDevice method", () => {
- expect(sdk.getConnectedDevice).toBeDefined();
+ expect(dmk.getConnectedDevice).toBeDefined();
});
it("should have sendCommand method", () => {
- expect(sdk.sendCommand).toBeDefined();
+ expect(dmk.sendCommand).toBeDefined();
+ });
+
+ it("should have listConnectedDevices method", () => {
+ expect(dmk.listConnectedDevices).toBeDefined();
});
- it("should have listDeviceSessions method", () => {
- expect(sdk.listDeviceSessions).toBeDefined();
+ it("should have listenToConnectedDevice method", () => {
+ expect(dmk.listenToConnectedDevice).toBeDefined();
});
});
describe("stubbed", () => {
beforeEach(() => {
- sdk = new DeviceSdk({
+ dmk = new DeviceManagementKit({
stub: true,
loggers: [],
config: {
managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
},
});
});
- it("should create a stubbed sdk", () => {
- expect(sdk).toBeDefined();
- expect(sdk).toBeInstanceOf(DeviceSdk);
+ it("should create a stubbed dmk", () => {
+ expect(dmk).toBeDefined();
+ expect(dmk).toBeInstanceOf(DeviceManagementKit);
});
it("should return a stubbed config", () => {
expect(
- sdk.container.get(
+ dmk.container.get(
configTypes.LocalConfigDataSource,
),
).toBeInstanceOf(StubLocalConfigDataSource);
});
it("should return a stubbed version", async () => {
- expect(await sdk.getVersion()).toBe("0.0.0-stub.1");
+ expect(await dmk.getVersion()).toBe("0.0.0-stub.1");
});
it.each([
@@ -101,14 +108,18 @@ describe("DeviceSdk", () => {
[discoveryTypes.ConnectUseCase],
[sendTypes.SendApduUseCase],
[commandTypes.SendCommandUseCase],
- [usbDiTypes.GetConnectedDeviceUseCase],
+ [discoveryTypes.GetConnectedDeviceUseCase],
[discoveryTypes.DisconnectUseCase],
[deviceSessionTypes.GetDeviceSessionStateUseCase],
- [deviceSessionTypes.ListDeviceSessionsUseCase],
- ])("should have %p use case", (diSymbol) => {
- const uc = sdk.container.get(diSymbol);
- expect(uc).toBeInstanceOf(StubUseCase);
- expect(uc.execute()).toBe("stub");
- });
+ [discoveryTypes.ListConnectedDevicesUseCase],
+ [discoveryTypes.ListenToConnectedDeviceUseCase],
+ ])(
+ "should have %p use case",
+ (diSymbol: interfaces.ServiceIdentifier) => {
+ const uc = dmk.container.get(diSymbol);
+ expect(uc).toBeInstanceOf(StubUseCase);
+ expect(uc.execute()).toBe("stub");
+ },
+ );
});
});
diff --git a/packages/core/src/api/DeviceSdk.ts b/packages/device-management-kit/src/api/DeviceManagementKit.ts
similarity index 54%
rename from packages/core/src/api/DeviceSdk.ts
rename to packages/device-management-kit/src/api/DeviceManagementKit.ts
index bc623d6c1..8d308ec54 100644
--- a/packages/core/src/api/DeviceSdk.ts
+++ b/packages/device-management-kit/src/api/DeviceManagementKit.ts
@@ -1,66 +1,78 @@
-import { Container } from "inversify";
-import { Observable } from "rxjs";
+import { type Container } from "inversify";
+import { type Observable } from "rxjs";
import { commandTypes } from "@api/command/di/commandTypes";
-import { CommandResult } from "@api/command/model/CommandResult";
+import { type CommandResult } from "@api/command/model/CommandResult";
import {
- SendCommandUseCase,
- SendCommandUseCaseArgs,
+ type SendCommandUseCase,
+ type SendCommandUseCaseArgs,
} from "@api/command/use-case/SendCommandUseCase";
import {
- ExecuteDeviceActionUseCase,
- ExecuteDeviceActionUseCaseArgs,
+ type ExecuteDeviceActionUseCase,
+ type ExecuteDeviceActionUseCaseArgs,
} from "@api/device-action/use-case/ExecuteDeviceActionUseCase";
-import { ApduResponse } from "@api/device-session/ApduResponse";
-import { DeviceSessionState } from "@api/device-session/DeviceSessionState";
-import { DeviceSessionId } from "@api/device-session/types";
+import { type ApduResponse } from "@api/device-session/ApduResponse";
+import { type DeviceSessionState } from "@api/device-session/DeviceSessionState";
+import { type DeviceSessionId } from "@api/device-session/types";
+import { type ConnectedDevice } from "@api/transport/model/ConnectedDevice";
import {
- ConnectUseCaseArgs,
- DisconnectUseCaseArgs,
- DiscoveredDevice,
- SendApduUseCaseArgs,
+ type ConnectUseCaseArgs,
+ type DisconnectUseCaseArgs,
+ type DiscoveredDevice,
+ type GetConnectedDeviceUseCaseArgs,
+ type SendApduUseCaseArgs,
+ type StartDiscoveringUseCaseArgs,
} from "@api/types";
-import { ConnectedDevice } from "@api/usb/model/ConnectedDevice";
import { configTypes } from "@internal/config/di/configTypes";
-import { GetSdkVersionUseCase } from "@internal/config/use-case/GetSdkVersionUseCase";
+import { type GetDmkVersionUseCase } from "@internal/config/use-case/GetDmkVersionUseCase";
import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes";
-import { DeviceSession } from "@internal/device-session/model/DeviceSession";
-import { GetDeviceSessionStateUseCase } from "@internal/device-session/use-case/GetDeviceSessionStateUseCase";
-import { ListDeviceSessionsUseCase } from "@internal/device-session/use-case/ListDeviceSessionsUseCase";
+import { type CloseSessionsUseCase } from "@internal/device-session/use-case/CloseSessionsUseCase";
+import { type GetDeviceSessionStateUseCase } from "@internal/device-session/use-case/GetDeviceSessionStateUseCase";
import { discoveryTypes } from "@internal/discovery/di/discoveryTypes";
-import { ConnectUseCase } from "@internal/discovery/use-case/ConnectUseCase";
-import { DisconnectUseCase } from "@internal/discovery/use-case/DisconnectUseCase";
+import { type ConnectUseCase } from "@internal/discovery/use-case/ConnectUseCase";
+import { type DisconnectUseCase } from "@internal/discovery/use-case/DisconnectUseCase";
+import { type GetConnectedDeviceUseCase } from "@internal/discovery/use-case/GetConnectedDeviceUseCase";
+import { type ListConnectedDevicesUseCase } from "@internal/discovery/use-case/ListConnectedDevicesUseCase";
+import { type ListenToConnectedDeviceUseCase } from "@internal/discovery/use-case/ListenToConnectedDeviceUseCase";
+import { type ListenToKnownDevicesUseCase } from "@internal/discovery/use-case/ListenToKnownDevicesUseCase";
import type { StartDiscoveringUseCase } from "@internal/discovery/use-case/StartDiscoveringUseCase";
import type { StopDiscoveringUseCase } from "@internal/discovery/use-case/StopDiscoveringUseCase";
import { sendTypes } from "@internal/send/di/sendTypes";
-import { SendApduUseCase } from "@internal/send/use-case/SendApduUseCase";
-import { usbDiTypes } from "@internal/usb/di/usbDiTypes";
-import {
- GetConnectedDeviceUseCase,
- GetConnectedDeviceUseCaseArgs,
-} from "@internal/usb/use-case/GetConnectedDeviceUseCase";
-import { makeContainer, MakeContainerProps } from "@root/src/di";
+import { type SendApduUseCase } from "@internal/send/use-case/SendApduUseCase";
+import { makeContainer, type MakeContainerProps } from "@root/src/di";
import {
- DeviceActionIntermediateValue,
- ExecuteDeviceActionReturnType,
+ type DeviceActionIntermediateValue,
+ type ExecuteDeviceActionReturnType,
} from "./device-action/DeviceAction";
import { deviceActionTypes } from "./device-action/di/deviceActionTypes";
-import { SdkError } from "./Error";
+import { type DmkError } from "./Error";
/**
- * The main class to interact with the SDK.
+ * The main class to interact with the Device Management Kit.
*
- * NB: do not instantiate this class directly, instead, use `DeviceSdkBuilder`.
+ * NB: do not instantiate this class directly, instead, use `LedgerDMKBuilder`.
*/
-export class DeviceSdk {
+export class DeviceManagementKit {
readonly container: Container;
/** @internal */
- constructor({ stub, loggers, config }: MakeContainerProps) {
+ constructor({
+ stub,
+ transports,
+ customTransports,
+ loggers,
+ config,
+ }: Partial = {}) {
// NOTE: MakeContainerProps might not be the exact type here
// For the init of the project this is sufficient, but we might need to
// update the constructor arguments as we go (we might have more than just the container config)
- this.container = makeContainer({ stub, loggers, config });
+ this.container = makeContainer({
+ stub,
+ transports,
+ customTransports,
+ loggers,
+ config,
+ });
}
/**
@@ -68,26 +80,29 @@ export class DeviceSdk {
*/
getVersion(): Promise {
return this.container
- .get(configTypes.GetSdkVersionUseCase)
- .getSdkVersion();
+ .get(configTypes.GetDmkVersionUseCase)
+ .getDmkVersion();
}
/**
- * Starts discovering devices connected via USB HID (BLE not implemented yet).
+ * Starts discovering devices connected.
*
* For the WeHID implementation, this use-case needs to be called as a result
* of an user interaction (button "click" event for ex).
*
+ * @param {StartDiscoveringUseCaseArgs} args - The transport to use for discover, or undefined to discover from all transports.
* @returns {Observable} An observable of discovered devices.
*/
- startDiscovering(): Observable {
+ startDiscovering(
+ args: StartDiscoveringUseCaseArgs,
+ ): Observable {
return this.container
.get(discoveryTypes.StartDiscoveringUseCase)
- .execute();
+ .execute(args);
}
/**
- * Stops discovering devices connected via USB HID (and later BLE).
+ * Stops discovering devices connected.
*/
stopDiscovering() {
return this.container
@@ -96,11 +111,24 @@ export class DeviceSdk {
}
/**
- * Connects to a device previously discovered with `DeviceSdk.startDiscovering`.
+ * Listen to list of known discovered devices (and later BLE).
+ *
+ * @returns {Observable} An observable of known discovered devices.
+ */
+ listenToKnownDevices(): Observable {
+ return this.container
+ .get(
+ discoveryTypes.ListenToKnownDevicesUseCase,
+ )
+ .execute();
+ }
+
+ /**
+ * Connects to a device previously discovered with `DeviceManagementKit.startDiscovering`.
* Creates a new device session which:
* - Represents the connection to the device.
* - Is terminated upon disconnection of the device.
- * - Exposes the device state through an observable (see `DeviceSdk.getDeviceSessionState`)
+ * - Exposes the device state through an observable (see `DeviceManagementKit.getDeviceSessionState`)
* - Should be used for all subsequent communication with the device.
*
* @param {ConnectUseCaseArgs} args - The device ID (obtained in discovery) to connect to.
@@ -113,7 +141,7 @@ export class DeviceSdk {
}
/**
- * Disconnects to a discovered device via USB HID (and later BLE).
+ * Disconnects to a discovered device.
*
* @param {DisconnectUseCaseArgs} args - The session ID to disconnect.
*/
@@ -151,7 +179,7 @@ export class DeviceSdk {
executeDeviceAction<
Output,
Input,
- Error extends SdkError,
+ Error extends DmkError,
IntermediateValue extends DeviceActionIntermediateValue,
>(
args: ExecuteDeviceActionUseCaseArgs<
@@ -176,7 +204,7 @@ export class DeviceSdk {
*/
getConnectedDevice(args: GetConnectedDeviceUseCaseArgs): ConnectedDevice {
return this.container
- .get(usbDiTypes.GetConnectedDeviceUseCase)
+ .get(discoveryTypes.GetConnectedDeviceUseCase)
.execute(args);
}
@@ -197,14 +225,37 @@ export class DeviceSdk {
}
/**
- * Lists all device sessions.
+ * Close the Device Management kit.
+ *
+ */
+ close() {
+ return this.container
+ .get(deviceSessionTypes.CloseSessionsUseCase)
+ .execute();
+ }
+
+ /**
+ * Lists all connected devices.
+ *
+ * @returns {ConnectedDevice[]} The list of device sessions.
+ */
+ listConnectedDevices(): ConnectedDevice[] {
+ return this.container
+ .get(
+ discoveryTypes.ListConnectedDevicesUseCase,
+ )
+ .execute();
+ }
+
+ /**
+ * Listen to connected device.
*
- * @returns {DeviceSession[]} The list of device sessions.
+ * @returns {Observable} An observable of connected device.
*/
- listDeviceSessions(): DeviceSession[] {
+ listenToConnectedDevice(): Observable {
return this.container
- .get(
- deviceSessionTypes.ListDeviceSessionsUseCase,
+ .get(
+ discoveryTypes.ListenToConnectedDeviceUseCase,
)
.execute();
}
diff --git a/packages/core/src/api/DeviceSdkBuilder.test.ts b/packages/device-management-kit/src/api/DeviceManagementKitBuilder.test.ts
similarity index 56%
rename from packages/core/src/api/DeviceSdkBuilder.test.ts
rename to packages/device-management-kit/src/api/DeviceManagementKitBuilder.test.ts
index c2c19b779..be76d5864 100644
--- a/packages/core/src/api/DeviceSdkBuilder.test.ts
+++ b/packages/device-management-kit/src/api/DeviceManagementKitBuilder.test.ts
@@ -1,20 +1,20 @@
import { ConsoleLogger } from "./logger-subscriber/service/ConsoleLogger";
-import { DeviceSdk } from "./DeviceSdk";
-import { LedgerDeviceSdkBuilder } from "./DeviceSdkBuilder";
+import { DeviceManagementKit } from "./DeviceManagementKit";
+import { DeviceManagementKitBuilder } from "./DeviceManagementKitBuilder";
jest.mock("./logger-subscriber/service/ConsoleLogger");
-let builder: LedgerDeviceSdkBuilder;
+let builder: DeviceManagementKitBuilder;
let logger: ConsoleLogger;
-describe("LedgerDeviceSdkBuilder", () => {
+describe("LedgerDeviceManagementKitBuilder", () => {
beforeEach(() => {
- builder = new LedgerDeviceSdkBuilder();
+ builder = new DeviceManagementKitBuilder();
});
- it("should build a DeviceSdk instance", () => {
- const sdk: DeviceSdk = builder.build();
- expect(sdk).toBeInstanceOf(DeviceSdk);
+ it("should build a DeviceManagementKit instance", () => {
+ const dmk: DeviceManagementKit = builder.build();
+ expect(dmk).toBeInstanceOf(DeviceManagementKit);
});
it("should set the stub flag", () => {
diff --git a/packages/device-management-kit/src/api/DeviceManagementKitBuilder.ts b/packages/device-management-kit/src/api/DeviceManagementKitBuilder.ts
new file mode 100644
index 000000000..e8c49120a
--- /dev/null
+++ b/packages/device-management-kit/src/api/DeviceManagementKitBuilder.ts
@@ -0,0 +1,75 @@
+import {
+ DEFAULT_MANAGER_API_BASE_URL,
+ DEFAULT_MOCK_SERVER_BASE_URL,
+} from "@internal/manager-api/model/Const";
+
+import { type LoggerSubscriberService } from "./logger-subscriber/service/LoggerSubscriberService";
+import { type Transport } from "./transport/model/Transport";
+import { type BuiltinTransports } from "./transport/model/TransportIdentifier";
+import { DeviceManagementKit } from "./DeviceManagementKit";
+import { type DmkConfig } from "./DmkConfig";
+
+/**
+ * Builder for the `DeviceManagementKit` class.
+ *
+ * @example
+ * ```
+ * const dmk = new LedgerDeviceManagementKitBuilder()
+ * .setStub(false)
+ * .addTransport(BuiltinTransports.USB)
+ * .addCustomTransport(new MyTransport())
+ * .addLogger(myLogger)
+ * .build();
+ * ```
+ */
+export class DeviceManagementKitBuilder {
+ private stub = false;
+ private readonly loggers: LoggerSubscriberService[] = [];
+ private readonly transports: BuiltinTransports[] = [];
+ private readonly customTransports: Transport[] = [];
+ private config: DmkConfig = {
+ managerApiUrl: DEFAULT_MANAGER_API_BASE_URL,
+ mockUrl: DEFAULT_MOCK_SERVER_BASE_URL,
+ };
+
+ build(): DeviceManagementKit {
+ return new DeviceManagementKit({
+ stub: this.stub,
+ transports: this.transports,
+ customTransports: this.customTransports,
+ loggers: this.loggers,
+ config: this.config,
+ });
+ }
+
+ setStub(stubbed: boolean): DeviceManagementKitBuilder {
+ this.stub = stubbed;
+ return this;
+ }
+
+ addTransport(transport: BuiltinTransports): DeviceManagementKitBuilder {
+ this.transports.push(transport);
+ return this;
+ }
+
+ addCustomTransport(transport: Transport): DeviceManagementKitBuilder {
+ this.customTransports.push(transport);
+ return this;
+ }
+
+ /**
+ * Add a logger to the SDK that will receive its logs
+ */
+ addLogger(logger: LoggerSubscriberService): DeviceManagementKitBuilder {
+ this.loggers.push(logger);
+ return this;
+ }
+
+ addConfig(config: Partial): DeviceManagementKitBuilder {
+ this.config = {
+ ...this.config,
+ ...config,
+ };
+ return this;
+ }
+}
diff --git a/packages/device-management-kit/src/api/DmkConfig.ts b/packages/device-management-kit/src/api/DmkConfig.ts
new file mode 100644
index 000000000..40b51a50d
--- /dev/null
+++ b/packages/device-management-kit/src/api/DmkConfig.ts
@@ -0,0 +1,4 @@
+export type DmkConfig = {
+ mockUrl: string;
+ managerApiUrl: string;
+};
diff --git a/packages/core/src/api/Error.ts b/packages/device-management-kit/src/api/Error.ts
similarity index 86%
rename from packages/core/src/api/Error.ts
rename to packages/device-management-kit/src/api/Error.ts
index 76057c217..acd6d66d1 100644
--- a/packages/core/src/api/Error.ts
+++ b/packages/device-management-kit/src/api/Error.ts
@@ -1,4 +1,4 @@
-export interface SdkError {
+export interface DmkError {
readonly _tag: string;
readonly originalError?: unknown;
message?: string;
@@ -31,11 +31,11 @@ export abstract class DeviceExchangeError {
this._tag = tag;
this.originalError = originalError;
this.errorCode = errorCode;
- this.message = message;
+ this.message = message ?? "An error occured during device exchange.";
}
}
-export class UnknownDeviceExchangeError implements SdkError {
+export class UnknownDeviceExchangeError implements DmkError {
readonly _tag = "UnknownDeviceExchangeError";
readonly originalError?: unknown;
readonly message: string;
diff --git a/packages/core/src/api/apdu/model/Apdu.test.ts b/packages/device-management-kit/src/api/apdu/model/Apdu.test.ts
similarity index 100%
rename from packages/core/src/api/apdu/model/Apdu.test.ts
rename to packages/device-management-kit/src/api/apdu/model/Apdu.test.ts
diff --git a/packages/core/src/api/apdu/model/Apdu.ts b/packages/device-management-kit/src/api/apdu/model/Apdu.ts
similarity index 100%
rename from packages/core/src/api/apdu/model/Apdu.ts
rename to packages/device-management-kit/src/api/apdu/model/Apdu.ts
diff --git a/packages/core/src/api/apdu/utils/ApduBuilder.test.ts b/packages/device-management-kit/src/api/apdu/utils/ApduBuilder.test.ts
similarity index 100%
rename from packages/core/src/api/apdu/utils/ApduBuilder.test.ts
rename to packages/device-management-kit/src/api/apdu/utils/ApduBuilder.test.ts
diff --git a/packages/core/src/api/apdu/utils/ApduBuilder.ts b/packages/device-management-kit/src/api/apdu/utils/ApduBuilder.ts
similarity index 98%
rename from packages/core/src/api/apdu/utils/ApduBuilder.ts
rename to packages/device-management-kit/src/api/apdu/utils/ApduBuilder.ts
index 7b906722b..6add16954 100644
--- a/packages/core/src/api/apdu/utils/ApduBuilder.ts
+++ b/packages/device-management-kit/src/api/apdu/utils/ApduBuilder.ts
@@ -1,6 +1,6 @@
import { Apdu } from "@api/apdu/model/Apdu";
-import { AppBuilderError } from "./AppBuilderError";
+import { type AppBuilderError } from "./AppBuilderError";
import { ByteArrayBuilder } from "./ByteArrayBuilder";
export const HEADER_LENGTH = 5;
diff --git a/packages/core/src/api/apdu/utils/ApduParser.test.ts b/packages/device-management-kit/src/api/apdu/utils/ApduParser.test.ts
similarity index 100%
rename from packages/core/src/api/apdu/utils/ApduParser.test.ts
rename to packages/device-management-kit/src/api/apdu/utils/ApduParser.test.ts
diff --git a/packages/core/src/api/apdu/utils/ApduParser.ts b/packages/device-management-kit/src/api/apdu/utils/ApduParser.ts
similarity index 96%
rename from packages/core/src/api/apdu/utils/ApduParser.ts
rename to packages/device-management-kit/src/api/apdu/utils/ApduParser.ts
index 43253482c..9ec1c35c2 100644
--- a/packages/core/src/api/apdu/utils/ApduParser.ts
+++ b/packages/device-management-kit/src/api/apdu/utils/ApduParser.ts
@@ -1,5 +1,5 @@
-import { ApduResponse } from "@api/device-session/ApduResponse";
-import { HexaString } from "@api/utils/HexaString";
+import { type ApduResponse } from "@api/device-session/ApduResponse";
+import { type HexaString } from "@api/utils/HexaString";
import { ByteArrayParser } from "./ByteArrayParser";
diff --git a/packages/core/src/api/apdu/utils/AppBuilderError.ts b/packages/device-management-kit/src/api/apdu/utils/AppBuilderError.ts
similarity index 78%
rename from packages/core/src/api/apdu/utils/AppBuilderError.ts
rename to packages/device-management-kit/src/api/apdu/utils/AppBuilderError.ts
index 748e18f93..64e2c0bbe 100644
--- a/packages/core/src/api/apdu/utils/AppBuilderError.ts
+++ b/packages/device-management-kit/src/api/apdu/utils/AppBuilderError.ts
@@ -1,12 +1,12 @@
-import { SdkError } from "@api/Error";
+import { type DmkError } from "@api/Error";
import { APDU_MAX_PAYLOAD } from "./ApduBuilder";
-interface SdkAppBuilderError extends SdkError {
+interface DmkAppBuilderError extends DmkError {
readonly message: string;
}
-export class ValueOverflowError implements SdkAppBuilderError {
+export class ValueOverflowError implements DmkAppBuilderError {
readonly _tag = "ValueOverflow";
readonly originalError?: Error;
readonly message: string;
@@ -15,7 +15,7 @@ export class ValueOverflowError implements SdkAppBuilderError {
}
}
-export class DataOverflowError implements SdkAppBuilderError {
+export class DataOverflowError implements DmkAppBuilderError {
readonly _tag = "DataOverflow";
readonly message: string;
readonly originalError?: Error;
@@ -27,7 +27,7 @@ export class DataOverflowError implements SdkAppBuilderError {
}
}
-export class HexaStringEncodeError implements SdkAppBuilderError {
+export class HexaStringEncodeError implements DmkAppBuilderError {
readonly _tag = "HexaString";
readonly message: string;
readonly originalError?: Error;
diff --git a/packages/core/src/api/apdu/utils/ByteArrayBuilder.test.ts b/packages/device-management-kit/src/api/apdu/utils/ByteArrayBuilder.test.ts
similarity index 100%
rename from packages/core/src/api/apdu/utils/ByteArrayBuilder.test.ts
rename to packages/device-management-kit/src/api/apdu/utils/ByteArrayBuilder.test.ts
diff --git a/packages/core/src/api/apdu/utils/ByteArrayBuilder.ts b/packages/device-management-kit/src/api/apdu/utils/ByteArrayBuilder.ts
similarity index 99%
rename from packages/core/src/api/apdu/utils/ByteArrayBuilder.ts
rename to packages/device-management-kit/src/api/apdu/utils/ByteArrayBuilder.ts
index b403687df..958a394c1 100644
--- a/packages/core/src/api/apdu/utils/ByteArrayBuilder.ts
+++ b/packages/device-management-kit/src/api/apdu/utils/ByteArrayBuilder.ts
@@ -1,7 +1,7 @@
import { hexaStringToBuffer } from "@api/utils/HexaString";
import {
- AppBuilderError,
+ type AppBuilderError,
DataOverflowError,
HexaStringEncodeError,
ValueOverflowError,
diff --git a/packages/core/src/api/apdu/utils/ByteArrayParser.test.ts b/packages/device-management-kit/src/api/apdu/utils/ByteArrayParser.test.ts
similarity index 100%
rename from packages/core/src/api/apdu/utils/ByteArrayParser.test.ts
rename to packages/device-management-kit/src/api/apdu/utils/ByteArrayParser.test.ts
diff --git a/packages/core/src/api/apdu/utils/ByteArrayParser.ts b/packages/device-management-kit/src/api/apdu/utils/ByteArrayParser.ts
similarity index 99%
rename from packages/core/src/api/apdu/utils/ByteArrayParser.ts
rename to packages/device-management-kit/src/api/apdu/utils/ByteArrayParser.ts
index e0d388541..989fc78ff 100644
--- a/packages/core/src/api/apdu/utils/ByteArrayParser.ts
+++ b/packages/device-management-kit/src/api/apdu/utils/ByteArrayParser.ts
@@ -1,4 +1,4 @@
-import { bufferToHexaString, HexaString } from "@api/utils/HexaString";
+import { bufferToHexaString, type HexaString } from "@api/utils/HexaString";
export type TaggedField = {
readonly tag: number;
diff --git a/packages/core/src/api/command/Command.ts b/packages/device-management-kit/src/api/command/Command.ts
similarity index 80%
rename from packages/core/src/api/command/Command.ts
rename to packages/device-management-kit/src/api/command/Command.ts
index 0908d2816..f624ed941 100644
--- a/packages/core/src/api/command/Command.ts
+++ b/packages/device-management-kit/src/api/command/Command.ts
@@ -1,7 +1,7 @@
-import { Apdu } from "@api/apdu/model/Apdu";
-import { CommandResult } from "@api/command/model/CommandResult";
-import { DeviceModelId } from "@api/device/DeviceModel";
-import { ApduResponse } from "@api/device-session/ApduResponse";
+import { type Apdu } from "@api/apdu/model/Apdu";
+import { type CommandResult } from "@api/command/model/CommandResult";
+import { type DeviceModelId } from "@api/device/DeviceModel";
+import { type ApduResponse } from "@api/device-session/ApduResponse";
/**
* A command that can be sent to a device.
diff --git a/packages/core/src/api/command/Errors.ts b/packages/device-management-kit/src/api/command/Errors.ts
similarity index 73%
rename from packages/core/src/api/command/Errors.ts
rename to packages/device-management-kit/src/api/command/Errors.ts
index 7425b3e4e..7faa161a1 100644
--- a/packages/core/src/api/command/Errors.ts
+++ b/packages/device-management-kit/src/api/command/Errors.ts
@@ -1,6 +1,6 @@
-import { SdkError } from "@api/Error";
+import { type DmkError } from "@api/Error";
-export class InvalidStatusWordError implements SdkError {
+export class InvalidStatusWordError implements DmkError {
readonly _tag = "InvalidStatusWordError";
readonly originalError?: Error;
@@ -9,7 +9,7 @@ export class InvalidStatusWordError implements SdkError {
}
}
-export class InvalidBatteryStatusTypeError implements SdkError {
+export class InvalidBatteryStatusTypeError implements DmkError {
readonly _tag = "InvalidBatteryStatusTypeError";
readonly originalError: Error;
@@ -18,7 +18,7 @@ export class InvalidBatteryStatusTypeError implements SdkError {
}
}
-export class InvalidBatteryDataError implements SdkError {
+export class InvalidBatteryDataError implements DmkError {
readonly _tag = "InvalidBatteryDataError";
readonly originalError: Error;
@@ -27,7 +27,7 @@ export class InvalidBatteryDataError implements SdkError {
}
}
-export class InvalidBatteryFlagsError implements SdkError {
+export class InvalidBatteryFlagsError implements DmkError {
readonly _tag = "InvalidBatteryFlagsError";
readonly originalError: Error;
@@ -36,7 +36,7 @@ export class InvalidBatteryFlagsError implements SdkError {
}
}
-export class InvalidResponseFormatError implements SdkError {
+export class InvalidResponseFormatError implements DmkError {
readonly _tag = "InvalidResponseFormatError";
readonly originalError: Error;
diff --git a/packages/core/src/api/command/di/commandModule.test.ts b/packages/device-management-kit/src/api/command/di/commandModule.test.ts
similarity index 100%
rename from packages/core/src/api/command/di/commandModule.test.ts
rename to packages/device-management-kit/src/api/command/di/commandModule.test.ts
diff --git a/packages/core/src/api/command/di/commandModule.ts b/packages/device-management-kit/src/api/command/di/commandModule.ts
similarity index 100%
rename from packages/core/src/api/command/di/commandModule.ts
rename to packages/device-management-kit/src/api/command/di/commandModule.ts
diff --git a/packages/core/src/api/command/di/commandTypes.ts b/packages/device-management-kit/src/api/command/di/commandTypes.ts
similarity index 100%
rename from packages/core/src/api/command/di/commandTypes.ts
rename to packages/device-management-kit/src/api/command/di/commandTypes.ts
diff --git a/packages/core/src/api/command/model/CommandResult.test.ts b/packages/device-management-kit/src/api/command/model/CommandResult.test.ts
similarity index 100%
rename from packages/core/src/api/command/model/CommandResult.test.ts
rename to packages/device-management-kit/src/api/command/model/CommandResult.test.ts
diff --git a/packages/core/src/api/command/model/CommandResult.ts b/packages/device-management-kit/src/api/command/model/CommandResult.ts
similarity index 82%
rename from packages/core/src/api/command/model/CommandResult.ts
rename to packages/device-management-kit/src/api/command/model/CommandResult.ts
index 03b03dd31..e584f99ce 100644
--- a/packages/core/src/api/command/model/CommandResult.ts
+++ b/packages/device-management-kit/src/api/command/model/CommandResult.ts
@@ -1,11 +1,14 @@
import {
- InvalidBatteryDataError,
- InvalidBatteryStatusTypeError,
- InvalidResponseFormatError,
- InvalidStatusWordError,
+ type InvalidBatteryDataError,
+ type InvalidBatteryStatusTypeError,
+ type InvalidResponseFormatError,
+ type InvalidStatusWordError,
} from "@api/command/Errors";
-import { GlobalCommandErrorStatusCode } from "@api/command/utils/GlobalCommandError";
-import { DeviceExchangeError, UnknownDeviceExchangeError } from "@api/Error";
+import { type GlobalCommandErrorStatusCode } from "@api/command/utils/GlobalCommandError";
+import {
+ type DeviceExchangeError,
+ type UnknownDeviceExchangeError,
+} from "@api/Error";
export enum CommandResultStatus {
Error = "ERROR",
diff --git a/packages/core/src/api/command/os/CloseAppCommand.test.ts b/packages/device-management-kit/src/api/command/os/CloseAppCommand.test.ts
similarity index 100%
rename from packages/core/src/api/command/os/CloseAppCommand.test.ts
rename to packages/device-management-kit/src/api/command/os/CloseAppCommand.test.ts
diff --git a/packages/core/src/api/command/os/CloseAppCommand.ts b/packages/device-management-kit/src/api/command/os/CloseAppCommand.ts
similarity index 78%
rename from packages/core/src/api/command/os/CloseAppCommand.ts
rename to packages/device-management-kit/src/api/command/os/CloseAppCommand.ts
index 20e924d24..9d86cad08 100644
--- a/packages/core/src/api/command/os/CloseAppCommand.ts
+++ b/packages/device-management-kit/src/api/command/os/CloseAppCommand.ts
@@ -1,13 +1,13 @@
-import { Apdu } from "@api/apdu/model/Apdu";
-import { ApduBuilder, ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
-import { Command } from "@api/command/Command";
+import { type Apdu } from "@api/apdu/model/Apdu";
+import { ApduBuilder, type ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
+import { type Command } from "@api/command/Command";
import {
- CommandResult,
+ type CommandResult,
CommandResultFactory,
} from "@api/command/model/CommandResult";
import { CommandUtils } from "@api/command/utils/CommandUtils";
import { GlobalCommandErrorHandler } from "@api/command/utils/GlobalCommandError";
-import { ApduResponse } from "@api/device-session/ApduResponse";
+import { type ApduResponse } from "@api/device-session/ApduResponse";
export type CloseAppCommandResult = CommandResult;
diff --git a/packages/core/src/api/command/os/GetAppAndVersionCommand.test.ts b/packages/device-management-kit/src/api/command/os/GetAppAndVersionCommand.test.ts
similarity index 100%
rename from packages/core/src/api/command/os/GetAppAndVersionCommand.test.ts
rename to packages/device-management-kit/src/api/command/os/GetAppAndVersionCommand.test.ts
diff --git a/packages/core/src/api/command/os/GetAppAndVersionCommand.ts b/packages/device-management-kit/src/api/command/os/GetAppAndVersionCommand.ts
similarity index 88%
rename from packages/core/src/api/command/os/GetAppAndVersionCommand.ts
rename to packages/device-management-kit/src/api/command/os/GetAppAndVersionCommand.ts
index a1cf27fc8..259662880 100644
--- a/packages/core/src/api/command/os/GetAppAndVersionCommand.ts
+++ b/packages/device-management-kit/src/api/command/os/GetAppAndVersionCommand.ts
@@ -1,15 +1,15 @@
-import { Apdu } from "@api/apdu/model/Apdu";
-import { ApduBuilder, ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
+import { type Apdu } from "@api/apdu/model/Apdu";
+import { ApduBuilder, type ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
import { ApduParser } from "@api/apdu/utils/ApduParser";
-import { Command } from "@api/command/Command";
+import { type Command } from "@api/command/Command";
import { InvalidResponseFormatError } from "@api/command/Errors";
import {
- CommandResult,
+ type CommandResult,
CommandResultFactory,
} from "@api/command/model/CommandResult";
import { CommandUtils } from "@api/command/utils/CommandUtils";
import { GlobalCommandErrorHandler } from "@api/command/utils/GlobalCommandError";
-import { ApduResponse } from "@api/device-session/ApduResponse";
+import { type ApduResponse } from "@api/device-session/ApduResponse";
export type GetAppAndVersionResponse = {
/**
diff --git a/packages/core/src/api/command/os/GetBatteryStatusCommand.test.ts b/packages/device-management-kit/src/api/command/os/GetBatteryStatusCommand.test.ts
similarity index 100%
rename from packages/core/src/api/command/os/GetBatteryStatusCommand.test.ts
rename to packages/device-management-kit/src/api/command/os/GetBatteryStatusCommand.test.ts
diff --git a/packages/core/src/api/command/os/GetBatteryStatusCommand.ts b/packages/device-management-kit/src/api/command/os/GetBatteryStatusCommand.ts
similarity index 94%
rename from packages/core/src/api/command/os/GetBatteryStatusCommand.ts
rename to packages/device-management-kit/src/api/command/os/GetBatteryStatusCommand.ts
index bb8ebf226..15bcfd321 100644
--- a/packages/core/src/api/command/os/GetBatteryStatusCommand.ts
+++ b/packages/device-management-kit/src/api/command/os/GetBatteryStatusCommand.ts
@@ -1,16 +1,16 @@
-import { Apdu } from "@api/apdu/model/Apdu";
-import { ApduBuilder, ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
+import { type Apdu } from "@api/apdu/model/Apdu";
+import { ApduBuilder, type ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
import { ApduParser } from "@api/apdu/utils/ApduParser";
-import { Command } from "@api/command/Command";
+import { type Command } from "@api/command/Command";
import {
InvalidBatteryDataError,
InvalidBatteryStatusTypeError,
} from "@api/command/Errors";
import {
- CommandResult,
+ type CommandResult,
CommandResultFactory,
} from "@api/command/model/CommandResult";
-import { ApduResponse } from "@api/device-session/ApduResponse";
+import { type ApduResponse } from "@api/device-session/ApduResponse";
/**
* The type of battery information to retrieve.
diff --git a/packages/core/src/api/command/os/GetOsVersionCommand.test.ts b/packages/device-management-kit/src/api/command/os/GetOsVersionCommand.test.ts
similarity index 100%
rename from packages/core/src/api/command/os/GetOsVersionCommand.test.ts
rename to packages/device-management-kit/src/api/command/os/GetOsVersionCommand.test.ts
diff --git a/packages/core/src/api/command/os/GetOsVersionCommand.ts b/packages/device-management-kit/src/api/command/os/GetOsVersionCommand.ts
similarity index 92%
rename from packages/core/src/api/command/os/GetOsVersionCommand.ts
rename to packages/device-management-kit/src/api/command/os/GetOsVersionCommand.ts
index 9afbc73fa..1e73c46bd 100644
--- a/packages/core/src/api/command/os/GetOsVersionCommand.ts
+++ b/packages/device-management-kit/src/api/command/os/GetOsVersionCommand.ts
@@ -1,15 +1,15 @@
-import { Apdu } from "@api/apdu/model/Apdu";
-import { ApduBuilder, ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
+import { type Apdu } from "@api/apdu/model/Apdu";
+import { ApduBuilder, type ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
import { ApduParser } from "@api/apdu/utils/ApduParser";
-import { Command } from "@api/command/Command";
+import { type Command } from "@api/command/Command";
import {
- CommandResult,
+ type CommandResult,
CommandResultFactory,
} from "@api/command/model/CommandResult";
import { CommandUtils } from "@api/command/utils/CommandUtils";
import { GlobalCommandErrorHandler } from "@api/command/utils/GlobalCommandError";
import { DeviceModelId } from "@api/device/DeviceModel";
-import { ApduResponse } from "@api/device-session/ApduResponse";
+import { type ApduResponse } from "@api/device-session/ApduResponse";
/**
* Response of the GetOsVersionCommand.
diff --git a/packages/core/src/api/command/os/ListAppsCommand.test.ts b/packages/device-management-kit/src/api/command/os/ListAppsCommand.test.ts
similarity index 99%
rename from packages/core/src/api/command/os/ListAppsCommand.test.ts
rename to packages/device-management-kit/src/api/command/os/ListAppsCommand.test.ts
index b594de310..65286f6b1 100644
--- a/packages/core/src/api/command/os/ListAppsCommand.test.ts
+++ b/packages/device-management-kit/src/api/command/os/ListAppsCommand.test.ts
@@ -1,5 +1,5 @@
import {
- CommandErrorResult,
+ type CommandErrorResult,
CommandResultFactory,
isSuccessCommandResult,
} from "@api/command/model/CommandResult";
@@ -8,7 +8,7 @@ import { ApduResponse } from "@api/device-session/ApduResponse";
import {
ListAppsCommand,
ListAppsCommandError,
- ListAppsErrorCodes,
+ type ListAppsErrorCodes,
} from "./ListAppsCommand";
// [NOTE] EXAMPLES CREATED USING A NANO X
diff --git a/packages/core/src/api/command/os/ListAppsCommand.ts b/packages/device-management-kit/src/api/command/os/ListAppsCommand.ts
similarity index 89%
rename from packages/core/src/api/command/os/ListAppsCommand.ts
rename to packages/device-management-kit/src/api/command/os/ListAppsCommand.ts
index 14eadd75e..f7dbc35e8 100644
--- a/packages/core/src/api/command/os/ListAppsCommand.ts
+++ b/packages/device-management-kit/src/api/command/os/ListAppsCommand.ts
@@ -1,19 +1,19 @@
-import { Apdu } from "@api/apdu/model/Apdu";
-import { ApduBuilder, ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
+import { type Apdu } from "@api/apdu/model/Apdu";
+import { ApduBuilder, type ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
import { ApduParser } from "@api/apdu/utils/ApduParser";
-import { Command } from "@api/command/Command";
+import { type Command } from "@api/command/Command";
import {
- CommandResult,
+ type CommandResult,
CommandResultFactory,
} from "@api/command/model/CommandResult";
import {
- CommandErrors,
+ type CommandErrors,
isCommandErrorCode,
} from "@api/command/utils/CommandErrors";
import { CommandUtils } from "@api/command/utils/CommandUtils";
import { GlobalCommandErrorHandler } from "@api/command/utils/GlobalCommandError";
-import { ApduResponse } from "@api/device-session/ApduResponse";
-import { CommandErrorArgs, DeviceExchangeError } from "@api/Error";
+import { type ApduResponse } from "@api/device-session/ApduResponse";
+import { type CommandErrorArgs, DeviceExchangeError } from "@api/Error";
export type AppResponse = {
readonly appEntryLength: number;
diff --git a/packages/core/src/api/command/os/OpenAppCommand.test.ts b/packages/device-management-kit/src/api/command/os/OpenAppCommand.test.ts
similarity index 95%
rename from packages/core/src/api/command/os/OpenAppCommand.test.ts
rename to packages/device-management-kit/src/api/command/os/OpenAppCommand.test.ts
index eca62a592..3f46fddba 100644
--- a/packages/core/src/api/command/os/OpenAppCommand.test.ts
+++ b/packages/device-management-kit/src/api/command/os/OpenAppCommand.test.ts
@@ -1,11 +1,11 @@
-import { CommandErrorResult } from "@api/command/model/CommandResult";
+import { type CommandErrorResult } from "@api/command/model/CommandResult";
import { ApduResponse } from "@api/device-session/ApduResponse";
import { isSuccessCommandResult } from "@root/src";
import {
OpenAppCommand,
OpenAppCommandError,
- OpenAppErrorCodes,
+ type OpenAppErrorCodes,
} from "./OpenAppCommand";
describe("OpenAppCommand", () => {
diff --git a/packages/core/src/api/command/os/OpenAppCommand.ts b/packages/device-management-kit/src/api/command/os/OpenAppCommand.ts
similarity index 85%
rename from packages/core/src/api/command/os/OpenAppCommand.ts
rename to packages/device-management-kit/src/api/command/os/OpenAppCommand.ts
index 4b20d9a3c..916ab8c8d 100644
--- a/packages/core/src/api/command/os/OpenAppCommand.ts
+++ b/packages/device-management-kit/src/api/command/os/OpenAppCommand.ts
@@ -1,19 +1,19 @@
-import { Apdu } from "@api/apdu/model/Apdu";
-import { ApduBuilder, ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
+import { type Apdu } from "@api/apdu/model/Apdu";
+import { ApduBuilder, type ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
import { ApduParser } from "@api/apdu/utils/ApduParser";
-import { Command } from "@api/command/Command";
+import { type Command } from "@api/command/Command";
import {
- CommandResult,
+ type CommandResult,
CommandResultFactory,
} from "@api/command/model/CommandResult";
import {
- CommandErrors,
+ type CommandErrors,
isCommandErrorCode,
} from "@api/command/utils/CommandErrors";
import { CommandUtils } from "@api/command/utils/CommandUtils";
import { GlobalCommandErrorHandler } from "@api/command/utils/GlobalCommandError";
-import { ApduResponse } from "@api/device-session/ApduResponse";
-import { CommandErrorArgs, DeviceExchangeError } from "@api/Error";
+import { type ApduResponse } from "@api/device-session/ApduResponse";
+import { type CommandErrorArgs, DeviceExchangeError } from "@api/Error";
export type OpenAppArgs = {
readonly appName: string;
diff --git a/packages/core/src/api/command/use-case/SendCommandUseCase.test.ts b/packages/device-management-kit/src/api/command/use-case/SendCommandUseCase.test.ts
similarity index 83%
rename from packages/core/src/api/command/use-case/SendCommandUseCase.test.ts
rename to packages/device-management-kit/src/api/command/use-case/SendCommandUseCase.test.ts
index 623244451..4e66fb99a 100644
--- a/packages/core/src/api/command/use-case/SendCommandUseCase.test.ts
+++ b/packages/device-management-kit/src/api/command/use-case/SendCommandUseCase.test.ts
@@ -1,16 +1,16 @@
import { Left } from "purify-ts";
-import { Command } from "@api/command/Command";
+import { type Command } from "@api/command/Command";
import { CommandResultStatus } from "@api/command/model/CommandResult";
import { deviceSessionStubBuilder } from "@internal/device-session/model/DeviceSession.stub";
import { DefaultDeviceSessionService } from "@internal/device-session/service/DefaultDeviceSessionService";
-import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
+import { type DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource";
-import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
+import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService";
-import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
+import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
import { SendCommandUseCase } from "./SendCommandUseCase";
@@ -27,6 +27,7 @@ describe("SendCommandUseCase", () => {
sessionService = new DefaultDeviceSessionService(() => logger);
managerApiDataSource = new AxiosManagerApiDataSource({
managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
});
managerApi = new DefaultManagerApiService(managerApiDataSource);
command = {
@@ -58,6 +59,8 @@ describe("SendCommandUseCase", () => {
command,
});
+ deviceSession.close();
+
expect(response).toStrictEqual({
status: CommandResultStatus.Success,
data: undefined,
diff --git a/packages/core/src/api/command/use-case/SendCommandUseCase.ts b/packages/device-management-kit/src/api/command/use-case/SendCommandUseCase.ts
similarity index 100%
rename from packages/core/src/api/command/use-case/SendCommandUseCase.ts
rename to packages/device-management-kit/src/api/command/use-case/SendCommandUseCase.ts
diff --git a/packages/core/src/api/command/utils/CommandErrors.ts b/packages/device-management-kit/src/api/command/utils/CommandErrors.ts
similarity index 91%
rename from packages/core/src/api/command/utils/CommandErrors.ts
rename to packages/device-management-kit/src/api/command/utils/CommandErrors.ts
index a97322455..dbcaa469e 100644
--- a/packages/core/src/api/command/utils/CommandErrors.ts
+++ b/packages/device-management-kit/src/api/command/utils/CommandErrors.ts
@@ -1,4 +1,4 @@
-import { DeviceExchangeErrorArgs } from "@api/Error";
+import { type DeviceExchangeErrorArgs } from "@api/Error";
/**
* CommandErrors dictionary utility type
diff --git a/packages/core/src/api/command/utils/CommandUtils.test.ts b/packages/device-management-kit/src/api/command/utils/CommandUtils.test.ts
similarity index 100%
rename from packages/core/src/api/command/utils/CommandUtils.test.ts
rename to packages/device-management-kit/src/api/command/utils/CommandUtils.test.ts
diff --git a/packages/core/src/api/command/utils/CommandUtils.ts b/packages/device-management-kit/src/api/command/utils/CommandUtils.ts
similarity index 88%
rename from packages/core/src/api/command/utils/CommandUtils.ts
rename to packages/device-management-kit/src/api/command/utils/CommandUtils.ts
index bc63a2789..3b51440a4 100644
--- a/packages/core/src/api/command/utils/CommandUtils.ts
+++ b/packages/device-management-kit/src/api/command/utils/CommandUtils.ts
@@ -1,4 +1,4 @@
-import { ApduResponse } from "@api/device-session/ApduResponse";
+import { type ApduResponse } from "@api/device-session/ApduResponse";
export class CommandUtils {
static isValidStatusCode(statusCode: Uint8Array) {
diff --git a/packages/core/src/api/command/utils/GlobalCommandError.test.ts b/packages/device-management-kit/src/api/command/utils/GlobalCommandError.test.ts
similarity index 100%
rename from packages/core/src/api/command/utils/GlobalCommandError.test.ts
rename to packages/device-management-kit/src/api/command/utils/GlobalCommandError.test.ts
diff --git a/packages/core/src/api/command/utils/GlobalCommandError.ts b/packages/device-management-kit/src/api/command/utils/GlobalCommandError.ts
similarity index 89%
rename from packages/core/src/api/command/utils/GlobalCommandError.ts
rename to packages/device-management-kit/src/api/command/utils/GlobalCommandError.ts
index 6b3629ded..8f9589923 100644
--- a/packages/core/src/api/command/utils/GlobalCommandError.ts
+++ b/packages/device-management-kit/src/api/command/utils/GlobalCommandError.ts
@@ -1,12 +1,12 @@
import { ApduParser } from "@api/apdu/utils/ApduParser";
-import { ApduResponse } from "@api/device-session/ApduResponse";
+import { type ApduResponse } from "@api/device-session/ApduResponse";
import {
- CommandErrorArgs,
+ type CommandErrorArgs,
DeviceExchangeError,
UnknownDeviceExchangeError,
} from "@api/Error";
-import { CommandErrors, isCommandErrorCode } from "./CommandErrors";
+import { type CommandErrors, isCommandErrorCode } from "./CommandErrors";
/**
* Status word list of global errors that any command could result
@@ -37,7 +37,7 @@ export const GLOBAL_ERRORS: CommandErrors = {
*/
export class GlobalCommandErrorHandler {
/**
- * Static method to get a handled GlobalCommandError or an unhandled SdkError from an apdu response
+ * Static method to get a handled GlobalCommandError or an unhandled DmkError from an apdu response
* @param apduResponse
*/
static handle(
diff --git a/packages/core/src/api/device-action/DeviceAction.ts b/packages/device-management-kit/src/api/device-action/DeviceAction.ts
similarity index 68%
rename from packages/core/src/api/device-action/DeviceAction.ts
rename to packages/device-management-kit/src/api/device-action/DeviceAction.ts
index 304b95bde..32d5a22ca 100644
--- a/packages/core/src/api/device-action/DeviceAction.ts
+++ b/packages/device-management-kit/src/api/device-action/DeviceAction.ts
@@ -1,12 +1,12 @@
-import { Observable } from "rxjs";
+import { type Observable } from "rxjs";
-import { Command } from "@api/command/Command";
-import { CommandResult } from "@api/command/model/CommandResult";
-import { DeviceSessionState } from "@api/device-session/DeviceSessionState";
-import { SdkError } from "@api/Error";
-import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
+import { type Command } from "@api/command/Command";
+import { type CommandResult } from "@api/command/model/CommandResult";
+import { type DeviceSessionState } from "@api/device-session/DeviceSessionState";
+import { type DmkError } from "@api/Error";
+import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
-import { DeviceActionState } from "./model/DeviceActionState";
+import { type DeviceActionState } from "./model/DeviceActionState";
export type InternalApi = {
readonly sendCommand: (
@@ -34,7 +34,7 @@ export type ExecuteDeviceActionReturnType = {
export interface DeviceAction<
Output,
Input,
- Error extends SdkError,
+ Error extends DmkError,
IntermediateValue extends DeviceActionIntermediateValue,
> {
readonly input: Input;
diff --git a/packages/core/src/api/device-action/__test-utils__/data.ts b/packages/device-management-kit/src/api/device-action/__test-utils__/data.ts
similarity index 100%
rename from packages/core/src/api/device-action/__test-utils__/data.ts
rename to packages/device-management-kit/src/api/device-action/__test-utils__/data.ts
diff --git a/packages/signer/keyring-eth/src/internal/app-binder/device-action/__test-utils__/makeInternalApi.ts b/packages/device-management-kit/src/api/device-action/__test-utils__/makeInternalApi.ts
similarity index 89%
rename from packages/signer/keyring-eth/src/internal/app-binder/device-action/__test-utils__/makeInternalApi.ts
rename to packages/device-management-kit/src/api/device-action/__test-utils__/makeInternalApi.ts
index d6d8d1f18..08209a960 100644
--- a/packages/signer/keyring-eth/src/internal/app-binder/device-action/__test-utils__/makeInternalApi.ts
+++ b/packages/device-management-kit/src/api/device-action/__test-utils__/makeInternalApi.ts
@@ -1,4 +1,4 @@
-import { InternalApi } from "@ledgerhq/device-management-kit";
+import { type InternalApi } from "@api/device-action/DeviceAction";
const sendCommandMock = jest.fn();
const apiGetDeviceSessionStateMock = jest.fn();
diff --git a/packages/core/src/api/device-action/__test-utils__/setupTestMachine.ts b/packages/device-management-kit/src/api/device-action/__test-utils__/setupTestMachine.ts
similarity index 86%
rename from packages/core/src/api/device-action/__test-utils__/setupTestMachine.ts
rename to packages/device-management-kit/src/api/device-action/__test-utils__/setupTestMachine.ts
index 2e2b23a55..0c69439be 100644
--- a/packages/core/src/api/device-action/__test-utils__/setupTestMachine.ts
+++ b/packages/device-management-kit/src/api/device-action/__test-utils__/setupTestMachine.ts
@@ -6,9 +6,9 @@ import { UnknownDAError } from "@api/device-action/os/Errors";
import { GetDeviceStatusDeviceAction } from "@api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction";
import { GoToDashboardDeviceAction } from "@api/device-action/os/GoToDashboard/GoToDashboardDeviceAction";
import { ListAppsDeviceAction } from "@api/device-action/os/ListApps/ListAppsDeviceAction";
-import { SdkError } from "@api/Error";
+import { type DmkError } from "@api/Error";
-import { BTC_APP } from "./data";
+import { type BTC_APP } from "./data";
type App = typeof BTC_APP;
@@ -75,11 +75,22 @@ export const setupGoToDashboardMock = (error: boolean = false) => {
};
export const setupGetDeviceStatusMock = (
- output: { currentApp: string; currentAppVersion: string } | SdkError = {
- currentApp: "BOLOS",
- currentAppVersion: "1.0.0",
- },
+ outputs: ReadonlyArray<
+ { currentApp: string; currentAppVersion: string } | DmkError
+ > = [
+ {
+ currentApp: "BOLOS",
+ currentAppVersion: "1.0.0",
+ },
+ ],
) => {
+ const outputFn = jest.fn();
+
+ for (const output of outputs) {
+ outputFn.mockImplementationOnce(() =>
+ "currentApp" in output ? Right(output) : Left(output),
+ );
+ }
(GetDeviceStatusDeviceAction as jest.Mock).mockImplementation(() => ({
makeStateMachine: jest.fn().mockImplementation(() =>
createMachine({
@@ -100,9 +111,7 @@ export const setupGetDeviceStatusMock = (
type: "final",
},
},
- output: () => {
- return "currentApp" in output ? Right(output) : Left(output);
- },
+ output: outputFn,
}),
),
}));
diff --git a/packages/core/src/api/device-action/__test-utils__/testDeviceActionStates.ts b/packages/device-management-kit/src/api/device-action/__test-utils__/testDeviceActionStates.ts
similarity index 82%
rename from packages/core/src/api/device-action/__test-utils__/testDeviceActionStates.ts
rename to packages/device-management-kit/src/api/device-action/__test-utils__/testDeviceActionStates.ts
index 8fa08f591..dad6bedd9 100644
--- a/packages/core/src/api/device-action/__test-utils__/testDeviceActionStates.ts
+++ b/packages/device-management-kit/src/api/device-action/__test-utils__/testDeviceActionStates.ts
@@ -1,10 +1,10 @@
import {
- DeviceAction,
- DeviceActionIntermediateValue,
- InternalApi,
+ type DeviceAction,
+ type DeviceActionIntermediateValue,
+ type InternalApi,
} from "@api/device-action/DeviceAction";
-import { DeviceActionState } from "@api/device-action/model/DeviceActionState";
-import { SdkError } from "@api/Error";
+import { type DeviceActionState } from "@api/device-action/model/DeviceActionState";
+import { type DmkError } from "@api/Error";
/**
* Test that the states emitted by a device action match the expected states.
@@ -16,7 +16,7 @@ import { SdkError } from "@api/Error";
export function testDeviceActionStates<
Output,
Input,
- Error extends SdkError,
+ Error extends DmkError,
IntermediateValue extends DeviceActionIntermediateValue,
>(
deviceAction: DeviceAction,
diff --git a/packages/core/src/api/device-action/di/deviceActionModule.test.ts b/packages/device-management-kit/src/api/device-action/di/deviceActionModule.test.ts
similarity index 100%
rename from packages/core/src/api/device-action/di/deviceActionModule.test.ts
rename to packages/device-management-kit/src/api/device-action/di/deviceActionModule.test.ts
diff --git a/packages/core/src/api/device-action/di/deviceActionModule.ts b/packages/device-management-kit/src/api/device-action/di/deviceActionModule.ts
similarity index 100%
rename from packages/core/src/api/device-action/di/deviceActionModule.ts
rename to packages/device-management-kit/src/api/device-action/di/deviceActionModule.ts
diff --git a/packages/core/src/api/device-action/di/deviceActionTypes.ts b/packages/device-management-kit/src/api/device-action/di/deviceActionTypes.ts
similarity index 100%
rename from packages/core/src/api/device-action/di/deviceActionTypes.ts
rename to packages/device-management-kit/src/api/device-action/di/deviceActionTypes.ts
diff --git a/packages/core/src/api/device-action/model/DeviceActionState.ts b/packages/device-management-kit/src/api/device-action/model/DeviceActionState.ts
similarity index 100%
rename from packages/core/src/api/device-action/model/DeviceActionState.ts
rename to packages/device-management-kit/src/api/device-action/model/DeviceActionState.ts
diff --git a/packages/core/src/api/device-action/model/UserInteractionRequired.ts b/packages/device-management-kit/src/api/device-action/model/UserInteractionRequired.ts
similarity index 100%
rename from packages/core/src/api/device-action/model/UserInteractionRequired.ts
rename to packages/device-management-kit/src/api/device-action/model/UserInteractionRequired.ts
diff --git a/packages/core/src/api/device-action/os/Const.ts b/packages/device-management-kit/src/api/device-action/os/Const.ts
similarity index 100%
rename from packages/core/src/api/device-action/os/Const.ts
rename to packages/device-management-kit/src/api/device-action/os/Const.ts
diff --git a/packages/core/src/api/device-action/os/Errors.ts b/packages/device-management-kit/src/api/device-action/os/Errors.ts
similarity index 72%
rename from packages/core/src/api/device-action/os/Errors.ts
rename to packages/device-management-kit/src/api/device-action/os/Errors.ts
index 9a925aec2..71a178c6f 100644
--- a/packages/core/src/api/device-action/os/Errors.ts
+++ b/packages/device-management-kit/src/api/device-action/os/Errors.ts
@@ -1,6 +1,6 @@
-import { SdkError } from "@api/Error";
+import { type DmkError } from "@api/Error";
-export class DeviceNotOnboardedError implements SdkError {
+export class DeviceNotOnboardedError implements DmkError {
readonly _tag = "DeviceNotOnboardedError";
readonly originalError?: Error;
@@ -9,7 +9,7 @@ export class DeviceNotOnboardedError implements SdkError {
}
}
-export class DeviceLockedError implements SdkError {
+export class DeviceLockedError implements DmkError {
readonly _tag = "DeviceLockedError";
readonly originalError?: Error;
@@ -18,7 +18,7 @@ export class DeviceLockedError implements SdkError {
}
}
-export class UnknownDAError implements SdkError {
+export class UnknownDAError implements DmkError {
readonly _tag = "UnknownDAError";
readonly originalError?: Error;
diff --git a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts b/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts
similarity index 99%
rename from packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts
rename to packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts
index 1cb810359..300ef916a 100644
--- a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts
+++ b/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.test.ts
@@ -18,7 +18,7 @@ import {
import { DeviceSessionStateType } from "@api/device-session/DeviceSessionState";
import { GetDeviceStatusDeviceAction } from "./GetDeviceStatusDeviceAction";
-import { GetDeviceStatusDAState } from "./types";
+import { type GetDeviceStatusDAState } from "./types";
describe("GetDeviceStatusDeviceAction", () => {
const getAppAndVersionMock = jest.fn();
diff --git a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts b/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts
similarity index 96%
rename from packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts
rename to packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts
index 1e9aeb790..491c699e0 100644
--- a/packages/core/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts
+++ b/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction.ts
@@ -6,31 +6,31 @@ import { assign, fromObservable, fromPromise, setup } from "xstate";
import { isSuccessCommandResult } from "@api/command/model/CommandResult";
import {
GetAppAndVersionCommand,
- GetAppAndVersionCommandResult,
+ type GetAppAndVersionCommandResult,
} from "@api/command/os/GetAppAndVersionCommand";
import { DeviceStatus } from "@api/device/DeviceStatus";
-import { InternalApi } from "@api/device-action/DeviceAction";
+import { type InternalApi } from "@api/device-action/DeviceAction";
import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
import { DEFAULT_UNLOCK_TIMEOUT_MS } from "@api/device-action/os/Const";
import {
DeviceLockedError,
DeviceNotOnboardedError,
} from "@api/device-action/os/Errors";
-import { StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes";
+import { type StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes";
import {
- DeviceActionStateMachine,
+ type DeviceActionStateMachine,
XStateDeviceAction,
} from "@api/device-action/xstate-utils/XStateDeviceAction";
import {
- DeviceSessionState,
+ type DeviceSessionState,
DeviceSessionStateType,
} from "@api/device-session/DeviceSessionState";
import {
- GetDeviceStatusDAError,
- GetDeviceStatusDAInput,
- GetDeviceStatusDAIntermediateValue,
- GetDeviceStatusDAOutput,
+ type GetDeviceStatusDAError,
+ type GetDeviceStatusDAInput,
+ type GetDeviceStatusDAIntermediateValue,
+ type GetDeviceStatusDAOutput,
} from "./types";
type GetDeviceStatusMachineInternalState = {
@@ -311,7 +311,7 @@ export class GetDeviceStatusDeviceAction extends XStateDeviceAction<
}
return Right({
currentApp: currentApp!,
- currentAppVersion,
+ currentAppVersion: currentAppVersion!,
});
},
});
diff --git a/packages/core/src/api/device-action/os/GetDeviceStatus/types.ts b/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/types.ts
similarity index 60%
rename from packages/core/src/api/device-action/os/GetDeviceStatus/types.ts
rename to packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/types.ts
index a21e015f1..9409621c3 100644
--- a/packages/core/src/api/device-action/os/GetDeviceStatus/types.ts
+++ b/packages/device-management-kit/src/api/device-action/os/GetDeviceStatus/types.ts
@@ -1,15 +1,15 @@
-import { CommandErrorResult } from "@api/command/model/CommandResult";
-import { DeviceActionState } from "@api/device-action/model/DeviceActionState";
-import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
+import { type CommandErrorResult } from "@api/command/model/CommandResult";
+import { type DeviceActionState } from "@api/device-action/model/DeviceActionState";
+import { type UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
import {
- DeviceLockedError,
- DeviceNotOnboardedError,
- UnknownDAError,
+ type DeviceLockedError,
+ type DeviceNotOnboardedError,
+ type UnknownDAError,
} from "@api/device-action/os/Errors";
export type GetDeviceStatusDAOutput = {
readonly currentApp: string;
- readonly currentAppVersion: string | null;
+ readonly currentAppVersion: string;
};
export type GetDeviceStatusDAInput = {
readonly unlockTimeout?: number;
@@ -23,8 +23,7 @@ export type GetDeviceStatusDAError =
export type GetDeviceStatusDARequiredInteraction =
| UserInteractionRequired.None
- | UserInteractionRequired.UnlockDevice
- | UserInteractionRequired.AllowSecureConnection;
+ | UserInteractionRequired.UnlockDevice;
export type GetDeviceStatusDAIntermediateValue = {
requiredUserInteraction: GetDeviceStatusDARequiredInteraction;
diff --git a/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts b/packages/device-management-kit/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts
similarity index 95%
rename from packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts
rename to packages/device-management-kit/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts
index 87639c7fc..b4caa6d70 100644
--- a/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts
+++ b/packages/device-management-kit/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.test.ts
@@ -14,7 +14,7 @@ import { DeviceSessionStateType } from "@api/device-session/DeviceSessionState";
import { UnknownDeviceExchangeError } from "@root/src";
import { GoToDashboardDeviceAction } from "./GoToDashboardDeviceAction";
-import { GoToDashboardDAState } from "./types";
+import { type GoToDashboardDAState } from "./types";
jest.mock("@api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction");
@@ -85,10 +85,12 @@ describe("GoToDashboardDeviceAction", () => {
});
it("should run the device action with device not on dashboard", (done) => {
- setupGetDeviceStatusMock({
- currentApp: "Bitcoin",
- currentAppVersion: "1.0.0",
- });
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "Bitcoin",
+ currentAppVersion: "1.0.0",
+ },
+ ]);
const goToDashboardDeviceAction = new GoToDashboardDeviceAction({
input: { unlockTimeout: 500 },
@@ -258,7 +260,7 @@ describe("GoToDashboardDeviceAction", () => {
describe("error cases", () => {
it("should return an error if GetDeviceStatus return an error", (done) => {
- setupGetDeviceStatusMock(new UnknownDAError("Unknown error"));
+ setupGetDeviceStatusMock([new UnknownDAError("Unknown error")]);
const goToDashboardDeviceAction = new GoToDashboardDeviceAction({
input: { unlockTimeout: 500 },
@@ -300,10 +302,12 @@ describe("GoToDashboardDeviceAction", () => {
describe("not on dashboard", () => {
it("should return an error if closeApp fails", (done) => {
- setupGetDeviceStatusMock({
- currentApp: "Bitcoin",
- currentAppVersion: "1.0.0",
- });
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "Bitcoin",
+ currentAppVersion: "1.0.0",
+ },
+ ]);
const goToDashboardDeviceAction = new GoToDashboardDeviceAction({
input: {},
@@ -359,10 +363,12 @@ describe("GoToDashboardDeviceAction", () => {
});
it("should return an error if getAppAndVersion fails", (done) => {
- setupGetDeviceStatusMock({
- currentApp: "Bitcoin",
- currentAppVersion: "1.0.0",
- });
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "Bitcoin",
+ currentAppVersion: "1.0.0",
+ },
+ ]);
const goToDashboardDeviceAction = new GoToDashboardDeviceAction({
input: { unlockTimeout: 500 },
@@ -431,10 +437,12 @@ describe("GoToDashboardDeviceAction", () => {
});
it("should return an error if getAppAndVersion does not return an app name", (done) => {
- setupGetDeviceStatusMock({
- currentApp: "Bitcoin",
- currentAppVersion: "1.0.0",
- });
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "Bitcoin",
+ currentAppVersion: "1.0.0",
+ },
+ ]);
const goToDashboardDeviceAction = new GoToDashboardDeviceAction({
input: { unlockTimeout: 500 },
diff --git a/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.ts b/packages/device-management-kit/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.ts
similarity index 97%
rename from packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.ts
rename to packages/device-management-kit/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.ts
index 61cf9b59d..8ee2d6231 100644
--- a/packages/core/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.ts
+++ b/packages/device-management-kit/src/api/device-action/os/GoToDashboard/GoToDashboardDeviceAction.ts
@@ -4,24 +4,24 @@ import { assign, fromPromise, setup } from "xstate";
import { isSuccessCommandResult } from "@api/command/model/CommandResult";
import {
CloseAppCommand,
- CloseAppCommandResult,
+ type CloseAppCommandResult,
} from "@api/command/os/CloseAppCommand";
import {
GetAppAndVersionCommand,
- GetAppAndVersionCommandResult,
+ type GetAppAndVersionCommandResult,
} from "@api/command/os/GetAppAndVersionCommand";
-import { InternalApi } from "@api/device-action/DeviceAction";
+import { type InternalApi } from "@api/device-action/DeviceAction";
import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
import { DEFAULT_UNLOCK_TIMEOUT_MS } from "@api/device-action/os/Const";
import { UnknownDAError } from "@api/device-action/os/Errors";
import { GetDeviceStatusDeviceAction } from "@api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction";
-import { StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes";
+import { type StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes";
import {
- DeviceActionStateMachine,
+ type DeviceActionStateMachine,
XStateDeviceAction,
} from "@api/device-action/xstate-utils/XStateDeviceAction";
import {
- DeviceSessionState,
+ type DeviceSessionState,
DeviceSessionStateType,
} from "@api/device-session/DeviceSessionState";
diff --git a/packages/core/src/api/device-action/os/GoToDashboard/types.ts b/packages/device-management-kit/src/api/device-action/os/GoToDashboard/types.ts
similarity index 61%
rename from packages/core/src/api/device-action/os/GoToDashboard/types.ts
rename to packages/device-management-kit/src/api/device-action/os/GoToDashboard/types.ts
index 4c7ac6bf6..fe30d96c7 100644
--- a/packages/core/src/api/device-action/os/GoToDashboard/types.ts
+++ b/packages/device-management-kit/src/api/device-action/os/GoToDashboard/types.ts
@@ -1,11 +1,11 @@
-import { CommandErrorResult } from "@api/command/model/CommandResult";
-import { DeviceActionState } from "@api/device-action/model/DeviceActionState";
-import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
-import { UnknownDAError } from "@api/device-action/os/Errors";
+import { type CommandErrorResult } from "@api/command/model/CommandResult";
+import { type DeviceActionState } from "@api/device-action/model/DeviceActionState";
+import { type UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
+import { type UnknownDAError } from "@api/device-action/os/Errors";
import {
- GetDeviceStatusDAError,
- GetDeviceStatusDAInput,
- GetDeviceStatusDAIntermediateValue,
+ type GetDeviceStatusDAError,
+ type GetDeviceStatusDAInput,
+ type GetDeviceStatusDAIntermediateValue,
} from "@api/device-action/os/GetDeviceStatus/types";
export type GoToDashboardDAOutput = void;
diff --git a/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts b/packages/device-management-kit/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts
similarity index 99%
rename from packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts
rename to packages/device-management-kit/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts
index b6f265068..8787f11cd 100644
--- a/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts
+++ b/packages/device-management-kit/src/api/device-action/os/ListApps/ListAppsDeviceAction.test.ts
@@ -1,5 +1,5 @@
import { CommandResultFactory } from "@api/command/model/CommandResult";
-import { ListAppsResponse } from "@api/command/os/ListAppsCommand";
+import { type ListAppsResponse } from "@api/command/os/ListAppsCommand";
import {
GLOBAL_ERRORS,
GlobalCommandError,
@@ -19,7 +19,7 @@ import { UserInteractionRequired } from "@api/device-action/model/UserInteractio
import { UnknownDAError } from "@api/device-action/os/Errors";
import { ListAppsDeviceAction } from "./ListAppsDeviceAction";
-import { ListAppsDAState } from "./types";
+import { type ListAppsDAState } from "./types";
jest.mock("@api/device-action/os/GoToDashboard/GoToDashboardDeviceAction");
diff --git a/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.ts b/packages/device-management-kit/src/api/device-action/os/ListApps/ListAppsDeviceAction.ts
similarity index 96%
rename from packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.ts
rename to packages/device-management-kit/src/api/device-action/os/ListApps/ListAppsDeviceAction.ts
index 0b68095e4..e68fd1a69 100644
--- a/packages/core/src/api/device-action/os/ListApps/ListAppsDeviceAction.ts
+++ b/packages/device-management-kit/src/api/device-action/os/ListApps/ListAppsDeviceAction.ts
@@ -3,25 +3,25 @@ import { assign, fromPromise, setup } from "xstate";
import { isSuccessCommandResult } from "@api/command/model/CommandResult";
import {
- AppResponse,
+ type AppResponse,
ListAppsCommand,
- ListAppsCommandResult,
+ type ListAppsCommandResult,
} from "@api/command/os/ListAppsCommand";
-import { InternalApi } from "@api/device-action/DeviceAction";
+import { type InternalApi } from "@api/device-action/DeviceAction";
import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
import { DEFAULT_UNLOCK_TIMEOUT_MS } from "@api/device-action/os/Const";
import { GoToDashboardDeviceAction } from "@api/device-action/os/GoToDashboard/GoToDashboardDeviceAction";
-import { StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes";
+import { type StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes";
import {
- DeviceActionStateMachine,
+ type DeviceActionStateMachine,
XStateDeviceAction,
} from "@api/device-action/xstate-utils/XStateDeviceAction";
import {
- ListAppsDAError,
- ListAppsDAInput,
- ListAppsDAIntermediateValue,
- ListAppsDAOutput,
+ type ListAppsDAError,
+ type ListAppsDAInput,
+ type ListAppsDAIntermediateValue,
+ type ListAppsDAOutput,
} from "./types";
type ListAppsMachineInternalState = {
diff --git a/packages/core/src/api/device-action/os/ListApps/types.ts b/packages/device-management-kit/src/api/device-action/os/ListApps/types.ts
similarity index 61%
rename from packages/core/src/api/device-action/os/ListApps/types.ts
rename to packages/device-management-kit/src/api/device-action/os/ListApps/types.ts
index d8a3012eb..a26aa0186 100644
--- a/packages/core/src/api/device-action/os/ListApps/types.ts
+++ b/packages/device-management-kit/src/api/device-action/os/ListApps/types.ts
@@ -1,15 +1,15 @@
-import { CommandErrorResult } from "@api/command/model/CommandResult";
+import { type CommandErrorResult } from "@api/command/model/CommandResult";
import {
- ListAppsErrorCodes,
- ListAppsResponse,
+ type ListAppsErrorCodes,
+ type ListAppsResponse,
} from "@api/command/os/ListAppsCommand";
-import { DeviceActionState } from "@api/device-action/model/DeviceActionState";
-import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
-import { UnknownDAError } from "@api/device-action/os/Errors";
+import { type DeviceActionState } from "@api/device-action/model/DeviceActionState";
+import { type UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
+import { type UnknownDAError } from "@api/device-action/os/Errors";
import {
- GoToDashboardDAError,
- GoToDashboardDAInput,
- GoToDashboardDAIntermediateValue,
+ type GoToDashboardDAError,
+ type GoToDashboardDAInput,
+ type GoToDashboardDAIntermediateValue,
} from "@api/device-action/os/GoToDashboard/types";
export type ListAppsDAOutput = ListAppsResponse;
diff --git a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts b/packages/device-management-kit/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts
similarity index 99%
rename from packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts
rename to packages/device-management-kit/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts
index eb5375ffc..8856c1f7c 100644
--- a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts
+++ b/packages/device-management-kit/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.test.ts
@@ -19,7 +19,7 @@ import { DeviceSessionStateType } from "@api/device-session/DeviceSessionState";
import { HttpFetchApiError } from "@internal/manager-api/model/Errors";
import { ListAppsWithMetadataDeviceAction } from "./ListAppsWithMetadataDeviceAction";
-import { ListAppsWithMetadataDAState } from "./types";
+import { type ListAppsWithMetadataDAState } from "./types";
jest.mock("@api/device-action/os/ListApps/ListAppsDeviceAction");
diff --git a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts b/packages/device-management-kit/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts
similarity index 92%
rename from packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts
rename to packages/device-management-kit/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts
index f059e4a3b..1cfee8edd 100644
--- a/packages/core/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts
+++ b/packages/device-management-kit/src/api/device-action/os/ListAppsWithMetadata/ListAppsWithMetadataDeviceAction.ts
@@ -1,32 +1,32 @@
-import { EitherAsync, Left, Right } from "purify-ts";
+import { type EitherAsync, Left, Right } from "purify-ts";
import {
- AnyEventObject,
+ type AnyEventObject,
assign,
fromCallback,
fromPromise,
setup,
} from "xstate";
-import { ListAppsResponse } from "@api/command/os/ListAppsCommand";
-import { InternalApi } from "@api/device-action/DeviceAction";
+import { type ListAppsResponse } from "@api/command/os/ListAppsCommand";
+import { type InternalApi } from "@api/device-action/DeviceAction";
import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
import { DEFAULT_UNLOCK_TIMEOUT_MS } from "@api/device-action/os/Const";
import { ListAppsDeviceAction } from "@api/device-action/os/ListApps/ListAppsDeviceAction";
-import { ListAppsDAOutput } from "@api/device-action/os/ListApps/types";
-import { StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes";
+import { type ListAppsDAOutput } from "@api/device-action/os/ListApps/types";
+import { type StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes";
import {
- DeviceActionStateMachine,
+ type DeviceActionStateMachine,
XStateDeviceAction,
} from "@api/device-action/xstate-utils/XStateDeviceAction";
-import { DeviceSessionState } from "@api/device-session/DeviceSessionState";
-import { HttpFetchApiError } from "@internal/manager-api/model/Errors";
-import { Application } from "@internal/manager-api/model/ManagerApiType";
+import { type DeviceSessionState } from "@api/device-session/DeviceSessionState";
+import { type HttpFetchApiError } from "@internal/manager-api/model/Errors";
+import { type Application } from "@internal/manager-api/model/ManagerApiType";
import {
- ListAppsWithMetadataDAError,
- ListAppsWithMetadataDAInput,
- ListAppsWithMetadataDAIntermediateValue,
- ListAppsWithMetadataDAOutput,
+ type ListAppsWithMetadataDAError,
+ type ListAppsWithMetadataDAInput,
+ type ListAppsWithMetadataDAIntermediateValue,
+ type ListAppsWithMetadataDAOutput,
} from "./types";
type ListAppsWithMetadataMachineInternalState = {
diff --git a/packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts b/packages/device-management-kit/src/api/device-action/os/ListAppsWithMetadata/types.ts
similarity index 54%
rename from packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts
rename to packages/device-management-kit/src/api/device-action/os/ListAppsWithMetadata/types.ts
index 6060e7a85..368ae95c1 100644
--- a/packages/core/src/api/device-action/os/ListAppsWithMetadata/types.ts
+++ b/packages/device-management-kit/src/api/device-action/os/ListAppsWithMetadata/types.ts
@@ -1,15 +1,15 @@
-import { CommandErrorResult } from "@api/command/model/CommandResult";
-import { ListAppsErrorCodes } from "@api/command/os/ListAppsCommand";
-import { DeviceActionState } from "@api/device-action/model/DeviceActionState";
-import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
-import { UnknownDAError } from "@api/device-action/os/Errors";
+import { type CommandErrorResult } from "@api/command/model/CommandResult";
+import { type ListAppsErrorCodes } from "@api/command/os/ListAppsCommand";
+import { type DeviceActionState } from "@api/device-action/model/DeviceActionState";
+import { type UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
+import { type UnknownDAError } from "@api/device-action/os/Errors";
import {
- ListAppsDAError,
- ListAppsDAInput,
- ListAppsDAIntermediateValue,
+ type ListAppsDAError,
+ type ListAppsDAInput,
+ type ListAppsDAIntermediateValue,
} from "@api/device-action/os/ListApps/types";
-import { HttpFetchApiError } from "@internal/manager-api/model/Errors";
-import { Application } from "@internal/manager-api/model/ManagerApiType";
+import { type HttpFetchApiError } from "@internal/manager-api/model/Errors";
+import { type Application } from "@internal/manager-api/model/ManagerApiType";
export type ListAppsWithMetadataDAOutput = Array;
export type ListAppsWithMetadataDAInput = ListAppsDAInput;
diff --git a/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts b/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts
similarity index 79%
rename from packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts
rename to packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts
index b83b708fd..d63b85458 100644
--- a/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts
+++ b/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.test.ts
@@ -4,6 +4,7 @@ import { InvalidStatusWordError } from "@api/command/Errors";
import { CommandResultFactory } from "@api/command/model/CommandResult";
import { DeviceStatus } from "@api/device/DeviceStatus";
import { makeDeviceActionInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi";
+import { setupGetDeviceStatusMock } from "@api/device-action/__test-utils__/setupTestMachine";
import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates";
import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState";
import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
@@ -17,6 +18,8 @@ import { DeviceSessionStateType } from "@api/device-session/DeviceSessionState";
import { OpenAppDeviceAction } from "./OpenAppDeviceAction";
import type { OpenAppDAState } from "./types";
+jest.mock("@api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction");
+
describe("OpenAppDeviceAction", () => {
const getAppAndVersionMock = jest.fn();
const openAppMock = jest.fn();
@@ -36,10 +39,8 @@ describe("OpenAppDeviceAction", () => {
};
}
- const {
- sendCommand: sendCommandMock,
- getDeviceSessionState: apiGetDeviceSessionStateMock,
- } = makeDeviceActionInternalApiMock();
+ const { getDeviceSessionState: apiGetDeviceSessionStateMock } =
+ makeDeviceActionInternalApiMock();
beforeEach(() => {
jest.resetAllMocks();
@@ -54,15 +55,12 @@ describe("OpenAppDeviceAction", () => {
currentApp: { name: "Bitcoin", version: "1.0.0" },
installedApps: [],
});
-
- sendCommandMock.mockResolvedValueOnce(
- CommandResultFactory({
- data: {
- name: "Bitcoin",
- version: "0.0.0",
- },
- }),
- );
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "Bitcoin",
+ currentAppVersion: "0.0.0",
+ },
+ ]);
const openAppDeviceAction = new OpenAppDeviceAction({
input: { appName: "Bitcoin" },
@@ -70,7 +68,13 @@ describe("OpenAppDeviceAction", () => {
const expectedStates: Array = [
{
- status: DeviceActionStatus.Pending, // get app and version
+ status: DeviceActionStatus.Pending, // get onboarding status
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
+ {
+ status: DeviceActionStatus.Pending, // get device status
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.None,
},
@@ -97,23 +101,27 @@ describe("OpenAppDeviceAction", () => {
deviceStatus: DeviceStatus.CONNECTED,
currentApp: { name: "Bitcoin", version: "1.0.0" },
});
- getAppAndVersionMock.mockResolvedValue(
- CommandResultFactory({
- data: {
- name: "Bitcoin",
- version: "1.0.0",
- },
- }),
- );
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "Bitcoin",
+ currentAppVersion: "1.0.0",
+ },
+ ]);
const openAppDeviceAction = new OpenAppDeviceAction({
- input: { appName: "Bitcoin" },
+ input: { appName: "Bitcoin", unlockTimeout: undefined },
});
jest
.spyOn(openAppDeviceAction, "extractDependencies")
.mockReturnValue(extractDependenciesMock());
const expectedStates: Array = [
+ {
+ status: DeviceActionStatus.Pending, // get onboarding status
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
{
status: DeviceActionStatus.Pending, // get app and version
intermediateValue: {
@@ -140,23 +148,13 @@ describe("OpenAppDeviceAction", () => {
deviceStatus: DeviceStatus.CONNECTED,
currentApp: { name: "BOLOS", version: "0.0.0" },
});
- getAppAndVersionMock
- .mockResolvedValueOnce(
- CommandResultFactory({
- data: {
- name: "BOLOS",
- version: "0.0.0",
- },
- }),
- )
- .mockResolvedValue(
- CommandResultFactory({
- data: {
- name: "Bitcoin",
- version: "1.0.0",
- },
- }),
- );
+
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "Bitcoin",
+ currentAppVersion: "1.0.0",
+ },
+ ]);
openAppMock.mockResolvedValue(CommandResultFactory({ data: undefined }));
@@ -174,12 +172,6 @@ describe("OpenAppDeviceAction", () => {
requiredUserInteraction: UserInteractionRequired.None,
},
},
- {
- status: DeviceActionStatus.Pending, // open app
- intermediateValue: {
- requiredUserInteraction: UserInteractionRequired.ConfirmOpenApp,
- },
- },
{
status: DeviceActionStatus.Pending, // get app and version
intermediateValue: {
@@ -214,23 +206,13 @@ describe("OpenAppDeviceAction", () => {
deviceStatus: DeviceStatus.CONNECTED,
currentApp: { name: "AnotherApp", version: "0.0.0" },
});
- getAppAndVersionMock
- .mockResolvedValueOnce(
- CommandResultFactory({
- data: {
- name: "AnotherApp",
- version: "0.0.0",
- },
- }),
- )
- .mockResolvedValueOnce(
- CommandResultFactory({
- data: {
- name: "Bitcoin",
- version: "1.0.0",
- },
- }),
- );
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "AnotherApp",
+ currentAppVersion: "0.0.0",
+ },
+ { currentApp: "Bitcoin", currentAppVersion: "1.0.0" },
+ ]);
closeAppMock.mockResolvedValue(CommandResultFactory({ data: undefined }));
openAppMock.mockResolvedValue(CommandResultFactory({ data: undefined }));
@@ -254,6 +236,12 @@ describe("OpenAppDeviceAction", () => {
requiredUserInteraction: UserInteractionRequired.None,
},
},
+ {
+ status: DeviceActionStatus.Pending, // get device status
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
{
status: DeviceActionStatus.Pending, // open app
intermediateValue: {
@@ -266,6 +254,12 @@ describe("OpenAppDeviceAction", () => {
requiredUserInteraction: UserInteractionRequired.None,
},
},
+ {
+ status: DeviceActionStatus.Pending, // get app and version
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
{
status: DeviceActionStatus.Completed,
output: undefined,
@@ -328,6 +322,8 @@ describe("OpenAppDeviceAction", () => {
currentApp: { name: "mockedCurrentApp", version: "1.0.0" },
});
+ setupGetDeviceStatusMock([new DeviceLockedError()]);
+
const openAppDeviceAction = new OpenAppDeviceAction({
input: { appName: "Bitcoin" },
});
@@ -337,6 +333,18 @@ describe("OpenAppDeviceAction", () => {
.mockReturnValue(extractDependenciesMock());
const expectedStates: Array = [
+ {
+ status: DeviceActionStatus.Pending,
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
+ {
+ status: DeviceActionStatus.Pending,
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
{
status: DeviceActionStatus.Error,
error: new DeviceLockedError(),
@@ -358,11 +366,7 @@ describe("OpenAppDeviceAction", () => {
currentApp: { name: "mockedCurrentApp", version: "1.0.0" },
});
- getAppAndVersionMock.mockReturnValue(
- CommandResultFactory({
- error: new InvalidStatusWordError("mocked error"),
- }),
- );
+ setupGetDeviceStatusMock([new InvalidStatusWordError("mocked error")]);
const openAppDeviceAction = new OpenAppDeviceAction({
input: { appName: "Bitcoin" },
@@ -379,6 +383,12 @@ describe("OpenAppDeviceAction", () => {
requiredUserInteraction: UserInteractionRequired.None,
},
},
+ {
+ status: DeviceActionStatus.Pending, // get device status
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
{
status: DeviceActionStatus.Error,
error: new InvalidStatusWordError("mocked error"),
@@ -411,6 +421,12 @@ describe("OpenAppDeviceAction", () => {
},
}),
);
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "BOLOS",
+ currentAppVersion: "0.0.0",
+ },
+ ]);
openAppMock.mockResolvedValue(
CommandResultFactory({
error: new InvalidStatusWordError("mocked error"),
@@ -426,7 +442,13 @@ describe("OpenAppDeviceAction", () => {
const expectedStates: Array = [
{
- status: DeviceActionStatus.Pending, // get app and version
+ status: DeviceActionStatus.Pending, // get onboarded status
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
+ {
+ status: DeviceActionStatus.Pending, // get device status
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.None,
},
@@ -457,14 +479,12 @@ describe("OpenAppDeviceAction", () => {
deviceStatus: DeviceStatus.CONNECTED,
currentApp: { name: "AnotherApp", version: "0.0.0" },
});
- getAppAndVersionMock.mockResolvedValue(
- CommandResultFactory({
- data: {
- name: "AnotherApp",
- version: "0.0.0",
- },
- }),
- );
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "AnotherApp",
+ currentAppVersion: "0.0.0",
+ },
+ ]);
closeAppMock.mockResolvedValue(
CommandResultFactory({
error: new InvalidStatusWordError("mocked error"),
@@ -480,7 +500,13 @@ describe("OpenAppDeviceAction", () => {
const expectedStates: Array = [
{
- status: DeviceActionStatus.Pending, // get app and version
+ status: DeviceActionStatus.Pending, // get onboarded status
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
+ {
+ status: DeviceActionStatus.Pending, // get device status
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.None,
},
@@ -511,14 +537,12 @@ describe("OpenAppDeviceAction", () => {
deviceStatus: DeviceStatus.CONNECTED,
currentApp: { name: "AnotherApp", version: "0.0.0" },
});
- getAppAndVersionMock.mockResolvedValue(
- CommandResultFactory({
- data: {
- name: "AnotherApp",
- version: "0.0.0",
- },
- }),
- );
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "AnotherApp",
+ currentAppVersion: "0.0.0",
+ },
+ ]);
closeAppMock.mockResolvedValue(CommandResultFactory({ data: undefined }));
openAppMock.mockResolvedValue(
CommandResultFactory({
@@ -535,7 +559,13 @@ describe("OpenAppDeviceAction", () => {
const expectedStates: Array = [
{
- status: DeviceActionStatus.Pending, // get app and version
+ status: DeviceActionStatus.Pending, // get onboarded status
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
+ {
+ status: DeviceActionStatus.Pending, // get device status
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.None,
},
@@ -573,9 +603,7 @@ describe("OpenAppDeviceAction", () => {
currentApp: { name: "mockedCurrentApp", version: "1.0.0" },
});
- getAppAndVersionMock.mockImplementation(() => {
- throw new UnknownDAError("Unknow error");
- });
+ setupGetDeviceStatusMock([new UnknownDAError("Unknown error")]);
const openAppDeviceAction = new OpenAppDeviceAction({
input: { appName: "Bitcoin" },
@@ -587,14 +615,20 @@ describe("OpenAppDeviceAction", () => {
const expectedStates: Array = [
{
- status: DeviceActionStatus.Pending, // get app and version
+ status: DeviceActionStatus.Pending, // get onboarded status
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
+ {
+ status: DeviceActionStatus.Pending, // get device status
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.None,
},
},
{
status: DeviceActionStatus.Error,
- error: new UnknownDAError("Unknow error"),
+ error: new UnknownDAError("Unknown error"),
},
];
@@ -616,14 +650,12 @@ describe("OpenAppDeviceAction", () => {
},
}),
);
- getAppAndVersionMock.mockResolvedValue(
- CommandResultFactory({
- data: {
- name: "BOLOS",
- version: "0.0.0",
- },
- }),
- );
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "BOLOS",
+ currentAppVersion: "0.0.0",
+ },
+ ]);
openAppMock.mockImplementation(() => {
throw new UnknownDAError("Unknown error");
});
@@ -637,7 +669,13 @@ describe("OpenAppDeviceAction", () => {
const expectedStates: Array = [
{
- status: DeviceActionStatus.Pending, // get app and version
+ status: DeviceActionStatus.Pending, // get device onboarded
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
+ {
+ status: DeviceActionStatus.Pending, // get device status
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.None,
},
@@ -672,14 +710,12 @@ describe("OpenAppDeviceAction", () => {
},
}),
);
- getAppAndVersionMock.mockResolvedValue(
- CommandResultFactory({
- data: {
- name: "anApp",
- version: "0.0.0",
- },
- }),
- );
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "anApp",
+ currentAppVersion: "0.0.0",
+ },
+ ]);
closeAppMock.mockImplementation(() => {
throw new UnknownDAError("Unknown error");
});
@@ -693,7 +729,13 @@ describe("OpenAppDeviceAction", () => {
const expectedStates: Array = [
{
- status: DeviceActionStatus.Pending, // get app and version
+ status: DeviceActionStatus.Pending, // get onboarded status
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
+ {
+ status: DeviceActionStatus.Pending, // get device status
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.None,
},
@@ -725,14 +767,12 @@ describe("OpenAppDeviceAction", () => {
deviceStatus: DeviceStatus.CONNECTED,
currentApp: { name: "AnotherApp", version: "0.0.0" },
});
- getAppAndVersionMock.mockResolvedValue(
- CommandResultFactory({
- data: {
- name: "AnotherApp",
- version: "0.0.0",
- },
- }),
- );
+ setupGetDeviceStatusMock([
+ {
+ currentApp: "AnotherApp",
+ currentAppVersion: "0.0.0",
+ },
+ ]);
const openAppDeviceAction = new OpenAppDeviceAction({
input: { appName: "Bitcoin" },
@@ -743,7 +783,13 @@ describe("OpenAppDeviceAction", () => {
const expectedStates: Array = [
{
- status: DeviceActionStatus.Pending, // get app and version
+ status: DeviceActionStatus.Pending, // get device onboarded
+ intermediateValue: {
+ requiredUserInteraction: UserInteractionRequired.None,
+ },
+ },
+ {
+ status: DeviceActionStatus.Pending, // get device status
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.None,
},
diff --git a/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.ts b/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.ts
similarity index 79%
rename from packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.ts
rename to packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.ts
index f19310a61..8eecb605e 100644
--- a/packages/core/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.ts
+++ b/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction.ts
@@ -4,38 +4,32 @@ import { assign, fromPromise, setup } from "xstate";
import { isSuccessCommandResult } from "@api/command/model/CommandResult";
import {
CloseAppCommand,
- CloseAppCommandResult,
+ type CloseAppCommandResult,
} from "@api/command/os/CloseAppCommand";
-import {
- GetAppAndVersionCommand,
- GetAppAndVersionCommandResult,
-} from "@api/command/os/GetAppAndVersionCommand";
import {
OpenAppCommand,
- OpenAppCommandResult,
+ type OpenAppCommandResult,
} from "@api/command/os/OpenAppCommand";
-import { DeviceStatus } from "@api/device/DeviceStatus";
-import { InternalApi } from "@api/device-action/DeviceAction";
+import { type InternalApi } from "@api/device-action/DeviceAction";
import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
+import { DEFAULT_UNLOCK_TIMEOUT_MS } from "@api/device-action/os/Const";
+import { DeviceNotOnboardedError } from "@api/device-action/os/Errors";
+import { GetDeviceStatusDeviceAction } from "@api/device-action/os/GetDeviceStatus/GetDeviceStatusDeviceAction";
+import { type StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes";
import {
- DeviceLockedError,
- DeviceNotOnboardedError,
-} from "@api/device-action/os/Errors";
-import { StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes";
-import {
- DeviceActionStateMachine,
+ type DeviceActionStateMachine,
XStateDeviceAction,
} from "@api/device-action/xstate-utils/XStateDeviceAction";
import {
- DeviceSessionState,
+ type DeviceSessionState,
DeviceSessionStateType,
} from "@api/device-session/DeviceSessionState";
import {
- OpenAppDAError,
- OpenAppDAInput,
- OpenAppDAIntermediateValue,
- OpenAppDAOutput,
+ type OpenAppDAError,
+ type OpenAppDAInput,
+ type OpenAppDAIntermediateValue,
+ type OpenAppDAOutput,
} from "./types";
type OpenAppStateMachineInternalState = {
@@ -44,7 +38,6 @@ type OpenAppStateMachineInternalState = {
};
export type MachineDependencies = {
- readonly getAppAndVersion: () => Promise;
readonly closeApp: () => Promise;
readonly openApp: (arg0: {
input: { appName: string };
@@ -73,7 +66,7 @@ export type ExtractMachineDependencies = (
* appName: "MyApp",
* },
* });
- * sdk.executeDeviceAction({ sessionId: "mySessionId", deviceAction });
+ * dmk.executeDeviceAction({ sessionId: "mySessionId", deviceAction });
* ```
*/
export class OpenAppDeviceAction extends XStateDeviceAction<
@@ -101,14 +94,21 @@ export class OpenAppDeviceAction extends XStateDeviceAction<
>;
const {
- getAppAndVersion,
closeApp,
openApp,
getDeviceSessionState,
- setDeviceSessionState,
isDeviceOnboarded,
+ setDeviceSessionState,
} = this.extractDependencies(internalApi);
+ const unlockTimeout = this.input.unlockTimeout ?? DEFAULT_UNLOCK_TIMEOUT_MS;
+
+ const getDeviceStatusMachine = new GetDeviceStatusDeviceAction({
+ input: {
+ unlockTimeout,
+ },
+ }).makeStateMachine(internalApi);
+
return setup({
types: {
input: {} as types["input"],
@@ -116,14 +116,12 @@ export class OpenAppDeviceAction extends XStateDeviceAction<
output: {} as types["output"],
},
actors: {
- getAppAndVersion: fromPromise(getAppAndVersion),
closeApp: fromPromise(closeApp),
openApp: fromPromise(openApp),
+ getDeviceStatus: getDeviceStatusMachine,
},
guards: {
isDeviceOnboarded: () => isDeviceOnboarded(), // TODO: we don't have this info for now, this can be derived from the "flags" obtained in the getVersion command
- isDeviceUnlocked: () =>
- getDeviceSessionState().deviceStatus !== DeviceStatus.LOCKED,
isRequestedAppOpen: ({ context }: { context: types["context"] }) => {
if (context._internalState.currentlyRunningApp === null)
throw new Error("context.currentlyRunningApp === null");
@@ -145,15 +143,6 @@ export class OpenAppDeviceAction extends XStateDeviceAction<
error: new DeviceNotOnboardedError(),
}),
}),
- assignErrorDeviceLocked: assign({
- _internalState: (_) => ({
- ..._.context._internalState,
- error: new DeviceLockedError(),
- }),
- intermediateValue: {
- requiredUserInteraction: UserInteractionRequired.UnlockDevice,
- },
- }),
assignUserActionNeededOpenApp: assign({
intermediateValue: (_) =>
({
@@ -215,7 +204,7 @@ export class OpenAppDeviceAction extends XStateDeviceAction<
// check onboarding status provided by device session
always: [
{
- target: "LockingCheck",
+ target: "GetDeviceStatus",
guard: {
type: "isDeviceOnboarded",
},
@@ -227,80 +216,77 @@ export class OpenAppDeviceAction extends XStateDeviceAction<
],
},
- LockingCheck: {
- // check locking status provided by device session
- always: [
- {
- target: "ApplicationAvailable",
- guard: "isDeviceUnlocked",
- },
- {
- target: "Error",
- actions: "assignErrorDeviceLocked",
- },
- ],
- },
-
- ApplicationAvailable: {
- // execute getAppAndVersion command
+ GetDeviceStatus: {
+ // We run the GetDeviceStatus flow to get information about the device state
invoke: {
- src: "getAppAndVersion",
+ id: "deviceStatus",
+ src: "getDeviceStatus",
+ input: (_) => ({
+ unlockTimeout: _.context.input.unlockTimeout,
+ }),
+ onSnapshot: {
+ actions: assign({
+ intermediateValue: (_) =>
+ _.event.snapshot.context.intermediateValue,
+ }),
+ },
onDone: {
- target: "ApplicationAvailableResultCheck",
+ target: "CheckDeviceStatus",
actions: assign({
_internalState: (_) => {
- if (isSuccessCommandResult(_.event.output)) {
- const state: DeviceSessionState = getDeviceSessionState();
- // Narrow the type to ReadyWithoutSecureChannelState or ReadyWithSecureChannelState
- if (
- state.sessionStateType !==
- DeviceSessionStateType.Connected
- ) {
- setDeviceSessionState({
- ...state,
- currentApp: _.event.output.data,
- });
- }
- return {
- ..._.context._internalState,
- currentlyRunningApp: _.event.output.data.name,
- };
- } else {
- return {
- ..._.context._internalState,
- error: _.event.output.error,
- };
- }
+ return _.event.output.caseOf(
+ {
+ Right: (output) => {
+ const state: DeviceSessionState =
+ getDeviceSessionState();
+
+ if (
+ state.sessionStateType !==
+ DeviceSessionStateType.Connected
+ ) {
+ setDeviceSessionState({
+ ...state,
+ currentApp: {
+ name: output.currentApp,
+ version: output.currentAppVersion,
+ },
+ });
+ }
+ return {
+ ..._.context._internalState,
+ currentlyRunningApp: output.currentApp,
+ };
+ },
+ Left: (error) => ({
+ ..._.context._internalState,
+ error,
+ }),
+ },
+ );
},
}),
},
- onError: {
- target: "Error",
- actions: "assignErrorFromEvent",
- },
+ // NOTE: The way we handle our DeviceActions means that we should never as we return an Either
+ // onError: {
+ // target: "Error",
+ // actions: "assignGetDeviceStatusUnknownError",
+ // },
},
},
-
- ApplicationAvailableResultCheck: {
+ CheckDeviceStatus: {
+ // We check the device status to see if we can have an error
always: [
{
target: "Error",
guard: "hasError",
},
- {
- target: "ApplicationCheck",
- },
- ],
- },
-
- ApplicationCheck: {
- // Is the current application the requested one
- always: [
{
target: "ApplicationReady",
guard: "isRequestedAppOpen",
},
- "DashboardCheck",
+ {
+ target: "DashboardCheck",
+ },
],
},
@@ -315,17 +301,21 @@ export class OpenAppDeviceAction extends XStateDeviceAction<
],
},
- CloseApplication: {
+ OpenApplication: {
+ // execute openApp command,
+ entry: "assignUserActionNeededOpenApp",
+ exit: "assignNoUserActionNeeded",
invoke: {
- src: "closeApp",
+ src: "openApp",
+ input: ({ context }) => ({ appName: context.input.appName }),
onDone: {
- target: "CloseApplicationResultCheck",
+ target: "OpenApplicationResultCheck",
actions: assign({
_internalState: (_) => {
if (isSuccessCommandResult(_.event.output)) {
return {
..._.context._internalState,
- currentlyRunningApp: "BOLOS",
+ currentlyRunningApp: _.context.input.appName,
};
} else {
return {
@@ -342,31 +332,28 @@ export class OpenAppDeviceAction extends XStateDeviceAction<
},
},
},
- CloseApplicationResultCheck: {
+
+ OpenApplicationResultCheck: {
always: [
{
target: "Error",
guard: "hasError",
},
- { target: "OpenApplication" },
+ { target: "GetDeviceStatus" },
],
},
- OpenApplication: {
- // execute openApp command,
- entry: "assignUserActionNeededOpenApp",
- exit: "assignNoUserActionNeeded",
+ CloseApplication: {
invoke: {
- src: "openApp",
- input: ({ context }) => ({ appName: context.input.appName }),
+ src: "closeApp",
onDone: {
- target: "OpenApplicationResultCheck",
+ target: "CloseApplicationResultCheck",
actions: assign({
_internalState: (_) => {
if (isSuccessCommandResult(_.event.output)) {
return {
..._.context._internalState,
- currentlyRunningApp: _.context.input.appName,
+ currentlyRunningApp: "BOLOS",
};
} else {
return {
@@ -383,14 +370,24 @@ export class OpenAppDeviceAction extends XStateDeviceAction<
},
},
},
-
- OpenApplicationResultCheck: {
+ CloseApplicationResultCheck: {
always: [
{
target: "Error",
guard: "hasError",
},
- { target: "ApplicationAvailable" },
+ { target: "OpenApplication" },
+ ],
+ },
+
+ ApplicationCheck: {
+ // Is the current application the requested one
+ always: [
+ {
+ target: "ApplicationReady",
+ guard: "isRequestedAppOpen",
+ },
+ "DashboardCheck",
],
},
@@ -418,8 +415,6 @@ export class OpenAppDeviceAction extends XStateDeviceAction<
}
extractDependencies(internalApi: InternalApi): MachineDependencies {
- const getAppAndVersion = async () =>
- internalApi.sendCommand(new GetAppAndVersionCommand());
const closeApp = async () => internalApi.sendCommand(new CloseAppCommand());
const openApp = async (arg0: { input: { appName: string } }) =>
internalApi.sendCommand(
@@ -427,7 +422,6 @@ export class OpenAppDeviceAction extends XStateDeviceAction<
);
return {
- getAppAndVersion,
closeApp,
openApp,
getDeviceSessionState: () => internalApi.getDeviceSessionState(),
diff --git a/packages/core/src/api/device-action/os/OpenAppDeviceAction/types.ts b/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/types.ts
similarity index 54%
rename from packages/core/src/api/device-action/os/OpenAppDeviceAction/types.ts
rename to packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/types.ts
index 0c0843625..5b1f3d60f 100644
--- a/packages/core/src/api/device-action/os/OpenAppDeviceAction/types.ts
+++ b/packages/device-management-kit/src/api/device-action/os/OpenAppDeviceAction/types.ts
@@ -1,16 +1,17 @@
-import { CommandErrorResult } from "@api/command/model/CommandResult";
-import { OpenAppErrorCodes } from "@api/command/os/OpenAppCommand";
-import { DeviceActionState } from "@api/device-action/model/DeviceActionState";
-import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
+import { type CommandErrorResult } from "@api/command/model/CommandResult";
+import { type OpenAppErrorCodes } from "@api/command/os/OpenAppCommand";
+import { type DeviceActionState } from "@api/device-action/model/DeviceActionState";
+import { type UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
import {
- DeviceLockedError,
- DeviceNotOnboardedError,
- UnknownDAError,
+ type DeviceLockedError,
+ type DeviceNotOnboardedError,
+ type UnknownDAError,
} from "@api/device-action/os/Errors";
+import { type GetDeviceStatusDAInput } from "@api/device-action/os/GetDeviceStatus/types";
export type OpenAppDAOutput = void;
-export type OpenAppDAInput = {
+export type OpenAppDAInput = GetDeviceStatusDAInput & {
readonly appName: string;
};
diff --git a/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts b/packages/device-management-kit/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts
similarity index 97%
rename from packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts
rename to packages/device-management-kit/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts
index 173c634a7..b4b0f1205 100644
--- a/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts
+++ b/packages/device-management-kit/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.test.ts
@@ -1,26 +1,26 @@
import { Left, Right } from "purify-ts";
import { assign, createMachine } from "xstate";
-import { Apdu } from "@api/apdu/model/Apdu";
+import { type Apdu } from "@api/apdu/model/Apdu";
import { ApduBuilder } from "@api/apdu/utils/ApduBuilder";
import { CommandResultFactory } from "@api/command/model/CommandResult";
import { makeDeviceActionInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi";
import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates";
import {
- DeviceActionState,
+ type DeviceActionState,
DeviceActionStatus,
} from "@api/device-action/model/DeviceActionState";
import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
import { UnknownDAError } from "@api/device-action/os/Errors";
import { OpenAppDeviceAction } from "@api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction";
-import { Command } from "@api/types";
+import { type Command } from "@api/types";
import { UnknownDeviceExchangeError } from "@root/src";
import { SendCommandInAppDeviceAction } from "./SendCommandInAppDeviceAction";
import {
- SendCommandInAppDAError,
- SendCommandInAppDAIntermediateValue,
- SendCommandInAppDAOutput,
+ type SendCommandInAppDAError,
+ type SendCommandInAppDAIntermediateValue,
+ type SendCommandInAppDAOutput,
} from "./SendCommandInAppDeviceActionTypes";
jest.mock(
diff --git a/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.ts b/packages/device-management-kit/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.ts
similarity index 94%
rename from packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.ts
rename to packages/device-management-kit/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.ts
index 4034bf193..809a1f4b6 100644
--- a/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.ts
+++ b/packages/device-management-kit/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceAction.ts
@@ -2,26 +2,26 @@ import { Left, Right } from "purify-ts";
import { assign, fromPromise, setup } from "xstate";
import {
- CommandResult,
+ type CommandResult,
isSuccessCommandResult,
} from "@api/command/model/CommandResult";
-import { InternalApi } from "@api/device-action/DeviceAction";
+import { type InternalApi } from "@api/device-action/DeviceAction";
import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
import { UnknownDAError } from "@api/device-action/os/Errors";
import { OpenAppDeviceAction } from "@api/device-action/os/OpenAppDeviceAction/OpenAppDeviceAction";
-import { StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes";
+import { type StateMachineTypes } from "@api/device-action/xstate-utils/StateMachineTypes";
import {
- DeviceActionStateMachine,
+ type DeviceActionStateMachine,
XStateDeviceAction,
} from "@api/device-action/xstate-utils/XStateDeviceAction";
-import { Command } from "@api/types";
+import { type Command } from "@api/types";
import {
- SendCommandInAppDAError,
- SendCommandInAppDAInput,
- SendCommandInAppDAIntermediateValue,
- SendCommandInAppDAInternalState,
- SendCommandInAppDAOutput,
+ type SendCommandInAppDAError,
+ type SendCommandInAppDAInput,
+ type SendCommandInAppDAIntermediateValue,
+ type SendCommandInAppDAInternalState,
+ type SendCommandInAppDAOutput,
} from "./SendCommandInAppDeviceActionTypes";
/**
@@ -47,7 +47,7 @@ import {
* requiredUserInteraction: UserInteractionRequired.None,
* },
* });
- * sdk.executeDeviceAction({ sessionId: "mySessionId", deviceAction });
+ * dmk.executeDeviceAction({ sessionId: "mySessionId", deviceAction });
* ```
*/
export class SendCommandInAppDeviceAction<
diff --git a/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceActionTypes.ts b/packages/device-management-kit/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceActionTypes.ts
similarity index 84%
rename from packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceActionTypes.ts
rename to packages/device-management-kit/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceActionTypes.ts
index 2781a0c93..efe5483a2 100644
--- a/packages/core/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceActionTypes.ts
+++ b/packages/device-management-kit/src/api/device-action/os/SendCommandInAppDeviceAction/SendCommandInAppDeviceActionTypes.ts
@@ -1,9 +1,9 @@
-import { CommandErrorResult } from "@api/command/model/CommandResult";
+import { type CommandErrorResult } from "@api/command/model/CommandResult";
import {
- OpenAppDAError,
- OpenAppDAIntermediateValue,
+ type OpenAppDAError,
+ type OpenAppDAIntermediateValue,
} from "@api/device-action/os/OpenAppDeviceAction/types";
-import { Command } from "@api/types";
+import { type Command } from "@api/types";
export type SendCommandInAppDAOutput = CommandResponse;
diff --git a/packages/core/src/api/device-action/use-case/ExecuteDeviceActionUseCase.ts b/packages/device-management-kit/src/api/device-action/use-case/ExecuteDeviceActionUseCase.ts
similarity index 96%
rename from packages/core/src/api/device-action/use-case/ExecuteDeviceActionUseCase.ts
rename to packages/device-management-kit/src/api/device-action/use-case/ExecuteDeviceActionUseCase.ts
index 76bc7d724..475fbad5e 100644
--- a/packages/core/src/api/device-action/use-case/ExecuteDeviceActionUseCase.ts
+++ b/packages/device-management-kit/src/api/device-action/use-case/ExecuteDeviceActionUseCase.ts
@@ -5,7 +5,7 @@ import {
DeviceActionIntermediateValue,
ExecuteDeviceActionReturnType,
} from "@api/device-action/DeviceAction";
-import { SdkError } from "@api/Error";
+import { DmkError } from "@api/Error";
import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes";
import type { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes";
@@ -14,7 +14,7 @@ import { LoggerPublisherService } from "@internal/logger-publisher/service/Logge
export type ExecuteDeviceActionUseCaseArgs<
Output,
Input,
- Error extends SdkError,
+ Error extends DmkError,
IntermediateValue extends DeviceActionIntermediateValue,
> = {
/**
@@ -53,7 +53,7 @@ export class ExecuteDeviceActionUseCase {
*/
execute<
Output,
- Error extends SdkError,
+ Error extends DmkError,
IntermediateValue extends DeviceActionIntermediateValue,
Input,
>({
diff --git a/packages/core/src/api/device-action/xstate-utils/StateMachineTypes.ts b/packages/device-management-kit/src/api/device-action/xstate-utils/StateMachineTypes.ts
similarity index 92%
rename from packages/core/src/api/device-action/xstate-utils/StateMachineTypes.ts
rename to packages/device-management-kit/src/api/device-action/xstate-utils/StateMachineTypes.ts
index c148f5d4f..fe287637e 100644
--- a/packages/core/src/api/device-action/xstate-utils/StateMachineTypes.ts
+++ b/packages/device-management-kit/src/api/device-action/xstate-utils/StateMachineTypes.ts
@@ -1,4 +1,4 @@
-import { Either } from "purify-ts";
+import { type Either } from "purify-ts";
/**
* The internal types of an XState state machine.
diff --git a/packages/core/src/api/device-action/xstate-utils/XStateDeviceAction.ts b/packages/device-management-kit/src/api/device-action/xstate-utils/XStateDeviceAction.ts
similarity index 92%
rename from packages/core/src/api/device-action/xstate-utils/XStateDeviceAction.ts
rename to packages/device-management-kit/src/api/device-action/xstate-utils/XStateDeviceAction.ts
index b26c3d877..1e784b3f5 100644
--- a/packages/core/src/api/device-action/xstate-utils/XStateDeviceAction.ts
+++ b/packages/device-management-kit/src/api/device-action/xstate-utils/XStateDeviceAction.ts
@@ -1,25 +1,30 @@
import { createBrowserInspector } from "@statelyai/inspect";
import { Observable, ReplaySubject, share } from "rxjs";
-import { createActor, SnapshotFrom, StateMachine, StateSchema } from "xstate";
+import {
+ createActor,
+ type SnapshotFrom,
+ type StateMachine,
+ type StateSchema,
+} from "xstate";
import {
- DeviceAction,
- DeviceActionIntermediateValue,
- ExecuteDeviceActionReturnType,
- InternalApi,
+ type DeviceAction,
+ type DeviceActionIntermediateValue,
+ type ExecuteDeviceActionReturnType,
+ type InternalApi,
} from "@api/device-action/DeviceAction";
import {
- DeviceActionState,
+ type DeviceActionState,
DeviceActionStatus,
} from "@api/device-action/model/DeviceActionState";
-import { SdkError } from "@api/Error";
+import { type DmkError } from "@api/Error";
-import { StateMachineTypes } from "./StateMachineTypes";
+import { type StateMachineTypes } from "./StateMachineTypes";
export type DeviceActionStateMachine<
Output,
Input,
- Error extends SdkError,
+ Error extends DmkError,
IntermediateValue extends DeviceActionIntermediateValue,
InternalState,
> = StateMachine<
@@ -73,7 +78,7 @@ export type DeviceActionStateMachine<
export abstract class XStateDeviceAction<
Output,
Input,
- Error extends SdkError,
+ Error extends DmkError,
IntermediateValue extends DeviceActionIntermediateValue,
InternalState,
> implements DeviceAction
diff --git a/packages/core/src/api/device-session/ApduResponse.stub.ts b/packages/device-management-kit/src/api/device-session/ApduResponse.stub.ts
similarity index 78%
rename from packages/core/src/api/device-session/ApduResponse.stub.ts
rename to packages/device-management-kit/src/api/device-session/ApduResponse.stub.ts
index 2b22a8502..f6c11229f 100644
--- a/packages/core/src/api/device-session/ApduResponse.stub.ts
+++ b/packages/device-management-kit/src/api/device-session/ApduResponse.stub.ts
@@ -1,4 +1,4 @@
-import { ApduResponse, ApduResponseConstructorArgs } from "./ApduResponse";
+import { ApduResponse, type ApduResponseConstructorArgs } from "./ApduResponse";
type ApduResponseStub = (
props?: Partial,
diff --git a/packages/core/src/api/device-session/ApduResponse.ts b/packages/device-management-kit/src/api/device-session/ApduResponse.ts
similarity index 100%
rename from packages/core/src/api/device-session/ApduResponse.ts
rename to packages/device-management-kit/src/api/device-session/ApduResponse.ts
diff --git a/packages/core/src/api/device-session/DeviceSessionState.ts b/packages/device-management-kit/src/api/device-session/DeviceSessionState.ts
similarity index 89%
rename from packages/core/src/api/device-session/DeviceSessionState.ts
rename to packages/device-management-kit/src/api/device-session/DeviceSessionState.ts
index a2a020fd4..dd57f7807 100644
--- a/packages/core/src/api/device-session/DeviceSessionState.ts
+++ b/packages/device-management-kit/src/api/device-session/DeviceSessionState.ts
@@ -1,7 +1,7 @@
-import { GetAppAndVersionResponse } from "@api/command/os/GetAppAndVersionCommand";
-import { BatteryStatusFlags } from "@api/command/os/GetBatteryStatusCommand";
-import { DeviceStatus } from "@api/device/DeviceStatus";
-import { Application } from "@internal/manager-api/model/ManagerApiType";
+import { type GetAppAndVersionResponse } from "@api/command/os/GetAppAndVersionCommand";
+import { type BatteryStatusFlags } from "@api/command/os/GetBatteryStatusCommand";
+import { type DeviceStatus } from "@api/device/DeviceStatus";
+import { type Application } from "@internal/manager-api/model/ManagerApiType";
/**
* The battery status of a device.
diff --git a/packages/core/src/api/device-session/types.ts b/packages/device-management-kit/src/api/device-session/types.ts
similarity index 100%
rename from packages/core/src/api/device-session/types.ts
rename to packages/device-management-kit/src/api/device-session/types.ts
diff --git a/packages/core/src/api/device/DeviceModel.ts b/packages/device-management-kit/src/api/device/DeviceModel.ts
similarity index 100%
rename from packages/core/src/api/device/DeviceModel.ts
rename to packages/device-management-kit/src/api/device/DeviceModel.ts
diff --git a/packages/core/src/api/device/DeviceStatus.ts b/packages/device-management-kit/src/api/device/DeviceStatus.ts
similarity index 100%
rename from packages/core/src/api/device/DeviceStatus.ts
rename to packages/device-management-kit/src/api/device/DeviceStatus.ts
diff --git a/packages/core/src/api/discovery/ConnectionType.ts b/packages/device-management-kit/src/api/discovery/ConnectionType.ts
similarity index 100%
rename from packages/core/src/api/discovery/ConnectionType.ts
rename to packages/device-management-kit/src/api/discovery/ConnectionType.ts
diff --git a/packages/core/src/api/index.ts b/packages/device-management-kit/src/api/index.ts
similarity index 92%
rename from packages/core/src/api/index.ts
rename to packages/device-management-kit/src/api/index.ts
index d78f4d06b..1de6aa487 100644
--- a/packages/core/src/api/index.ts
+++ b/packages/device-management-kit/src/api/index.ts
@@ -16,6 +16,7 @@ export {
type GetAppAndVersionResponse,
} from "./command/os/GetAppAndVersionCommand";
export {
+ BatteryStatusType,
type GetBatteryStatusArgs,
GetBatteryStatusCommand,
type GetBatteryStatusResponse,
@@ -40,14 +41,16 @@ export {
export { DeviceModel, DeviceModelId } from "./device/DeviceModel";
export { DeviceStatus } from "./device/DeviceStatus";
export { ApduResponse } from "./device-session/ApduResponse";
-export { DeviceSdk } from "./DeviceSdk";
-export { LedgerDeviceSdkBuilder as DeviceSdkBuilder } from "./DeviceSdkBuilder";
+export { DeviceManagementKit } from "./DeviceManagementKit";
+export { DeviceManagementKitBuilder } from "./DeviceManagementKitBuilder";
export { DeviceExchangeError, UnknownDeviceExchangeError } from "./Error";
export { LogLevel } from "./logger-subscriber/model/LogLevel";
export { ConsoleLogger } from "./logger-subscriber/service/ConsoleLogger";
export { WebLogsExporterLogger } from "./logger-subscriber/service/WebLogsExporterLogger";
+export { ConnectedDevice } from "./transport/model/ConnectedDevice";
+export { BuiltinTransports } from "./transport/model/TransportIdentifier";
export * from "./types";
-export { ConnectedDevice } from "./usb/model/ConnectedDevice";
+export * from "@api/apdu/utils/AppBuilderError";
export { InvalidStatusWordError } from "@api/command/Errors";
export {
type DeviceAction,
@@ -118,7 +121,7 @@ export {
type DeviceSessionState,
DeviceSessionStateType,
} from "@api/device-session/DeviceSessionState";
-export { type SdkError } from "@api/Error";
+export { type DmkError } from "@api/Error";
export { base64StringToBuffer, isBase64String } from "@api/utils/Base64String";
export {
bufferToHexaString,
diff --git a/packages/core/src/api/logger-subscriber/__mocks__/ConsoleLogger.ts b/packages/device-management-kit/src/api/logger-subscriber/__mocks__/ConsoleLogger.ts
similarity index 100%
rename from packages/core/src/api/logger-subscriber/__mocks__/ConsoleLogger.ts
rename to packages/device-management-kit/src/api/logger-subscriber/__mocks__/ConsoleLogger.ts
diff --git a/packages/core/src/api/logger-subscriber/model/LogLevel.ts b/packages/device-management-kit/src/api/logger-subscriber/model/LogLevel.ts
similarity index 100%
rename from packages/core/src/api/logger-subscriber/model/LogLevel.ts
rename to packages/device-management-kit/src/api/logger-subscriber/model/LogLevel.ts
diff --git a/packages/core/src/api/logger-subscriber/model/LogSubscriberOptions.ts b/packages/device-management-kit/src/api/logger-subscriber/model/LogSubscriberOptions.ts
similarity index 100%
rename from packages/core/src/api/logger-subscriber/model/LogSubscriberOptions.ts
rename to packages/device-management-kit/src/api/logger-subscriber/model/LogSubscriberOptions.ts
diff --git a/packages/core/src/api/logger-subscriber/service/ConsoleLogger.test.ts b/packages/device-management-kit/src/api/logger-subscriber/service/ConsoleLogger.test.ts
similarity index 98%
rename from packages/core/src/api/logger-subscriber/service/ConsoleLogger.test.ts
rename to packages/device-management-kit/src/api/logger-subscriber/service/ConsoleLogger.test.ts
index 068fc8436..d581efcd8 100644
--- a/packages/core/src/api/logger-subscriber/service/ConsoleLogger.test.ts
+++ b/packages/device-management-kit/src/api/logger-subscriber/service/ConsoleLogger.test.ts
@@ -1,5 +1,5 @@
import { LogLevel } from "@api/logger-subscriber/model/LogLevel";
-import { LogSubscriberOptions } from "@api/logger-subscriber/model/LogSubscriberOptions";
+import { type LogSubscriberOptions } from "@api/logger-subscriber/model/LogSubscriberOptions";
import { ConsoleLogger } from "./ConsoleLogger";
diff --git a/packages/core/src/api/logger-subscriber/service/ConsoleLogger.ts b/packages/device-management-kit/src/api/logger-subscriber/service/ConsoleLogger.ts
similarity index 86%
rename from packages/core/src/api/logger-subscriber/service/ConsoleLogger.ts
rename to packages/device-management-kit/src/api/logger-subscriber/service/ConsoleLogger.ts
index fe0b48242..269b9db78 100644
--- a/packages/core/src/api/logger-subscriber/service/ConsoleLogger.ts
+++ b/packages/device-management-kit/src/api/logger-subscriber/service/ConsoleLogger.ts
@@ -1,6 +1,6 @@
import { LogLevel } from "@api/logger-subscriber/model/LogLevel";
-import { LogSubscriberOptions } from "@api/logger-subscriber/model/LogSubscriberOptions";
-import { LoggerSubscriberService } from "@api/logger-subscriber/service/LoggerSubscriberService";
+import { type LogSubscriberOptions } from "@api/logger-subscriber/model/LogSubscriberOptions";
+import { type LoggerSubscriberService } from "@api/logger-subscriber/service/LoggerSubscriberService";
export class ConsoleLogger implements LoggerSubscriberService {
private readonly maxLevel: LogLevel;
diff --git a/packages/device-management-kit/src/api/logger-subscriber/service/LoggerSubscriberService.ts b/packages/device-management-kit/src/api/logger-subscriber/service/LoggerSubscriberService.ts
new file mode 100644
index 000000000..0002b7a6e
--- /dev/null
+++ b/packages/device-management-kit/src/api/logger-subscriber/service/LoggerSubscriberService.ts
@@ -0,0 +1,18 @@
+import { type LogLevel } from "@api/logger-subscriber/model/LogLevel";
+import { type LogSubscriberOptions } from "@api/logger-subscriber/model/LogSubscriberOptions";
+
+/**
+ * Logger subscriber service.
+ *
+ * Implement this interface and use `DeviceManagementKitBuilder.addLogger` to
+ * receive logs from the SDK.
+ */
+export type LogParams = [
+ level: LogLevel,
+ message: string,
+ options: LogSubscriberOptions,
+];
+
+export interface LoggerSubscriberService {
+ log(...logParams: LogParams): void;
+}
diff --git a/packages/core/src/api/logger-subscriber/service/WebLogsExporterLogger.test.ts b/packages/device-management-kit/src/api/logger-subscriber/service/WebLogsExporterLogger.test.ts
similarity index 81%
rename from packages/core/src/api/logger-subscriber/service/WebLogsExporterLogger.test.ts
rename to packages/device-management-kit/src/api/logger-subscriber/service/WebLogsExporterLogger.test.ts
index 0a813282c..33e59736f 100644
--- a/packages/core/src/api/logger-subscriber/service/WebLogsExporterLogger.test.ts
+++ b/packages/device-management-kit/src/api/logger-subscriber/service/WebLogsExporterLogger.test.ts
@@ -1,7 +1,8 @@
-import { ConnectionType } from "@api/discovery/ConnectionType";
+import { BuiltinTransports } from "@api/transport/model/TransportIdentifier";
import { deviceModelStubBuilder } from "@internal/device-model/model/DeviceModel.stub";
import { DeviceSession } from "@internal/device-session/model/DeviceSession";
-import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
+import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
+import { type InternalConnectedDevice } from "@internal/transport/model/InternalConnectedDevice";
import { getJSONStringifyReplacer } from "./WebLogsExporterLogger";
@@ -21,11 +22,12 @@ describe("getJSONStringifyReplacer", () => {
const stubDeviceModel = deviceModelStubBuilder();
const replacer = getJSONStringifyReplacer();
- const connectedDevice = {
+ const connectedDevice: InternalConnectedDevice = {
deviceModel: deviceModelStubBuilder(),
- type: "USB" as ConnectionType,
+ type: "USB",
id: "mockedDeviceId",
sendApdu: jest.fn(),
+ transport: BuiltinTransports.USB,
};
const value = new DeviceSession(
@@ -40,6 +42,7 @@ describe("getJSONStringifyReplacer", () => {
const expected = `{"id":"mockedSessionId","connectedDevice":{"deviceModel":${JSON.stringify(
stubDeviceModel,
)},"type":"USB","id":"mockedDeviceId"}}`;
+ value.close();
expect(result).toEqual(expected);
});
diff --git a/packages/core/src/api/logger-subscriber/service/WebLogsExporterLogger.ts b/packages/device-management-kit/src/api/logger-subscriber/service/WebLogsExporterLogger.ts
similarity index 95%
rename from packages/core/src/api/logger-subscriber/service/WebLogsExporterLogger.ts
rename to packages/device-management-kit/src/api/logger-subscriber/service/WebLogsExporterLogger.ts
index 851c3ff86..3568ed4e2 100644
--- a/packages/core/src/api/logger-subscriber/service/WebLogsExporterLogger.ts
+++ b/packages/device-management-kit/src/api/logger-subscriber/service/WebLogsExporterLogger.ts
@@ -1,8 +1,8 @@
import { LogLevel } from "@api/logger-subscriber/model/LogLevel";
-import { LogSubscriberOptions } from "@api/types";
+import { type LogSubscriberOptions } from "@api/types";
import { DeviceSession } from "@internal/device-session/model/DeviceSession";
-import { LoggerSubscriberService } from "./LoggerSubscriberService";
+import { type LoggerSubscriberService } from "./LoggerSubscriberService";
/**
* This function is used to format the logs to JSON format,
diff --git a/packages/core/src/api/usb/model/ConnectedDevice.ts b/packages/device-management-kit/src/api/transport/model/ConnectedDevice.ts
similarity index 57%
rename from packages/core/src/api/usb/model/ConnectedDevice.ts
rename to packages/device-management-kit/src/api/transport/model/ConnectedDevice.ts
index 55aa0fae3..695736b37 100644
--- a/packages/core/src/api/usb/model/ConnectedDevice.ts
+++ b/packages/device-management-kit/src/api/transport/model/ConnectedDevice.ts
@@ -1,13 +1,16 @@
-import { DeviceId, DeviceModelId } from "@api/device/DeviceModel";
-import { ConnectionType } from "@api/discovery/ConnectionType";
-import { InternalConnectedDevice } from "@internal/usb/model/InternalConnectedDevice";
+import { type DeviceId, type DeviceModelId } from "@api/device/DeviceModel";
+import { type ConnectionType } from "@api/discovery/ConnectionType";
+import { type DeviceSessionId } from "@api/types";
+import { type InternalConnectedDevice } from "@internal/transport/model/InternalConnectedDevice";
type ConnectedDeviceConstructorArgs = {
readonly internalConnectedDevice: InternalConnectedDevice;
+ readonly sessionId: DeviceSessionId;
};
export class ConnectedDevice {
public readonly id: DeviceId;
+ public readonly sessionId: DeviceSessionId;
public readonly modelId: DeviceModelId;
public readonly name: string;
public readonly type: ConnectionType;
@@ -18,8 +21,10 @@ export class ConnectedDevice {
deviceModel: { id: deviceModelId, productName: deviceName },
type,
},
+ sessionId,
}: ConnectedDeviceConstructorArgs) {
this.id = id;
+ this.sessionId = sessionId;
this.modelId = deviceModelId;
this.name = deviceName;
this.type = type;
diff --git a/packages/device-management-kit/src/api/transport/model/DiscoveredDevice.ts b/packages/device-management-kit/src/api/transport/model/DiscoveredDevice.ts
new file mode 100644
index 000000000..651e883dc
--- /dev/null
+++ b/packages/device-management-kit/src/api/transport/model/DiscoveredDevice.ts
@@ -0,0 +1,11 @@
+import { type DeviceId, type DeviceModel } from "@api/device/DeviceModel";
+import { type TransportIdentifier } from "@api/transport/model/TransportIdentifier";
+
+/**
+ * A discovered device.
+ */
+export type DiscoveredDevice = {
+ readonly id: DeviceId;
+ readonly deviceModel: DeviceModel;
+ readonly transport: TransportIdentifier;
+};
diff --git a/packages/device-management-kit/src/api/transport/model/Transport.ts b/packages/device-management-kit/src/api/transport/model/Transport.ts
new file mode 100644
index 000000000..8a3ff8225
--- /dev/null
+++ b/packages/device-management-kit/src/api/transport/model/Transport.ts
@@ -0,0 +1,44 @@
+import { type Either } from "purify-ts";
+import { type Observable } from "rxjs";
+
+import { type DeviceId } from "@api/device/DeviceModel";
+import { type DmkError } from "@api/Error";
+import { type TransportIdentifier } from "@api/transport/model/TransportIdentifier";
+import { type ConnectError } from "@internal/transport/model/Errors";
+import { type InternalConnectedDevice } from "@internal/transport/model/InternalConnectedDevice";
+import { type InternalDiscoveredDevice } from "@internal/transport/model/InternalDiscoveredDevice";
+
+export type DisconnectHandler = (deviceId: DeviceId) => void;
+
+/**
+ * Transport interface
+ */
+export interface Transport {
+ /**
+ * Get the transport identifier, which is a string to uniquely identify that transport.
+ */
+ getIdentifier(): TransportIdentifier;
+
+ isSupported(): boolean;
+
+ startDiscovering(): Observable;
+
+ stopDiscovering(): void;
+
+ listenToKnownDevices(): Observable;
+
+ /**
+ * Enables communication with the device by connecting to it.
+ *
+ * @param params containing
+ * - id: the device id from the DTO discovered device
+ */
+ connect(params: {
+ deviceId: DeviceId;
+ onDisconnect: DisconnectHandler;
+ }): Promise>;
+
+ disconnect(params: {
+ connectedDevice: InternalConnectedDevice;
+ }): Promise>;
+}
diff --git a/packages/device-management-kit/src/api/transport/model/TransportIdentifier.ts b/packages/device-management-kit/src/api/transport/model/TransportIdentifier.ts
new file mode 100644
index 000000000..597d93e86
--- /dev/null
+++ b/packages/device-management-kit/src/api/transport/model/TransportIdentifier.ts
@@ -0,0 +1,7 @@
+export type TransportIdentifier = string;
+
+export enum BuiltinTransports {
+ USB = "USB",
+ BLE = "BLE",
+ MOCK_SERVER = "MOCK_SERVER",
+}
diff --git a/packages/core/src/api/types.ts b/packages/device-management-kit/src/api/types.ts
similarity index 66%
rename from packages/core/src/api/types.ts
rename to packages/device-management-kit/src/api/types.ts
index 2ee306005..8cab9bb3a 100644
--- a/packages/core/src/api/types.ts
+++ b/packages/device-management-kit/src/api/types.ts
@@ -4,8 +4,13 @@ export type { DeviceId } from "./device/DeviceModel";
export type { ConnectionType } from "./discovery/ConnectionType";
export type { CommandErrorArgs } from "./Error";
export type { LogSubscriberOptions } from "./logger-subscriber/model/LogSubscriberOptions";
-export type { LoggerSubscriberService } from "./logger-subscriber/service/LoggerSubscriberService";
-export type { DiscoveredDevice } from "./usb/model/DiscoveredDevice";
+export type {
+ LoggerSubscriberService,
+ LogParams,
+} from "./logger-subscriber/service/LoggerSubscriberService";
+export type { DiscoveredDevice } from "./transport/model/DiscoveredDevice";
+export type { Transport } from "./transport/model/Transport";
+export type { TransportIdentifier } from "./transport/model/TransportIdentifier";
export type { ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
export type { Command } from "@api/command/Command";
export type {
@@ -15,8 +20,11 @@ export type {
export type { SendCommandUseCaseArgs } from "@api/command/use-case/SendCommandUseCase";
export type { DeviceModelId } from "@api/device/DeviceModel";
export type { ExecuteDeviceActionUseCaseArgs } from "@api/device-action/use-case/ExecuteDeviceActionUseCase";
+export type { DeviceSessionState } from "@api/device-session/DeviceSessionState";
export type { DeviceSessionId } from "@api/device-session/types";
export type { HexaString } from "@api/utils/HexaString";
export type { ConnectUseCaseArgs } from "@internal/discovery/use-case/ConnectUseCase";
export type { DisconnectUseCaseArgs } from "@internal/discovery/use-case/DisconnectUseCase";
+export type { GetConnectedDeviceUseCaseArgs } from "@internal/discovery/use-case/GetConnectedDeviceUseCase";
+export type { StartDiscoveringUseCaseArgs } from "@internal/discovery/use-case/StartDiscoveringUseCase";
export type { SendApduUseCaseArgs } from "@internal/send/use-case/SendApduUseCase";
diff --git a/packages/core/src/api/utils/Base64String.test.ts b/packages/device-management-kit/src/api/utils/Base64String.test.ts
similarity index 100%
rename from packages/core/src/api/utils/Base64String.test.ts
rename to packages/device-management-kit/src/api/utils/Base64String.test.ts
diff --git a/packages/core/src/api/utils/Base64String.ts b/packages/device-management-kit/src/api/utils/Base64String.ts
similarity index 100%
rename from packages/core/src/api/utils/Base64String.ts
rename to packages/device-management-kit/src/api/utils/Base64String.ts
diff --git a/packages/core/src/api/utils/HexaString.test.ts b/packages/device-management-kit/src/api/utils/HexaString.test.ts
similarity index 100%
rename from packages/core/src/api/utils/HexaString.test.ts
rename to packages/device-management-kit/src/api/utils/HexaString.test.ts
diff --git a/packages/core/src/api/utils/HexaString.ts b/packages/device-management-kit/src/api/utils/HexaString.ts
similarity index 100%
rename from packages/core/src/api/utils/HexaString.ts
rename to packages/device-management-kit/src/api/utils/HexaString.ts
diff --git a/packages/core/src/di.stub.ts b/packages/device-management-kit/src/di.stub.ts
similarity index 100%
rename from packages/core/src/di.stub.ts
rename to packages/device-management-kit/src/di.stub.ts
diff --git a/packages/core/src/di.ts b/packages/device-management-kit/src/di.ts
similarity index 58%
rename from packages/core/src/di.ts
rename to packages/device-management-kit/src/di.ts
index 25e5595c5..d4c85a931 100644
--- a/packages/core/src/di.ts
+++ b/packages/device-management-kit/src/di.ts
@@ -4,32 +4,48 @@ import { Container } from "inversify";
// import { makeLoggerMiddleware } from "inversify-logger-middleware";
import { commandModuleFactory } from "@api/command/di/commandModule";
import { deviceActionModuleFactory } from "@api/device-action/di/deviceActionModule";
-import { LoggerSubscriberService } from "@api/logger-subscriber/service/LoggerSubscriberService";
-import { SdkConfig } from "@api/SdkConfig";
+// Uncomment this line to enable the logger middleware
+// import { makeLoggerMiddleware } from "inversify-logger-middleware";
+import { type DmkConfig } from "@api/DmkConfig";
+import { type LoggerSubscriberService } from "@api/logger-subscriber/service/LoggerSubscriberService";
+import { type Transport } from "@api/transport/model/Transport";
+import { type BuiltinTransports } from "@api/transport/model/TransportIdentifier";
import { configModuleFactory } from "@internal/config/di/configModule";
import { deviceModelModuleFactory } from "@internal/device-model/di/deviceModelModule";
import { deviceSessionModuleFactory } from "@internal/device-session/di/deviceSessionModule";
import { discoveryModuleFactory } from "@internal/discovery/di/discoveryModule";
import { loggerModuleFactory } from "@internal/logger-publisher/di/loggerModule";
import { managerApiModuleFactory } from "@internal/manager-api/di/managerApiModule";
-import { DEFAULT_MANAGER_API_BASE_URL } from "@internal/manager-api/model/Const";
+import {
+ DEFAULT_MANAGER_API_BASE_URL,
+ DEFAULT_MOCK_SERVER_BASE_URL,
+} from "@internal/manager-api/model/Const";
import { sendModuleFactory } from "@internal/send/di/sendModule";
-import { usbModuleFactory } from "@internal/usb/di/usbModule";
+import { transportModuleFactory } from "@internal/transport//di/transportModule";
+import { bleModuleFactory } from "@internal/transport/ble/di/bleModule";
+import { usbModuleFactory } from "@internal/transport/usb/di/usbModule";
// Uncomment this line to enable the logger middleware
// const logger = makeLoggerMiddleware();
export type MakeContainerProps = {
- stub?: boolean;
- loggers?: LoggerSubscriberService[];
- config: SdkConfig;
+ stub: boolean;
+ transports: BuiltinTransports[];
+ customTransports: Transport[];
+ loggers: LoggerSubscriberService[];
+ config: DmkConfig;
};
export const makeContainer = ({
stub = false,
+ transports = [],
+ customTransports = [],
loggers = [],
- config = { managerApiUrl: DEFAULT_MANAGER_API_BASE_URL },
-}: MakeContainerProps) => {
+ config = {
+ managerApiUrl: DEFAULT_MANAGER_API_BASE_URL,
+ mockUrl: DEFAULT_MOCK_SERVER_BASE_URL,
+ },
+}: Partial) => {
const container = new Container();
// Uncomment this line to enable the logger middleware
@@ -38,6 +54,7 @@ export const makeContainer = ({
container.load(
configModuleFactory({ stub }),
deviceModelModuleFactory({ stub }),
+ transportModuleFactory({ stub, transports, customTransports, config }),
usbModuleFactory({ stub }),
managerApiModuleFactory({ stub, config }),
discoveryModuleFactory({ stub }),
@@ -46,6 +63,7 @@ export const makeContainer = ({
sendModuleFactory({ stub }),
commandModuleFactory({ stub }),
deviceActionModuleFactory({ stub }),
+ bleModuleFactory(),
// modules go here
);
diff --git a/packages/core/src/index.ts b/packages/device-management-kit/src/index.ts
similarity index 100%
rename from packages/core/src/index.ts
rename to packages/device-management-kit/src/index.ts
diff --git a/packages/core/src/internal/config/data/ConfigDataSource.ts b/packages/device-management-kit/src/internal/config/data/ConfigDataSource.ts
similarity index 67%
rename from packages/core/src/internal/config/data/ConfigDataSource.ts
rename to packages/device-management-kit/src/internal/config/data/ConfigDataSource.ts
index f2e830f58..41fb7314e 100644
--- a/packages/core/src/internal/config/data/ConfigDataSource.ts
+++ b/packages/device-management-kit/src/internal/config/data/ConfigDataSource.ts
@@ -1,9 +1,9 @@
-import { Either } from "purify-ts";
+import { type Either } from "purify-ts";
-import { Config } from "@internal/config/model/Config";
+import { type Config } from "@internal/config/model/Config";
import {
- LocalConfigFailure,
- RemoteConfigFailure,
+ type LocalConfigFailure,
+ type RemoteConfigFailure,
} from "@internal/config/model/Errors";
// Describe the different data sources interfaces our application could have
diff --git a/packages/core/src/internal/config/data/Dto.ts b/packages/device-management-kit/src/internal/config/data/Dto.ts
similarity index 100%
rename from packages/core/src/internal/config/data/Dto.ts
rename to packages/device-management-kit/src/internal/config/data/Dto.ts
diff --git a/packages/core/src/internal/config/data/LocalConfigDataSource.stub.ts b/packages/device-management-kit/src/internal/config/data/LocalConfigDataSource.stub.ts
similarity index 100%
rename from packages/core/src/internal/config/data/LocalConfigDataSource.stub.ts
rename to packages/device-management-kit/src/internal/config/data/LocalConfigDataSource.stub.ts
diff --git a/packages/core/src/internal/config/data/LocalConfigDataSource.test.ts b/packages/device-management-kit/src/internal/config/data/LocalConfigDataSource.test.ts
similarity index 94%
rename from packages/core/src/internal/config/data/LocalConfigDataSource.test.ts
rename to packages/device-management-kit/src/internal/config/data/LocalConfigDataSource.test.ts
index 2da291e69..ad0bc7589 100644
--- a/packages/core/src/internal/config/data/LocalConfigDataSource.test.ts
+++ b/packages/device-management-kit/src/internal/config/data/LocalConfigDataSource.test.ts
@@ -1,8 +1,9 @@
import { Either, Left } from "purify-ts";
import { JSONParseError, ReadFileError } from "@internal/config/model/Errors";
+import pkg from "@root/package.json";
-import { LocalConfigDataSource } from "./ConfigDataSource";
+import { type LocalConfigDataSource } from "./ConfigDataSource";
import * as LocalConfig from "./LocalConfigDataSource";
const { FileLocalConfigDataSource } = LocalConfig;
@@ -70,7 +71,7 @@ describe("LocalConfigDataSource", () => {
expect(LocalConfig.stubFsReadFile()).toEqual(
JSON.stringify({
name: "@ledgerhq/device-management-kit",
- version: "0.3.0",
+ version: pkg.version,
}),
);
});
diff --git a/packages/core/src/internal/config/data/LocalConfigDataSource.ts b/packages/device-management-kit/src/internal/config/data/LocalConfigDataSource.ts
similarity index 100%
rename from packages/core/src/internal/config/data/LocalConfigDataSource.ts
rename to packages/device-management-kit/src/internal/config/data/LocalConfigDataSource.ts
diff --git a/packages/core/src/internal/config/data/RemoteConfigDataSource.stub.ts b/packages/device-management-kit/src/internal/config/data/RemoteConfigDataSource.stub.ts
similarity index 100%
rename from packages/core/src/internal/config/data/RemoteConfigDataSource.stub.ts
rename to packages/device-management-kit/src/internal/config/data/RemoteConfigDataSource.stub.ts
diff --git a/packages/core/src/internal/config/data/RemoteConfigDataSource.test.ts b/packages/device-management-kit/src/internal/config/data/RemoteConfigDataSource.test.ts
similarity index 98%
rename from packages/core/src/internal/config/data/RemoteConfigDataSource.test.ts
rename to packages/device-management-kit/src/internal/config/data/RemoteConfigDataSource.test.ts
index 556cc244d..2ecfe434a 100644
--- a/packages/core/src/internal/config/data/RemoteConfigDataSource.test.ts
+++ b/packages/device-management-kit/src/internal/config/data/RemoteConfigDataSource.test.ts
@@ -6,7 +6,7 @@ import {
ParseResponseError,
} from "@internal/config/model/Errors";
-import { RemoteConfigDataSource } from "./ConfigDataSource";
+import { type RemoteConfigDataSource } from "./ConfigDataSource";
import { RestRemoteConfigDataSource } from "./RemoteConfigDataSource";
let datasource: RemoteConfigDataSource;
diff --git a/packages/core/src/internal/config/data/RemoteConfigDataSource.ts b/packages/device-management-kit/src/internal/config/data/RemoteConfigDataSource.ts
similarity index 100%
rename from packages/core/src/internal/config/data/RemoteConfigDataSource.ts
rename to packages/device-management-kit/src/internal/config/data/RemoteConfigDataSource.ts
diff --git a/packages/core/src/internal/config/data/__mocks__/LocalConfigDataSource.ts b/packages/device-management-kit/src/internal/config/data/__mocks__/LocalConfigDataSource.ts
similarity index 54%
rename from packages/core/src/internal/config/data/__mocks__/LocalConfigDataSource.ts
rename to packages/device-management-kit/src/internal/config/data/__mocks__/LocalConfigDataSource.ts
index 123123c42..9c939ab3c 100644
--- a/packages/core/src/internal/config/data/__mocks__/LocalConfigDataSource.ts
+++ b/packages/device-management-kit/src/internal/config/data/__mocks__/LocalConfigDataSource.ts
@@ -1,4 +1,4 @@
-import { LocalConfigDataSource } from "@internal/config/data/ConfigDataSource";
+import { type LocalConfigDataSource } from "@internal/config/data/ConfigDataSource";
export class FileLocalConfigDataSource implements LocalConfigDataSource {
getConfig = jest.fn();
diff --git a/packages/core/src/internal/config/data/__mocks__/RemoteConfigDataSource.ts b/packages/device-management-kit/src/internal/config/data/__mocks__/RemoteConfigDataSource.ts
similarity index 64%
rename from packages/core/src/internal/config/data/__mocks__/RemoteConfigDataSource.ts
rename to packages/device-management-kit/src/internal/config/data/__mocks__/RemoteConfigDataSource.ts
index 92d2744dd..aaa51b131 100644
--- a/packages/core/src/internal/config/data/__mocks__/RemoteConfigDataSource.ts
+++ b/packages/device-management-kit/src/internal/config/data/__mocks__/RemoteConfigDataSource.ts
@@ -1,4 +1,4 @@
-import { RemoteConfigDataSource } from "@internal/config/data/ConfigDataSource";
+import { type RemoteConfigDataSource } from "@internal/config/data/ConfigDataSource";
export class RestRemoteConfigDataSource implements RemoteConfigDataSource {
getConfig = jest.fn();
diff --git a/packages/core/src/internal/config/di/configModule.test.ts b/packages/device-management-kit/src/internal/config/di/configModule.test.ts
similarity index 100%
rename from packages/core/src/internal/config/di/configModule.test.ts
rename to packages/device-management-kit/src/internal/config/di/configModule.test.ts
diff --git a/packages/core/src/internal/config/di/configModule.ts b/packages/device-management-kit/src/internal/config/di/configModule.ts
similarity index 89%
rename from packages/core/src/internal/config/di/configModule.ts
rename to packages/device-management-kit/src/internal/config/di/configModule.ts
index e749a2a52..01ec9be87 100644
--- a/packages/core/src/internal/config/di/configModule.ts
+++ b/packages/device-management-kit/src/internal/config/di/configModule.ts
@@ -5,7 +5,7 @@ import { StubLocalConfigDataSource } from "@internal/config/data/LocalConfigData
import { RestRemoteConfigDataSource } from "@internal/config/data/RemoteConfigDataSource";
import { StubRemoteConfigDataSource } from "@internal/config/data/RemoteConfigDataSource.stub";
import { DefaultConfigService } from "@internal/config/service/DefaultConfigService";
-import { GetSdkVersionUseCase } from "@internal/config/use-case/GetSdkVersionUseCase";
+import { GetDmkVersionUseCase } from "@internal/config/use-case/GetDmkVersionUseCase";
import { configTypes } from "./configTypes";
@@ -19,7 +19,7 @@ export const configModuleFactory = ({ stub }: FactoryProps) =>
new ContainerModule((bind, _unbind, _isBound, rebind) => {
bind(configTypes.LocalConfigDataSource).to(FileLocalConfigDataSource);
bind(configTypes.RemoteConfigDataSource).to(RestRemoteConfigDataSource);
- bind(configTypes.GetSdkVersionUseCase).to(GetSdkVersionUseCase);
+ bind(configTypes.GetDmkVersionUseCase).to(GetDmkVersionUseCase);
bind(configTypes.ConfigService).to(DefaultConfigService);
if (stub) {
diff --git a/packages/core/src/internal/config/di/configTypes.ts b/packages/device-management-kit/src/internal/config/di/configTypes.ts
similarity index 77%
rename from packages/core/src/internal/config/di/configTypes.ts
rename to packages/device-management-kit/src/internal/config/di/configTypes.ts
index ffe10b732..896543d13 100644
--- a/packages/core/src/internal/config/di/configTypes.ts
+++ b/packages/device-management-kit/src/internal/config/di/configTypes.ts
@@ -2,5 +2,5 @@ export const configTypes = {
LocalConfigDataSource: Symbol.for("LocalConfigDataSource"),
RemoteConfigDataSource: Symbol.for("RemoteConfigDataSource"),
ConfigService: Symbol.for("ConfigService"),
- GetSdkVersionUseCase: Symbol.for("GetSdkVersionUseCase"),
+ GetDmkVersionUseCase: Symbol.for("GetDmkVersionUseCase"),
};
diff --git a/packages/core/src/internal/config/model/Config.ts b/packages/device-management-kit/src/internal/config/model/Config.ts
similarity index 100%
rename from packages/core/src/internal/config/model/Config.ts
rename to packages/device-management-kit/src/internal/config/model/Config.ts
diff --git a/packages/core/src/internal/config/model/Errors.ts b/packages/device-management-kit/src/internal/config/model/Errors.ts
similarity index 100%
rename from packages/core/src/internal/config/model/Errors.ts
rename to packages/device-management-kit/src/internal/config/model/Errors.ts
diff --git a/packages/device-management-kit/src/internal/config/service/ConfigService.ts b/packages/device-management-kit/src/internal/config/service/ConfigService.ts
new file mode 100644
index 000000000..060249a57
--- /dev/null
+++ b/packages/device-management-kit/src/internal/config/service/ConfigService.ts
@@ -0,0 +1,5 @@
+import { type Config } from "@internal/config/model/Config";
+
+export interface ConfigService {
+ getDmkConfig(): Promise;
+}
diff --git a/packages/core/src/internal/config/service/DefaultConfigService.test.ts b/packages/device-management-kit/src/internal/config/service/DefaultConfigService.test.ts
similarity index 90%
rename from packages/core/src/internal/config/service/DefaultConfigService.test.ts
rename to packages/device-management-kit/src/internal/config/service/DefaultConfigService.test.ts
index da040907f..380163786 100644
--- a/packages/core/src/internal/config/service/DefaultConfigService.test.ts
+++ b/packages/device-management-kit/src/internal/config/service/DefaultConfigService.test.ts
@@ -5,7 +5,7 @@ import { RestRemoteConfigDataSource } from "@internal/config/data/RemoteConfigDa
import { JSONParseError } from "@internal/config/model/Errors";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
-import { ConfigService } from "./ConfigService";
+import { type ConfigService } from "./ConfigService";
import { DefaultConfigService } from "./DefaultConfigService";
jest.mock("@internal/config/data/LocalConfigDataSource");
@@ -45,7 +45,7 @@ describe("DefaultConfigService", () => {
}),
);
- expect(await service.getSdkConfig()).toStrictEqual({
+ expect(await service.getDmkConfig()).toStrictEqual({
name: "DeviceSDK",
version: "1.0.0-local",
});
@@ -62,7 +62,7 @@ describe("DefaultConfigService", () => {
}),
);
- expect(await service.getSdkConfig()).toStrictEqual({
+ expect(await service.getDmkConfig()).toStrictEqual({
name: "DeviceSDK",
version: "1.0.0-remote",
});
@@ -74,8 +74,8 @@ describe("DefaultConfigService", () => {
localDataSource.getConfig.mockReturnValue(Left(new JSONParseError()));
remoteDataSource.getConfig.mockResolvedValue(Left(new JSONParseError()));
- expect(await service.getSdkConfig()).toStrictEqual({
- name: "DeadSdk",
+ expect(await service.getDmkConfig()).toStrictEqual({
+ name: "DeadDmk",
version: "0.0.0-dead.1",
});
});
diff --git a/packages/core/src/internal/config/service/DefaultConfigService.ts b/packages/device-management-kit/src/internal/config/service/DefaultConfigService.ts
similarity index 94%
rename from packages/core/src/internal/config/service/DefaultConfigService.ts
rename to packages/device-management-kit/src/internal/config/service/DefaultConfigService.ts
index 4fcdb79b8..0c538e2c7 100644
--- a/packages/core/src/internal/config/service/DefaultConfigService.ts
+++ b/packages/device-management-kit/src/internal/config/service/DefaultConfigService.ts
@@ -31,7 +31,7 @@ export class DefaultConfigService implements ConfigService {
this._logger = loggerServiceFactory("config");
}
- async getSdkConfig(): Promise {
+ async getDmkConfig(): Promise {
// Returns an Either
const localConfig = this._local
.getConfig()
@@ -52,7 +52,7 @@ export class DefaultConfigService implements ConfigService {
.mapLeft((error: RemoteConfigFailure) => {
// Here we handle the error and return a default value
this._logger.error("Local config available", { data: { error } });
- return { name: "DeadSdk", version: "0.0.0-dead.1" };
+ return { name: "DeadDmk", version: "0.0.0-dead.1" };
})
.extract();
});
diff --git a/packages/device-management-kit/src/internal/config/use-case/GetDmkVersionUseCase.test.ts b/packages/device-management-kit/src/internal/config/use-case/GetDmkVersionUseCase.test.ts
new file mode 100644
index 000000000..0cae5a8c8
--- /dev/null
+++ b/packages/device-management-kit/src/internal/config/use-case/GetDmkVersionUseCase.test.ts
@@ -0,0 +1,23 @@
+import { GetDmkVersionUseCase } from "./GetDmkVersionUseCase";
+
+const getDmkConfigMock = jest.fn();
+
+let usecase: GetDmkVersionUseCase;
+describe("GetDmkVersionUseCase", () => {
+ beforeEach(() => {
+ getDmkConfigMock.mockClear();
+ const configService = {
+ getDmkConfig: getDmkConfigMock,
+ };
+
+ usecase = new GetDmkVersionUseCase(configService);
+ });
+
+ it("should return the dmk version", async () => {
+ getDmkConfigMock.mockResolvedValue({
+ name: "DeviceSDK",
+ version: "1.0.0",
+ });
+ expect(await usecase.getDmkVersion()).toBe("1.0.0");
+ });
+});
diff --git a/packages/core/src/internal/config/use-case/GetSdkVersionUseCase.ts b/packages/device-management-kit/src/internal/config/use-case/GetDmkVersionUseCase.ts
similarity index 58%
rename from packages/core/src/internal/config/use-case/GetSdkVersionUseCase.ts
rename to packages/device-management-kit/src/internal/config/use-case/GetDmkVersionUseCase.ts
index 8157707df..0da9cb69c 100644
--- a/packages/core/src/internal/config/use-case/GetSdkVersionUseCase.ts
+++ b/packages/device-management-kit/src/internal/config/use-case/GetDmkVersionUseCase.ts
@@ -4,17 +4,17 @@ import { configTypes } from "@internal/config/di/configTypes";
import type { ConfigService } from "@internal/config/service/ConfigService";
/**
- * class GetSDKVersionUseCase
- * This is our actual use case that our SDK will use.
- * We will have many use cases in our SDK, and each should be contained in its own file.
+ * class GetDmkVersionUseCase
+ * This is our actual use case that our DMK will use.
+ * We will have many use cases in our DMK, and each should be contained in its own file.
*/
@injectable()
-export class GetSdkVersionUseCase {
+export class GetDmkVersionUseCase {
private _configService: ConfigService;
constructor(@inject(configTypes.ConfigService) configService: ConfigService) {
this._configService = configService;
}
- async getSdkVersion(): Promise {
- return (await this._configService.getSdkConfig()).version;
+ async getDmkVersion(): Promise {
+ return (await this._configService.getDmkConfig()).version;
}
}
diff --git a/packages/device-management-kit/src/internal/device-model/data/DeviceModelDataSource.ts b/packages/device-management-kit/src/internal/device-model/data/DeviceModelDataSource.ts
new file mode 100644
index 000000000..8d0d9001e
--- /dev/null
+++ b/packages/device-management-kit/src/internal/device-model/data/DeviceModelDataSource.ts
@@ -0,0 +1,20 @@
+import { type DeviceModelId } from "@api/device/DeviceModel";
+import { type InternalDeviceModel } from "@internal/device-model/model/DeviceModel";
+import { type BleDeviceInfos } from "@internal/transport/ble/model/BleDeviceInfos";
+
+/**
+ * Source of truth for the device models
+ */
+export interface DeviceModelDataSource {
+ getAllDeviceModels(): InternalDeviceModel[];
+
+ getDeviceModel(params: { id: DeviceModelId }): InternalDeviceModel;
+
+ filterDeviceModels(
+ params: Partial,
+ ): InternalDeviceModel[];
+
+ getBluetoothServicesInfos(): Record;
+
+ getBluetoothServices(): string[];
+}
diff --git a/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts b/packages/device-management-kit/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts
similarity index 71%
rename from packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts
rename to packages/device-management-kit/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts
index 360c7cf36..b450b1fd5 100644
--- a/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts
+++ b/packages/device-management-kit/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts
@@ -1,4 +1,5 @@
import { DeviceModelId } from "@api/device/DeviceModel";
+import { BleDeviceInfos } from "@internal/transport/ble/model/BleDeviceInfos";
import { StaticDeviceModelDataSource } from "./StaticDeviceModelDataSource";
@@ -149,4 +150,50 @@ describe("StaticDeviceModelDataSource", () => {
expect(deviceModels3.length).toEqual(0);
});
});
+ describe("getBluetoothServicesInfos", () => {
+ it("should return the ble service infos record", () => {
+ // given
+ const dataSource = new StaticDeviceModelDataSource();
+ // when
+ const bleServiceInfos = dataSource.getBluetoothServicesInfos();
+ // then
+ expect(bleServiceInfos).toStrictEqual({
+ "13d63400-2c97-0004-0000-4c6564676572": new BleDeviceInfos(
+ dataSource.getDeviceModel({ id: DeviceModelId.NANO_X }),
+ "13d63400-2c97-0004-0000-4c6564676572",
+ "13d63400-2c97-0004-0002-4c6564676572",
+ "13d63400-2c97-0004-0003-4c6564676572",
+ "13d63400-2c97-0004-0001-4c6564676572",
+ ),
+ "13d63400-2c97-6004-0000-4c6564676572": new BleDeviceInfos(
+ dataSource.getDeviceModel({ id: DeviceModelId.STAX }),
+ "13d63400-2c97-6004-0000-4c6564676572",
+ "13d63400-2c97-6004-0002-4c6564676572",
+ "13d63400-2c97-6004-0003-4c6564676572",
+ "13d63400-2c97-6004-0001-4c6564676572",
+ ),
+ "13d63400-2c97-3004-0000-4c6564676572": new BleDeviceInfos(
+ dataSource.getDeviceModel({ id: DeviceModelId.FLEX }),
+ "13d63400-2c97-3004-0000-4c6564676572",
+ "13d63400-2c97-3004-0002-4c6564676572",
+ "13d63400-2c97-3004-0003-4c6564676572",
+ "13d63400-2c97-3004-0001-4c6564676572",
+ ),
+ });
+ });
+ });
+ describe("getBluetoothServices", () => {
+ it("should return the bluetooth services", () => {
+ // given
+ const dataSource = new StaticDeviceModelDataSource();
+ // when
+ const bleServices = dataSource.getBluetoothServices();
+ // then
+ expect(bleServices).toStrictEqual([
+ "13d63400-2c97-0004-0000-4c6564676572",
+ "13d63400-2c97-6004-0000-4c6564676572",
+ "13d63400-2c97-3004-0000-4c6564676572",
+ ]);
+ });
+ });
});
diff --git a/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.ts b/packages/device-management-kit/src/internal/device-model/data/StaticDeviceModelDataSource.ts
similarity index 74%
rename from packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.ts
rename to packages/device-management-kit/src/internal/device-model/data/StaticDeviceModelDataSource.ts
index ae148a06e..57b7a9d86 100644
--- a/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.ts
+++ b/packages/device-management-kit/src/internal/device-model/data/StaticDeviceModelDataSource.ts
@@ -2,6 +2,7 @@ import { injectable } from "inversify";
import { DeviceModelId } from "@api/device/DeviceModel";
import { InternalDeviceModel } from "@internal/device-model/model/DeviceModel";
+import { BleDeviceInfos } from "@internal/transport/ble/model/BleDeviceInfos";
import { DeviceModelDataSource } from "./DeviceModelDataSource";
@@ -104,4 +105,40 @@ export class StaticDeviceModelDataSource implements DeviceModelDataSource {
});
});
}
+
+ getBluetoothServicesInfos(): Record {
+ return Object.values(StaticDeviceModelDataSource.deviceModelByIds).reduce<
+ Record
+ >((acc, deviceModel) => {
+ const { bluetoothSpec } = deviceModel;
+ if (bluetoothSpec) {
+ return {
+ ...acc,
+ ...bluetoothSpec.reduce>(
+ (serviceToModel, bleSpec) => ({
+ ...serviceToModel,
+ [bleSpec.serviceUuid]: new BleDeviceInfos(
+ deviceModel,
+ bleSpec.serviceUuid,
+ bleSpec.writeUuid,
+ bleSpec.writeCmdUuid,
+ bleSpec.notifyUuid,
+ ),
+ }),
+ {},
+ ),
+ };
+ }
+ return acc;
+ }, {});
+ }
+
+ getBluetoothServices(): string[] {
+ return Object.values(StaticDeviceModelDataSource.deviceModelByIds)
+ .map((deviceModel) =>
+ (deviceModel.bluetoothSpec || []).map((spec) => spec.serviceUuid),
+ )
+ .flat()
+ .filter((uuid) => !!uuid);
+ }
}
diff --git a/packages/core/src/internal/device-model/di/deviceModelModule.test.ts b/packages/device-management-kit/src/internal/device-model/di/deviceModelModule.test.ts
similarity index 100%
rename from packages/core/src/internal/device-model/di/deviceModelModule.test.ts
rename to packages/device-management-kit/src/internal/device-model/di/deviceModelModule.test.ts
diff --git a/packages/core/src/internal/device-model/di/deviceModelModule.ts b/packages/device-management-kit/src/internal/device-model/di/deviceModelModule.ts
similarity index 100%
rename from packages/core/src/internal/device-model/di/deviceModelModule.ts
rename to packages/device-management-kit/src/internal/device-model/di/deviceModelModule.ts
diff --git a/packages/core/src/internal/device-model/di/deviceModelTypes.ts b/packages/device-management-kit/src/internal/device-model/di/deviceModelTypes.ts
similarity index 100%
rename from packages/core/src/internal/device-model/di/deviceModelTypes.ts
rename to packages/device-management-kit/src/internal/device-model/di/deviceModelTypes.ts
diff --git a/packages/core/src/internal/device-model/model/DeviceModel.stub.ts b/packages/device-management-kit/src/internal/device-model/model/DeviceModel.stub.ts
similarity index 89%
rename from packages/core/src/internal/device-model/model/DeviceModel.stub.ts
rename to packages/device-management-kit/src/internal/device-model/model/DeviceModel.stub.ts
index 1d12ab403..66ef91958 100644
--- a/packages/core/src/internal/device-model/model/DeviceModel.stub.ts
+++ b/packages/device-management-kit/src/internal/device-model/model/DeviceModel.stub.ts
@@ -1,5 +1,5 @@
import { DeviceModelId } from "@api/device/DeviceModel";
-import { InternalDeviceModel } from "@internal/device-model/model/DeviceModel";
+import { type InternalDeviceModel } from "@internal/device-model/model/DeviceModel";
export function deviceModelStubBuilder(
props: Partial = {},
diff --git a/packages/core/src/internal/device-model/model/DeviceModel.test.ts b/packages/device-management-kit/src/internal/device-model/model/DeviceModel.test.ts
similarity index 100%
rename from packages/core/src/internal/device-model/model/DeviceModel.test.ts
rename to packages/device-management-kit/src/internal/device-model/model/DeviceModel.test.ts
diff --git a/packages/core/src/internal/device-model/model/DeviceModel.ts b/packages/device-management-kit/src/internal/device-model/model/DeviceModel.ts
similarity index 100%
rename from packages/core/src/internal/device-model/model/DeviceModel.ts
rename to packages/device-management-kit/src/internal/device-model/model/DeviceModel.ts
diff --git a/packages/core/src/internal/device-session/data/ApduResponseConst.ts b/packages/device-management-kit/src/internal/device-session/data/ApduResponseConst.ts
similarity index 100%
rename from packages/core/src/internal/device-session/data/ApduResponseConst.ts
rename to packages/device-management-kit/src/internal/device-session/data/ApduResponseConst.ts
diff --git a/packages/device-management-kit/src/internal/device-session/data/DeviceSessionRefresherConst.ts b/packages/device-management-kit/src/internal/device-session/data/DeviceSessionRefresherConst.ts
new file mode 100644
index 000000000..f2d987c23
--- /dev/null
+++ b/packages/device-management-kit/src/internal/device-session/data/DeviceSessionRefresherConst.ts
@@ -0,0 +1 @@
+export const DEVICE_SESSION_REFRESH_INTERVAL = 1000;
diff --git a/packages/core/src/internal/device-session/data/FramerConst.ts b/packages/device-management-kit/src/internal/device-session/data/FramerConst.ts
similarity index 100%
rename from packages/core/src/internal/device-session/data/FramerConst.ts
rename to packages/device-management-kit/src/internal/device-session/data/FramerConst.ts
diff --git a/packages/core/src/internal/device-session/di/deviceSessionModule.test.ts b/packages/device-management-kit/src/internal/device-session/di/deviceSessionModule.test.ts
similarity index 100%
rename from packages/core/src/internal/device-session/di/deviceSessionModule.test.ts
rename to packages/device-management-kit/src/internal/device-session/di/deviceSessionModule.test.ts
diff --git a/packages/core/src/internal/device-session/di/deviceSessionModule.ts b/packages/device-management-kit/src/internal/device-session/di/deviceSessionModule.ts
similarity index 73%
rename from packages/core/src/internal/device-session/di/deviceSessionModule.ts
rename to packages/device-management-kit/src/internal/device-session/di/deviceSessionModule.ts
index ea0ae2b66..6f99a9838 100644
--- a/packages/core/src/internal/device-session/di/deviceSessionModule.ts
+++ b/packages/device-management-kit/src/internal/device-session/di/deviceSessionModule.ts
@@ -1,20 +1,20 @@
-import { ContainerModule, interfaces } from "inversify";
+import { ContainerModule, type interfaces } from "inversify";
-import { ApduReceiverService } from "@internal/device-session/service/ApduReceiverService";
-import { ApduSenderService } from "@internal/device-session/service/ApduSenderService";
+import { type ApduReceiverService } from "@internal/device-session/service/ApduReceiverService";
+import { type ApduSenderService } from "@internal/device-session/service/ApduSenderService";
import {
- DefaultApduReceiverConstructorArgs,
+ type DefaultApduReceiverConstructorArgs,
DefaultApduReceiverService,
} from "@internal/device-session/service/DefaultApduReceiverService";
import {
DefaultApduSenderService,
- DefaultApduSenderServiceConstructorArgs,
+ type DefaultApduSenderServiceConstructorArgs,
} from "@internal/device-session/service/DefaultApduSenderService";
import { DefaultDeviceSessionService } from "@internal/device-session/service/DefaultDeviceSessionService";
+import { CloseSessionsUseCase } from "@internal/device-session/use-case/CloseSessionsUseCase";
import { GetDeviceSessionStateUseCase } from "@internal/device-session/use-case/GetDeviceSessionStateUseCase";
-import { ListDeviceSessionsUseCase } from "@internal/device-session/use-case/ListDeviceSessionsUseCase";
import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import { StubUseCase } from "@root/src/di.stub";
import { deviceSessionTypes } from "./deviceSessionTypes";
@@ -55,7 +55,7 @@ export const deviceSessionModuleFactory = (
(name: string) => LoggerPublisherService
>(loggerTypes.LoggerPublisherServiceFactory);
- return (args: DefaultApduReceiverConstructorArgs) => {
+ return (args: DefaultApduReceiverConstructorArgs = {}) => {
return new DefaultApduReceiverService(args, logger);
};
});
@@ -67,14 +67,10 @@ export const deviceSessionModuleFactory = (
bind(deviceSessionTypes.GetDeviceSessionStateUseCase).to(
GetDeviceSessionStateUseCase,
);
-
- bind(deviceSessionTypes.ListDeviceSessionsUseCase).to(
- ListDeviceSessionsUseCase,
- );
+ bind(deviceSessionTypes.CloseSessionsUseCase).to(CloseSessionsUseCase);
if (stub) {
rebind(deviceSessionTypes.GetDeviceSessionStateUseCase).to(StubUseCase);
- rebind(deviceSessionTypes.ListDeviceSessionsUseCase).to(StubUseCase);
}
},
);
diff --git a/packages/core/src/internal/device-session/di/deviceSessionTypes.ts b/packages/device-management-kit/src/internal/device-session/di/deviceSessionTypes.ts
similarity index 81%
rename from packages/core/src/internal/device-session/di/deviceSessionTypes.ts
rename to packages/device-management-kit/src/internal/device-session/di/deviceSessionTypes.ts
index 13d7faeec..16934e974 100644
--- a/packages/core/src/internal/device-session/di/deviceSessionTypes.ts
+++ b/packages/device-management-kit/src/internal/device-session/di/deviceSessionTypes.ts
@@ -3,5 +3,5 @@ export const deviceSessionTypes = {
ApduReceiverServiceFactory: Symbol.for("ApduReceiverServiceFactory"),
DeviceSessionService: Symbol.for("DeviceSessionService"),
GetDeviceSessionStateUseCase: Symbol.for("GetDeviceSessionStateUseCase"),
- ListDeviceSessionsUseCase: Symbol.for("ListDeviceSessionsUseCase"),
+ CloseSessionsUseCase: Symbol.for("CloseSessionsUseCase"),
};
diff --git a/packages/core/src/internal/device-session/model/DeviceSession.stub.ts b/packages/device-management-kit/src/internal/device-session/model/DeviceSession.stub.ts
similarity index 68%
rename from packages/core/src/internal/device-session/model/DeviceSession.stub.ts
rename to packages/device-management-kit/src/internal/device-session/model/DeviceSession.stub.ts
index 287744c8e..a21d9c148 100644
--- a/packages/core/src/internal/device-session/model/DeviceSession.stub.ts
+++ b/packages/device-management-kit/src/internal/device-session/model/DeviceSession.stub.ts
@@ -1,10 +1,10 @@
import {
DeviceSession,
- SessionConstructorArgs,
+ type SessionConstructorArgs,
} from "@internal/device-session/model/DeviceSession";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import type { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
-import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnectedDevice.stub";
+import { connectedDeviceStubBuilder } from "@internal/transport/model/InternalConnectedDevice.stub";
export const deviceSessionStubBuilder = (
props: Partial = {},
diff --git a/packages/core/src/internal/device-session/model/DeviceSession.ts b/packages/device-management-kit/src/internal/device-session/model/DeviceSession.ts
similarity index 80%
rename from packages/core/src/internal/device-session/model/DeviceSession.ts
rename to packages/device-management-kit/src/internal/device-session/model/DeviceSession.ts
index 286292766..630817944 100644
--- a/packages/core/src/internal/device-session/model/DeviceSession.ts
+++ b/packages/device-management-kit/src/internal/device-session/model/DeviceSession.ts
@@ -1,25 +1,27 @@
import { BehaviorSubject } from "rxjs";
import { v4 as uuidv4 } from "uuid";
-import { Command } from "@api/command/Command";
-import { CommandResult } from "@api/command/model/CommandResult";
-import { ListAppsResponse } from "@api/command/os/ListAppsCommand";
+import { type Command } from "@api/command/Command";
+import { type CommandResult } from "@api/command/model/CommandResult";
+import { type ListAppsResponse } from "@api/command/os/ListAppsCommand";
import { CommandUtils } from "@api/command/utils/CommandUtils";
import { DeviceStatus } from "@api/device/DeviceStatus";
import {
- DeviceAction,
- DeviceActionIntermediateValue,
- ExecuteDeviceActionReturnType,
+ type DeviceAction,
+ type DeviceActionIntermediateValue,
+ type ExecuteDeviceActionReturnType,
} from "@api/device-action/DeviceAction";
+import { type ApduResponse } from "@api/device-session/ApduResponse";
import {
- DeviceSessionState,
+ type DeviceSessionState,
DeviceSessionStateType,
} from "@api/device-session/DeviceSessionState";
-import { DeviceSessionId } from "@api/device-session/types";
-import { SdkError } from "@api/Error";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { type DeviceSessionId } from "@api/device-session/types";
+import { type DmkError } from "@api/Error";
+import { DEVICE_SESSION_REFRESH_INTERVAL } from "@internal/device-session/data/DeviceSessionRefresherConst";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
-import { InternalConnectedDevice } from "@internal/usb/model/InternalConnectedDevice";
+import { type InternalConnectedDevice } from "@internal/transport/model/InternalConnectedDevice";
import { DeviceSessionRefresher } from "./DeviceSessionRefresher";
@@ -51,8 +53,9 @@ export class DeviceSession {
});
this._refresher = new DeviceSessionRefresher(
{
- refreshInterval: 1000,
+ refreshInterval: DEVICE_SESSION_REFRESH_INTERVAL,
deviceStatus: DeviceStatus.CONNECTED,
+ deviceModelId: this._connectedDevice.deviceModel.id,
sendApduFn: (rawApdu: Uint8Array) =>
this.sendApdu(rawApdu, {
isPolling: true,
@@ -95,7 +98,10 @@ export class DeviceSession {
async sendApdu(
rawApdu: Uint8Array,
- options: { isPolling: boolean; triggersDisconnection: boolean } = {
+ options: {
+ isPolling: boolean;
+ triggersDisconnection: boolean;
+ } = {
isPolling: false,
triggersDisconnection: false,
},
@@ -107,7 +113,7 @@ export class DeviceSession {
options.triggersDisconnection,
);
- return errorOrResponse.ifRight((response) => {
+ return errorOrResponse.ifRight((response: ApduResponse) => {
if (CommandUtils.isLockedDeviceResponse(response)) {
this.updateDeviceStatus(DeviceStatus.LOCKED);
} else {
@@ -137,7 +143,7 @@ export class DeviceSession {
executeDeviceAction<
Output,
Input,
- Error extends SdkError,
+ Error extends DmkError,
IntermediateValue extends DeviceActionIntermediateValue,
>(
deviceAction: DeviceAction,
diff --git a/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.test.ts b/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.test.ts
new file mode 100644
index 000000000..315adc396
--- /dev/null
+++ b/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.test.ts
@@ -0,0 +1,208 @@
+import { Left, Right } from "purify-ts";
+
+import { CommandResultFactory } from "@api/command/model/CommandResult";
+import {
+ GetAppAndVersionCommand,
+ type GetAppAndVersionResponse,
+} from "@api/command/os/GetAppAndVersionCommand";
+import {
+ GetOsVersionCommand,
+ type GetOsVersionResponse,
+} from "@api/command/os/GetOsVersionCommand";
+import { DeviceModelId } from "@api/device/DeviceModel";
+import { DeviceStatus } from "@api/device/DeviceStatus";
+import { type ApduResponse } from "@api/device-session/ApduResponse";
+import { type DeviceSessionState } from "@api/device-session/DeviceSessionState";
+import { DEVICE_SESSION_REFRESH_INTERVAL } from "@internal/device-session/data/DeviceSessionRefresherConst";
+import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+
+import { DeviceSessionRefresher } from "./DeviceSessionRefresher";
+
+const mockSendApduFn = jest
+ .fn()
+ .mockResolvedValue(Promise.resolve(Right({} as ApduResponse)));
+const mockUpdateStateFn = jest.fn().mockImplementation(() => undefined);
+
+jest.useFakeTimers();
+
+describe("DeviceSessionRefresher", () => {
+ let deviceSessionRefresher: DeviceSessionRefresher;
+ let logger: LoggerPublisherService;
+
+ beforeEach(() => {
+ jest
+ .spyOn(GetAppAndVersionCommand.prototype, "parseResponse")
+ .mockReturnValueOnce(
+ CommandResultFactory({
+ data: {
+ name: "testAppName",
+ } as GetAppAndVersionResponse,
+ }),
+ );
+ jest
+ .spyOn(GetOsVersionCommand.prototype, "parseResponse")
+ .mockReturnValueOnce(
+ CommandResultFactory({
+ data: {} as GetOsVersionResponse,
+ }),
+ );
+ logger = new DefaultLoggerPublisherService(
+ [],
+ "DeviceSessionRefresherTest",
+ );
+ });
+
+ describe("With a modern device", () => {
+ beforeEach(() => {
+ const deviceIds = Object.values(DeviceModelId).filter(
+ (id) => id !== DeviceModelId.NANO_S,
+ );
+ deviceSessionRefresher = new DeviceSessionRefresher(
+ {
+ refreshInterval: DEVICE_SESSION_REFRESH_INTERVAL,
+ deviceStatus: DeviceStatus.CONNECTED,
+ sendApduFn: mockSendApduFn,
+ updateStateFn: mockUpdateStateFn,
+ deviceModelId:
+ deviceIds[Math.floor(Math.random() * deviceIds.length)]!,
+ },
+ logger,
+ );
+ });
+
+ afterEach(() => {
+ deviceSessionRefresher.stop();
+ jest.clearAllMocks();
+ });
+
+ it("should poll by calling sendApduFn 2 times", () => {
+ jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL * 2);
+ expect(mockSendApduFn).toHaveBeenCalledTimes(2);
+ });
+
+ it("should not poll when device is busy", () => {
+ deviceSessionRefresher.setDeviceStatus(DeviceStatus.BUSY);
+
+ jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL);
+
+ expect(mockSendApduFn).not.toHaveBeenCalled();
+ });
+
+ it("should not poll when device is disconnected", () => {
+ deviceSessionRefresher.setDeviceStatus(DeviceStatus.NOT_CONNECTED);
+
+ jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL);
+
+ expect(mockSendApduFn).not.toHaveBeenCalled();
+ });
+
+ it("should update device session state by calling updateStateFn", async () => {
+ jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL);
+
+ await expect(mockSendApduFn()).resolves.toEqual(Right({}));
+ expect(mockSendApduFn).toHaveBeenCalled();
+ expect(mockUpdateStateFn).toHaveBeenCalled();
+ });
+
+ it("should not update device session state with failed polling response", async () => {
+ mockSendApduFn.mockResolvedValueOnce(Promise.resolve(Left("error")));
+ const spy = jest.spyOn(logger, "error");
+
+ jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL);
+ await mockSendApduFn();
+
+ await expect(mockUpdateStateFn).not.toHaveBeenCalled();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it("should stop the refresher when device is disconnected", () => {
+ const spy = jest.spyOn(deviceSessionRefresher, "stop");
+ deviceSessionRefresher.setDeviceStatus(DeviceStatus.NOT_CONNECTED);
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it("should not throw error if stop is called on a stopped refresher", () => {
+ deviceSessionRefresher.stop();
+ expect(() => deviceSessionRefresher.stop()).not.toThrow();
+ });
+ });
+
+ describe("With a NanoS device", () => {
+ afterEach(() => {
+ deviceSessionRefresher.stop();
+ jest.clearAllMocks();
+ });
+
+ it("should call sendApduFn 2 times and update state 1 time for a single interval", async () => {
+ deviceSessionRefresher = new DeviceSessionRefresher(
+ {
+ refreshInterval: DEVICE_SESSION_REFRESH_INTERVAL,
+ deviceStatus: DeviceStatus.CONNECTED,
+ sendApduFn: mockSendApduFn,
+ updateStateFn: mockUpdateStateFn,
+ deviceModelId: DeviceModelId.NANO_S,
+ },
+ logger,
+ );
+ jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL * 2 + 100);
+
+ await Promise.resolve();
+ expect(mockSendApduFn).toHaveBeenNthCalledWith(
+ 1,
+ new GetAppAndVersionCommand().getApdu().getRawApdu(),
+ );
+ await Promise.resolve();
+ expect(mockSendApduFn).toHaveBeenLastCalledWith(
+ new GetOsVersionCommand().getApdu().getRawApdu(),
+ );
+ await Promise.resolve();
+ expect(mockSendApduFn).toHaveBeenCalledTimes(2);
+ await Promise.resolve();
+ expect(mockUpdateStateFn).toHaveBeenCalledTimes(1);
+ });
+
+ it("should set device locked when get os version times out", async () => {
+ mockSendApduFn.mockImplementation((apdu) => {
+ if (
+ apdu.toString() ===
+ new GetOsVersionCommand().getApdu().getRawApdu().toString()
+ ) {
+ return new Promise((resolve) =>
+ setTimeout(
+ () => resolve(Left("timeout")),
+ DEVICE_SESSION_REFRESH_INTERVAL * 10,
+ ),
+ );
+ }
+ return Promise.resolve(Right({}));
+ });
+ mockUpdateStateFn.mockImplementation(
+ (getState: () => DeviceSessionState) => {
+ deviceSessionRefresher.setDeviceStatus(getState().deviceStatus);
+ },
+ );
+ deviceSessionRefresher = new DeviceSessionRefresher(
+ {
+ refreshInterval: DEVICE_SESSION_REFRESH_INTERVAL,
+ deviceStatus: DeviceStatus.CONNECTED,
+ sendApduFn: mockSendApduFn,
+ updateStateFn: mockUpdateStateFn,
+ deviceModelId: DeviceModelId.NANO_S,
+ },
+ logger,
+ );
+ jest.spyOn(deviceSessionRefresher, "setDeviceStatus");
+ jest.advanceTimersByTime(DEVICE_SESSION_REFRESH_INTERVAL * 5 + 100);
+ await Promise.resolve();
+ expect(mockSendApduFn).toHaveBeenNthCalledWith(
+ 1,
+ new GetAppAndVersionCommand().getApdu().getRawApdu(),
+ );
+ await Promise.resolve();
+ expect(deviceSessionRefresher.setDeviceStatus).toHaveBeenCalledWith(
+ DeviceStatus.LOCKED,
+ );
+ });
+ });
+});
diff --git a/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.ts b/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.ts
new file mode 100644
index 000000000..23c29c6b0
--- /dev/null
+++ b/packages/device-management-kit/src/internal/device-session/model/DeviceSessionRefresher.ts
@@ -0,0 +1,218 @@
+import { injectable } from "inversify";
+import { Either } from "purify-ts";
+import {
+ delay,
+ filter,
+ from,
+ interval,
+ map,
+ Observable,
+ of,
+ race,
+ Subscription,
+ switchMap,
+} from "rxjs";
+
+import { isSuccessCommandResult } from "@api/command/model/CommandResult";
+import {
+ GetAppAndVersionCommand,
+ GetAppAndVersionCommandResult,
+} from "@api/command/os/GetAppAndVersionCommand";
+import { GetOsVersionCommand } from "@api/command/os/GetOsVersionCommand";
+import { DeviceModelId } from "@api/device/DeviceModel";
+import { DeviceStatus } from "@api/device/DeviceStatus";
+import { ApduResponse } from "@api/device-session/ApduResponse";
+import {
+ DeviceSessionState,
+ DeviceSessionStateType,
+} from "@api/device-session/DeviceSessionState";
+import { DmkError } from "@api/Error";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { SendApduFnType } from "@internal/transport/model/DeviceConnection";
+
+type UpdateStateFnType = (
+ callback: (state: DeviceSessionState) => DeviceSessionState,
+) => void;
+
+/**
+ * The arguments for the DeviceSessionRefresher.
+ */
+export type DeviceSessionRefresherArgs = {
+ /**
+ * The refresh interval in milliseconds.
+ */
+ refreshInterval: number;
+
+ /**
+ * The current device status when the refresher is created.
+ */
+ deviceStatus: Exclude;
+
+ /**
+ * The function used to send APDU commands to the device.
+ */
+ sendApduFn: (rawApdu: Uint8Array) => Promise>;
+
+ /**
+ * Callback that updates the state of the device session with
+ * polling response.
+ * @param callback - A function that will take the previous state and return the new state.
+ * @returns void
+ */
+ updateStateFn: UpdateStateFnType;
+
+ /**
+ * Device model to handle NanoS specific refresher
+ */
+ deviceModelId: DeviceModelId;
+};
+
+/**
+ * The session refresher that periodically sends a command to refresh the session.
+ */
+@injectable()
+export class DeviceSessionRefresher {
+ private readonly _logger: LoggerPublisherService;
+ private readonly _getAppAndVersionCommand = new GetAppAndVersionCommand();
+ private readonly _getOsVersionCommand = new GetOsVersionCommand();
+ private _deviceStatus: DeviceStatus;
+ private _subscription: Subscription;
+ private readonly _sendApduFn: SendApduFnType;
+ private readonly _updateStateFn: UpdateStateFnType;
+
+ constructor(
+ {
+ refreshInterval,
+ deviceStatus,
+ sendApduFn,
+ updateStateFn,
+ deviceModelId,
+ }: DeviceSessionRefresherArgs,
+ logger: LoggerPublisherService,
+ ) {
+ this._deviceStatus = deviceStatus;
+ this._logger = logger;
+ this._sendApduFn = sendApduFn;
+ this._updateStateFn = updateStateFn;
+
+ // NanoS has a specific refresher that sends GetAppAndVersion and GetOsVersion commands
+ const refreshObservable =
+ deviceModelId === DeviceModelId.NANO_S
+ ? this._getNanoSRefreshObservable(refreshInterval * 2)
+ : this._getDefaultRefreshObservable(interval(refreshInterval));
+
+ this._subscription = refreshObservable.subscribe((parsedResponse) => {
+ if (!parsedResponse || !isSuccessCommandResult(parsedResponse)) {
+ return;
+ }
+ // `batteryStatus` and `firmwareVersion` are not available in the polling response.
+ this._updateStateFn((state) => ({
+ ...state,
+ sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel,
+ deviceStatus: this._deviceStatus,
+ currentApp: parsedResponse.data,
+ installedApps: "installedApps" in state ? state.installedApps : [],
+ }));
+ });
+ }
+
+ /**
+ * Creates an observable that refreshes a device state with GetAppAndVersion command result.
+ *
+ * @param {ObservableInput} parentObservable - The parent observable to base the refresh observable on. Defaults to an array with a single number [0].
+ * @return {Observable} An observable that emits the result of the GetAppAndVersionCommand.
+ */
+ private _getDefaultRefreshObservable(
+ parentObservable: Observable = from([0]),
+ ): Observable {
+ return parentObservable.pipe(
+ filter(
+ () =>
+ ![DeviceStatus.BUSY, DeviceStatus.NOT_CONNECTED].includes(
+ this._deviceStatus,
+ ),
+ ),
+ switchMap(async () => {
+ const rawApdu = this._getAppAndVersionCommand.getApdu().getRawApdu();
+ return await this._sendApduFn(rawApdu);
+ }),
+ map((resp) =>
+ resp.caseOf({
+ Left: (error) => {
+ this._logger.error("Error in sending APDU when polling", {
+ data: { error },
+ });
+ return null;
+ },
+ Right: (data: ApduResponse) => {
+ try {
+ return this._getAppAndVersionCommand.parseResponse(data);
+ } catch (error) {
+ this._logger.error("Error in parsing APDU response", {
+ data: { error },
+ });
+ return null;
+ }
+ },
+ }),
+ ),
+ filter((parsedResponse) => parsedResponse !== null),
+ );
+ }
+
+ /**
+ * Creates an observable that emits events to refresh the NanoS device state.
+ *
+ * @param {number} refreshInterval - The interval, in milliseconds, at which the NanoS state should be refreshed.
+ * @return {Observable} An observable that emits events to refresh the NanoS device state and handle timeout scenarios.
+ */
+ private _getNanoSRefreshObservable(
+ refreshInterval: number,
+ ): Observable {
+ const nanoSRefreshObservable = this._getDefaultRefreshObservable().pipe(
+ switchMap(async (resp) => {
+ const rawApdu = this._getOsVersionCommand.getApdu().getRawApdu();
+ await this._sendApduFn(rawApdu);
+ return resp;
+ }),
+ );
+ const timeoutObservable = of(null).pipe(
+ delay(refreshInterval),
+ map((_) => {
+ this._logger.warn(
+ "Nanos refresh timeout, setting device status to LOCKED",
+ );
+ this._updateStateFn((state) => ({
+ ...state,
+ deviceStatus: DeviceStatus.LOCKED,
+ }));
+ }),
+ );
+ return interval(refreshInterval + 100).pipe(
+ switchMap(() => race(nanoSRefreshObservable, timeoutObservable)),
+ );
+ }
+
+ /**
+ * Maintain a device status to prevent sending APDU when the device is busy.
+ *
+ * @param {DeviceStatus} deviceStatus - The new device status.
+ */
+ setDeviceStatus(deviceStatus: DeviceStatus) {
+ if (deviceStatus === DeviceStatus.NOT_CONNECTED) {
+ this.stop();
+ }
+ this._deviceStatus = deviceStatus;
+ }
+
+ /**
+ * Stops the session refresher.
+ * The refresher will no longer send commands to refresh the session.
+ */
+ stop() {
+ if (!this._subscription || this._subscription.closed) {
+ return;
+ }
+ this._subscription.unsubscribe();
+ }
+}
diff --git a/packages/core/src/internal/device-session/model/Errors.ts b/packages/device-management-kit/src/internal/device-session/model/Errors.ts
similarity index 73%
rename from packages/core/src/internal/device-session/model/Errors.ts
rename to packages/device-management-kit/src/internal/device-session/model/Errors.ts
index 651ec412d..d036ef84f 100644
--- a/packages/core/src/internal/device-session/model/Errors.ts
+++ b/packages/device-management-kit/src/internal/device-session/model/Errors.ts
@@ -1,6 +1,6 @@
-import { SdkError } from "@root/src/api/Error";
+import { type DmkError } from "@root/src/api/Error";
-export class FramerOverflowError implements SdkError {
+export class FramerOverflowError implements DmkError {
readonly _tag = "FramerOverflowError";
originalError?: Error;
constructor() {
@@ -10,7 +10,7 @@ export class FramerOverflowError implements SdkError {
}
}
-export class FramerApduError implements SdkError {
+export class FramerApduError implements DmkError {
readonly _tag = "FramerApduError";
originalError?: Error;
@@ -19,7 +19,7 @@ export class FramerApduError implements SdkError {
}
}
-export class ReceiverApduError implements SdkError {
+export class ReceiverApduError implements DmkError {
readonly _tag = "ReceiverApduError";
originalError: Error;
@@ -28,7 +28,7 @@ export class ReceiverApduError implements SdkError {
}
}
-export class DeviceSessionNotFound implements SdkError {
+export class DeviceSessionNotFound implements DmkError {
readonly _tag = "DeviceSessionNotFound";
originalError?: Error;
@@ -37,7 +37,7 @@ export class DeviceSessionNotFound implements SdkError {
}
}
-export class DeviceSessionRefresherError implements SdkError {
+export class DeviceSessionRefresherError implements DmkError {
readonly _tag = "DeviceSessionRefresherError";
originalError?: Error;
diff --git a/packages/core/src/internal/device-session/model/Frame.ts b/packages/device-management-kit/src/internal/device-session/model/Frame.ts
similarity index 91%
rename from packages/core/src/internal/device-session/model/Frame.ts
rename to packages/device-management-kit/src/internal/device-session/model/Frame.ts
index 353de03b1..0af849dae 100644
--- a/packages/core/src/internal/device-session/model/Frame.ts
+++ b/packages/device-management-kit/src/internal/device-session/model/Frame.ts
@@ -1,4 +1,4 @@
-import { FrameHeader } from "@internal/device-session/model/FrameHeader";
+import { type FrameHeader } from "@internal/device-session/model/FrameHeader";
type FrameConstructorArgs = {
header: FrameHeader;
diff --git a/packages/core/src/internal/device-session/model/FrameHeader.ts b/packages/device-management-kit/src/internal/device-session/model/FrameHeader.ts
similarity index 97%
rename from packages/core/src/internal/device-session/model/FrameHeader.ts
rename to packages/device-management-kit/src/internal/device-session/model/FrameHeader.ts
index 8cd260a79..334463d60 100644
--- a/packages/core/src/internal/device-session/model/FrameHeader.ts
+++ b/packages/device-management-kit/src/internal/device-session/model/FrameHeader.ts
@@ -1,4 +1,4 @@
-import { Maybe } from "purify-ts";
+import { type Maybe } from "purify-ts";
import { FramerUtils } from "@internal/device-session/utils/FramerUtils";
diff --git a/packages/device-management-kit/src/internal/device-session/service/ApduReceiverService.ts b/packages/device-management-kit/src/internal/device-session/service/ApduReceiverService.ts
new file mode 100644
index 000000000..d6b56f139
--- /dev/null
+++ b/packages/device-management-kit/src/internal/device-session/service/ApduReceiverService.ts
@@ -0,0 +1,8 @@
+import { type Either, type Maybe } from "purify-ts";
+
+import { type ApduResponse } from "@api/device-session/ApduResponse";
+import { type DmkError } from "@api/Error";
+
+export interface ApduReceiverService {
+ handleFrame(apdu: Uint8Array): Either>;
+}
diff --git a/packages/core/src/internal/device-session/service/ApduSenderService.ts b/packages/device-management-kit/src/internal/device-session/service/ApduSenderService.ts
similarity index 55%
rename from packages/core/src/internal/device-session/service/ApduSenderService.ts
rename to packages/device-management-kit/src/internal/device-session/service/ApduSenderService.ts
index 1578c0520..2065a5ea8 100644
--- a/packages/core/src/internal/device-session/service/ApduSenderService.ts
+++ b/packages/device-management-kit/src/internal/device-session/service/ApduSenderService.ts
@@ -1,4 +1,4 @@
-import { Frame } from "@internal/device-session/model/Frame";
+import { type Frame } from "@internal/device-session/model/Frame";
export interface ApduSenderService {
getFrames: (apdu: Uint8Array) => Frame[];
diff --git a/packages/core/src/internal/device-session/service/DefaultApduReceiverService.stub.ts b/packages/device-management-kit/src/internal/device-session/service/DefaultApduReceiverService.stub.ts
similarity index 66%
rename from packages/core/src/internal/device-session/service/DefaultApduReceiverService.stub.ts
rename to packages/device-management-kit/src/internal/device-session/service/DefaultApduReceiverService.stub.ts
index 547fddae6..cda3fea24 100644
--- a/packages/core/src/internal/device-session/service/DefaultApduReceiverService.stub.ts
+++ b/packages/device-management-kit/src/internal/device-session/service/DefaultApduReceiverService.stub.ts
@@ -1,11 +1,11 @@
import { Maybe } from "purify-ts";
-import { ApduReceiverService } from "@internal/device-session/service/ApduReceiverService";
+import { type ApduReceiverService } from "@internal/device-session/service/ApduReceiverService";
import {
- DefaultApduReceiverConstructorArgs,
+ type DefaultApduReceiverConstructorArgs,
DefaultApduReceiverService,
} from "@internal/device-session/service/DefaultApduReceiverService";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
export const defaultApduReceiverServiceStubBuilder = (
props: Partial = {},
diff --git a/packages/core/src/internal/device-session/service/DefaultApduReceiverService.test.ts b/packages/device-management-kit/src/internal/device-session/service/DefaultApduReceiverService.test.ts
similarity index 98%
rename from packages/core/src/internal/device-session/service/DefaultApduReceiverService.test.ts
rename to packages/device-management-kit/src/internal/device-session/service/DefaultApduReceiverService.test.ts
index d3ea8cf0a..5a34df690 100644
--- a/packages/core/src/internal/device-session/service/DefaultApduReceiverService.test.ts
+++ b/packages/device-management-kit/src/internal/device-session/service/DefaultApduReceiverService.test.ts
@@ -1,13 +1,13 @@
import * as uuid from "uuid";
jest.mock("uuid");
-import { Just, Left, Maybe, Nothing, Right } from "purify-ts";
+import { Just, Left, type Maybe, Nothing, Right } from "purify-ts";
import { ApduResponse } from "@api/device-session/ApduResponse";
import { ReceiverApduError } from "@internal/device-session/model/Errors";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
-import { ApduReceiverService } from "./ApduReceiverService";
+import { type ApduReceiverService } from "./ApduReceiverService";
import { DefaultApduReceiverService } from "./DefaultApduReceiverService";
const loggerService = new DefaultLoggerPublisherService([], "frame");
diff --git a/packages/core/src/internal/device-session/service/DefaultApduReceiverService.ts b/packages/device-management-kit/src/internal/device-session/service/DefaultApduReceiverService.ts
similarity index 100%
rename from packages/core/src/internal/device-session/service/DefaultApduReceiverService.ts
rename to packages/device-management-kit/src/internal/device-session/service/DefaultApduReceiverService.ts
diff --git a/packages/core/src/internal/device-session/service/DefaultApduSenderService.stub.ts b/packages/device-management-kit/src/internal/device-session/service/DefaultApduSenderService.stub.ts
similarity index 68%
rename from packages/core/src/internal/device-session/service/DefaultApduSenderService.stub.ts
rename to packages/device-management-kit/src/internal/device-session/service/DefaultApduSenderService.stub.ts
index 207e16058..7f16c7b32 100644
--- a/packages/core/src/internal/device-session/service/DefaultApduSenderService.stub.ts
+++ b/packages/device-management-kit/src/internal/device-session/service/DefaultApduSenderService.stub.ts
@@ -1,11 +1,11 @@
import { Maybe } from "purify-ts";
-import { ApduSenderService } from "@internal/device-session/service/ApduSenderService";
+import { type ApduSenderService } from "@internal/device-session/service/ApduSenderService";
import {
DefaultApduSenderService,
- DefaultApduSenderServiceConstructorArgs,
+ type DefaultApduSenderServiceConstructorArgs,
} from "@internal/device-session/service/DefaultApduSenderService";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
export const defaultApduSenderServiceStubBuilder = (
props: Partial = {},
diff --git a/packages/core/src/internal/device-session/service/DefaultApduSenderService.test.ts b/packages/device-management-kit/src/internal/device-session/service/DefaultApduSenderService.test.ts
similarity index 100%
rename from packages/core/src/internal/device-session/service/DefaultApduSenderService.test.ts
rename to packages/device-management-kit/src/internal/device-session/service/DefaultApduSenderService.test.ts
diff --git a/packages/core/src/internal/device-session/service/DefaultApduSenderService.ts b/packages/device-management-kit/src/internal/device-session/service/DefaultApduSenderService.ts
similarity index 98%
rename from packages/core/src/internal/device-session/service/DefaultApduSenderService.ts
rename to packages/device-management-kit/src/internal/device-session/service/DefaultApduSenderService.ts
index 72c1f4dd9..a6ac32e07 100644
--- a/packages/core/src/internal/device-session/service/DefaultApduSenderService.ts
+++ b/packages/device-management-kit/src/internal/device-session/service/DefaultApduSenderService.ts
@@ -18,7 +18,7 @@ import { FrameHeader } from "@internal/device-session/model/FrameHeader";
import { FramerUtils } from "@internal/device-session/utils/FramerUtils";
import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes";
import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
-import { SdkError } from "@root/src/api/Error";
+import { DmkError } from "@root/src/api/Error";
import type { ApduSenderService } from "./ApduSenderService";
@@ -98,7 +98,7 @@ export class DefaultApduSenderService implements ApduSenderService {
private getFrameAtIndex(
apdu: Uint8Array,
frameIndex: number,
- ): Either {
+ ): Either {
const header = this.getFrameHeaderFrom(frameIndex, apdu.length);
const frameOffset =
frameIndex * this._frameSize - this.getHeaderSizeSumFrom(frameIndex);
diff --git a/packages/core/src/internal/device-session/service/DefaultDeviceSessionService.test.ts b/packages/device-management-kit/src/internal/device-session/service/DefaultDeviceSessionService.test.ts
similarity index 65%
rename from packages/core/src/internal/device-session/service/DefaultDeviceSessionService.test.ts
rename to packages/device-management-kit/src/internal/device-session/service/DefaultDeviceSessionService.test.ts
index 7f382eef0..404e4093a 100644
--- a/packages/core/src/internal/device-session/service/DefaultDeviceSessionService.test.ts
+++ b/packages/device-management-kit/src/internal/device-session/service/DefaultDeviceSessionService.test.ts
@@ -1,13 +1,14 @@
import { Either, Left } from "purify-ts";
+import { Observable } from "rxjs";
-import { DeviceSession } from "@internal/device-session/model/DeviceSession";
+import { type DeviceSession } from "@internal/device-session/model/DeviceSession";
+import { deviceSessionStubBuilder } from "@internal/device-session/model/DeviceSession.stub";
import { DeviceSessionNotFound } from "@internal/device-session/model/Errors";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource";
-import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
+import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService";
import type { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
-import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnectedDevice.stub";
import { DefaultDeviceSessionService } from "./DefaultDeviceSessionService";
@@ -26,18 +27,21 @@ describe("DefaultDeviceSessionService", () => {
sessionService = new DefaultDeviceSessionService(() => loggerService);
managerApiDataSource = new AxiosManagerApiDataSource({
managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
});
managerApi = new DefaultManagerApiService(managerApiDataSource);
- deviceSession = new DeviceSession(
- {
- connectedDevice: connectedDeviceStubBuilder(),
- },
+ deviceSession = deviceSessionStubBuilder(
+ {},
() => loggerService,
managerApi,
);
});
+ afterEach(() => {
+ deviceSession.close();
+ });
+
it("should have an empty sessions list", () => {
expect(sessionService.getDeviceSessions()).toEqual([]);
});
@@ -81,4 +85,41 @@ describe("DefaultDeviceSessionService", () => {
sessionService.addDeviceSession(deviceSession);
expect(sessionService.getDeviceSessions()).toEqual([deviceSession]);
});
+
+ it("should retrieve sessionObs", () => {
+ expect(sessionService.sessionsObs).toBeInstanceOf(
+ Observable,
+ );
+ });
+
+ it("should emit new session", (done) => {
+ const subscription = sessionService.sessionsObs.subscribe({
+ next(emittedDeviceSession) {
+ expect(emittedDeviceSession).toStrictEqual(deviceSession);
+ subscription.unsubscribe();
+ done();
+ },
+ });
+ sessionService.addDeviceSession(deviceSession);
+ });
+
+ it("should emit previous added session", () => {
+ const lastDeviceSession = deviceSessionStubBuilder(
+ { id: "last-session" },
+ () => loggerService,
+ managerApi,
+ );
+ const emittedSessions: DeviceSession[] = [];
+ sessionService.addDeviceSession(deviceSession);
+ sessionService.addDeviceSession(lastDeviceSession);
+
+ const subscription = sessionService.sessionsObs.subscribe({
+ next(emittedDeviceSession) {
+ emittedSessions.push(emittedDeviceSession);
+ },
+ });
+ lastDeviceSession.close();
+ expect(emittedSessions).toEqual([deviceSession, lastDeviceSession]);
+ subscription.unsubscribe();
+ });
});
diff --git a/packages/core/src/internal/device-session/service/DefaultDeviceSessionService.ts b/packages/device-management-kit/src/internal/device-session/service/DefaultDeviceSessionService.ts
similarity index 85%
rename from packages/core/src/internal/device-session/service/DefaultDeviceSessionService.ts
rename to packages/device-management-kit/src/internal/device-session/service/DefaultDeviceSessionService.ts
index 1d2eca8fa..8b049fe74 100644
--- a/packages/core/src/internal/device-session/service/DefaultDeviceSessionService.ts
+++ b/packages/device-management-kit/src/internal/device-session/service/DefaultDeviceSessionService.ts
@@ -1,5 +1,6 @@
import { inject, injectable } from "inversify";
import { Maybe } from "purify-ts";
+import { Observable, ReplaySubject } from "rxjs";
import { DeviceSession } from "@internal/device-session/model/DeviceSession";
import { DeviceSessionNotFound } from "@internal/device-session/model/Errors";
@@ -10,16 +11,22 @@ import { LoggerPublisherService } from "@internal/logger-publisher/service/Logge
@injectable()
export class DefaultDeviceSessionService implements DeviceSessionService {
private _sessions: DeviceSession[];
- private _logger: LoggerPublisherService;
+ private readonly _logger: LoggerPublisherService;
+ private _sessionsSubject: ReplaySubject;
constructor(
@inject(loggerTypes.LoggerPublisherServiceFactory)
loggerModuleFactory: (tag: string) => LoggerPublisherService,
) {
this._sessions = [];
+ this._sessionsSubject = new ReplaySubject();
this._logger = loggerModuleFactory("DeviceSessionService");
}
+ public get sessionsObs(): Observable {
+ return this._sessionsSubject.asObservable();
+ }
+
addDeviceSession(deviceSession: DeviceSession) {
const found = this._sessions.find((s) => s.id === deviceSession.id);
if (found) {
@@ -30,6 +37,7 @@ export class DefaultDeviceSessionService implements DeviceSessionService {
}
this._sessions.push(deviceSession);
+ this._sessionsSubject.next(deviceSession);
this._logger.info("DeviceSession added", { data: { deviceSession } });
return this;
}
diff --git a/packages/device-management-kit/src/internal/device-session/service/DeviceSessionService.ts b/packages/device-management-kit/src/internal/device-session/service/DeviceSessionService.ts
new file mode 100644
index 000000000..f6f3aefc6
--- /dev/null
+++ b/packages/device-management-kit/src/internal/device-session/service/DeviceSessionService.ts
@@ -0,0 +1,14 @@
+import { type Either } from "purify-ts";
+import { type Observable } from "rxjs";
+
+import { type DmkError } from "@api/Error";
+import { type DeviceSession } from "@internal/device-session/model/DeviceSession";
+
+export interface DeviceSessionService {
+ addDeviceSession(deviceSession: DeviceSession): DeviceSessionService;
+ getDeviceSessionById(sessionId: string): Either;
+ getDeviceSessionByDeviceId(deviceId: string): Either;
+ removeDeviceSession(sessionId: string): DeviceSessionService;
+ getDeviceSessions(): DeviceSession[];
+ get sessionsObs(): Observable;
+}
diff --git a/packages/device-management-kit/src/internal/device-session/use-case/CloseSessionsUseCase.ts b/packages/device-management-kit/src/internal/device-session/use-case/CloseSessionsUseCase.ts
new file mode 100644
index 000000000..1563f662f
--- /dev/null
+++ b/packages/device-management-kit/src/internal/device-session/use-case/CloseSessionsUseCase.ts
@@ -0,0 +1,23 @@
+import { inject, injectable } from "inversify";
+
+import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes";
+import type { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
+
+@injectable()
+export class CloseSessionsUseCase {
+ private readonly _sessionService: DeviceSessionService;
+ constructor(
+ @inject(deviceSessionTypes.DeviceSessionService)
+ sessionService: DeviceSessionService,
+ ) {
+ this._sessionService = sessionService;
+ }
+
+ execute() {
+ const deviceSessions = this._sessionService.getDeviceSessions();
+
+ for (const dSession of deviceSessions) {
+ dSession.close();
+ }
+ }
+}
diff --git a/packages/device-management-kit/src/internal/device-session/use-case/CloseSessionsUseState.test.ts b/packages/device-management-kit/src/internal/device-session/use-case/CloseSessionsUseState.test.ts
new file mode 100644
index 000000000..28e3534cb
--- /dev/null
+++ b/packages/device-management-kit/src/internal/device-session/use-case/CloseSessionsUseState.test.ts
@@ -0,0 +1,51 @@
+import { deviceSessionStubBuilder } from "@internal/device-session/model/DeviceSession.stub";
+import { DefaultDeviceSessionService } from "@internal/device-session/service/DefaultDeviceSessionService";
+import { type DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
+import { CloseSessionsUseCase } from "@internal/device-session/use-case/CloseSessionsUseCase";
+import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource";
+import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
+import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService";
+import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
+
+let logger: LoggerPublisherService;
+let managerApiDataSource: ManagerApiDataSource;
+let managerApi: ManagerApiService;
+let sessionService: DeviceSessionService;
+
+describe("CloseSessionsUseState", () => {
+ beforeEach(() => {
+ logger = new DefaultLoggerPublisherService(
+ [],
+ "close-sessions-use-case-test",
+ );
+ managerApiDataSource = new AxiosManagerApiDataSource({
+ managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
+ });
+ managerApi = new DefaultManagerApiService(managerApiDataSource);
+ sessionService = new DefaultDeviceSessionService(() => logger);
+ });
+
+ it("should be able to close every session", () => {
+ //given
+ const sessions = [...Array(10).keys()].map((id) => {
+ const session = deviceSessionStubBuilder(
+ { id: id.toString() },
+ () => logger,
+ managerApi,
+ );
+ jest.spyOn(session, "close");
+ return session;
+ });
+ sessions.forEach((session) => sessionService.addDeviceSession(session));
+ const useCase = new CloseSessionsUseCase(sessionService);
+ //when
+ useCase.execute();
+ //then
+ sessions.forEach((session) => {
+ expect(session.close).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/packages/core/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts b/packages/device-management-kit/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts
similarity index 82%
rename from packages/core/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts
rename to packages/device-management-kit/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts
index d00043301..c3cfa9b0b 100644
--- a/packages/core/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts
+++ b/packages/device-management-kit/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.test.ts
@@ -1,12 +1,12 @@
import { deviceSessionStubBuilder } from "@internal/device-session/model/DeviceSession.stub";
import { DefaultDeviceSessionService } from "@internal/device-session/service/DefaultDeviceSessionService";
-import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
+import { type DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource";
-import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
+import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService";
-import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
+import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
import { GetDeviceSessionStateUseCase } from "./GetDeviceSessionStateUseCase";
@@ -27,6 +27,7 @@ describe("GetDeviceSessionStateUseCase", () => {
);
managerApiDataSource = new AxiosManagerApiDataSource({
managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
});
managerApi = new DefaultManagerApiService(managerApiDataSource);
sessionService = new DefaultDeviceSessionService(() => logger);
@@ -50,6 +51,8 @@ describe("GetDeviceSessionStateUseCase", () => {
sessionId: fakeSessionId,
});
+ deviceSession.close();
+
// then
expect(response).toStrictEqual(deviceSession.state);
});
diff --git a/packages/core/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.ts b/packages/device-management-kit/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.ts
similarity index 100%
rename from packages/core/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.ts
rename to packages/device-management-kit/src/internal/device-session/use-case/GetDeviceSessionStateUseCase.ts
diff --git a/packages/core/src/internal/device-session/utils/FramerUtils.test.ts b/packages/device-management-kit/src/internal/device-session/utils/FramerUtils.test.ts
similarity index 100%
rename from packages/core/src/internal/device-session/utils/FramerUtils.test.ts
rename to packages/device-management-kit/src/internal/device-session/utils/FramerUtils.test.ts
diff --git a/packages/core/src/internal/device-session/utils/FramerUtils.ts b/packages/device-management-kit/src/internal/device-session/utils/FramerUtils.ts
similarity index 100%
rename from packages/core/src/internal/device-session/utils/FramerUtils.ts
rename to packages/device-management-kit/src/internal/device-session/utils/FramerUtils.ts
diff --git a/packages/core/src/internal/discovery/di/discoveryModule.test.ts b/packages/device-management-kit/src/internal/discovery/di/discoveryModule.test.ts
similarity index 68%
rename from packages/core/src/internal/discovery/di/discoveryModule.test.ts
rename to packages/device-management-kit/src/internal/discovery/di/discoveryModule.test.ts
index a0c7b5575..191cb3d33 100644
--- a/packages/core/src/internal/discovery/di/discoveryModule.test.ts
+++ b/packages/device-management-kit/src/internal/discovery/di/discoveryModule.test.ts
@@ -4,11 +4,15 @@ import { deviceModelModuleFactory } from "@internal/device-model/di/deviceModelM
import { deviceSessionModuleFactory } from "@internal/device-session/di/deviceSessionModule";
import { ConnectUseCase } from "@internal/discovery/use-case/ConnectUseCase";
import { DisconnectUseCase } from "@internal/discovery/use-case/DisconnectUseCase";
+import { ListConnectedDevicesUseCase } from "@internal/discovery/use-case/ListConnectedDevicesUseCase";
+import { ListenToKnownDevicesUseCase } from "@internal/discovery/use-case/ListenToKnownDevicesUseCase";
import { StartDiscoveringUseCase } from "@internal/discovery/use-case/StartDiscoveringUseCase";
import { StopDiscoveringUseCase } from "@internal/discovery/use-case/StopDiscoveringUseCase";
import { loggerModuleFactory } from "@internal/logger-publisher/di/loggerModule";
import { managerApiModuleFactory } from "@internal/manager-api/di/managerApiModule";
-import { usbModuleFactory } from "@internal/usb/di/usbModule";
+import { transportModuleFactory } from "@internal/transport/di/transportModule";
+import { usbModuleFactory } from "@internal/transport/usb/di/usbModule";
+import { BuiltinTransports } from "@root/src";
import { discoveryModuleFactory } from "./discoveryModule";
import { discoveryTypes } from "./discoveryTypes";
@@ -26,8 +30,12 @@ describe("discoveryModuleFactory", () => {
usbModuleFactory({ stub: false }),
deviceModelModuleFactory({ stub: false }),
deviceSessionModuleFactory(),
+ transportModuleFactory({ transports: [BuiltinTransports.USB] }),
managerApiModuleFactory({
- config: { managerApiUrl: "http://fake.url" },
+ config: {
+ managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
+ },
}),
);
});
@@ -52,5 +60,18 @@ describe("discoveryModuleFactory", () => {
const connectUseCase = container.get(discoveryTypes.ConnectUseCase);
expect(connectUseCase).toBeInstanceOf(ConnectUseCase);
+
+ const listenToKnownDevicesUseCase = container.get(
+ discoveryTypes.ListenToKnownDevicesUseCase,
+ );
+ expect(listenToKnownDevicesUseCase).toBeInstanceOf(
+ ListenToKnownDevicesUseCase,
+ );
+ const listConnectedDevicesUseCase = container.get(
+ discoveryTypes.ListConnectedDevicesUseCase,
+ );
+ expect(listConnectedDevicesUseCase).toBeInstanceOf(
+ ListConnectedDevicesUseCase,
+ );
});
});
diff --git a/packages/core/src/internal/discovery/di/discoveryModule.ts b/packages/device-management-kit/src/internal/discovery/di/discoveryModule.ts
similarity index 52%
rename from packages/core/src/internal/discovery/di/discoveryModule.ts
rename to packages/device-management-kit/src/internal/discovery/di/discoveryModule.ts
index e2beda61b..aef4e46e4 100644
--- a/packages/core/src/internal/discovery/di/discoveryModule.ts
+++ b/packages/device-management-kit/src/internal/discovery/di/discoveryModule.ts
@@ -2,6 +2,10 @@ import { ContainerModule } from "inversify";
import { ConnectUseCase } from "@internal/discovery/use-case/ConnectUseCase";
import { DisconnectUseCase } from "@internal/discovery/use-case/DisconnectUseCase";
+import { GetConnectedDeviceUseCase } from "@internal/discovery/use-case/GetConnectedDeviceUseCase";
+import { ListConnectedDevicesUseCase } from "@internal/discovery/use-case/ListConnectedDevicesUseCase";
+import { ListenToConnectedDeviceUseCase } from "@internal/discovery/use-case/ListenToConnectedDeviceUseCase";
+import { ListenToKnownDevicesUseCase } from "@internal/discovery/use-case/ListenToKnownDevicesUseCase";
import { StartDiscoveringUseCase } from "@internal/discovery/use-case/StartDiscoveringUseCase";
import { StopDiscoveringUseCase } from "@internal/discovery/use-case/StopDiscoveringUseCase";
import { StubUseCase } from "@root/src/di.stub";
@@ -18,11 +22,27 @@ export const discoveryModuleFactory = ({ stub = false }: FactoryProps) =>
bind(discoveryTypes.DisconnectUseCase).to(DisconnectUseCase);
bind(discoveryTypes.StartDiscoveringUseCase).to(StartDiscoveringUseCase);
bind(discoveryTypes.StopDiscoveringUseCase).to(StopDiscoveringUseCase);
+ bind(discoveryTypes.GetConnectedDeviceUseCase).to(
+ GetConnectedDeviceUseCase,
+ );
+ bind(discoveryTypes.ListenToKnownDevicesUseCase).to(
+ ListenToKnownDevicesUseCase,
+ );
+ bind(discoveryTypes.ListenToConnectedDeviceUseCase).to(
+ ListenToConnectedDeviceUseCase,
+ );
+ bind(discoveryTypes.ListConnectedDevicesUseCase).to(
+ ListConnectedDevicesUseCase,
+ );
if (stub) {
rebind(discoveryTypes.StartDiscoveringUseCase).to(StubUseCase);
rebind(discoveryTypes.StopDiscoveringUseCase).to(StubUseCase);
rebind(discoveryTypes.ConnectUseCase).to(StubUseCase);
rebind(discoveryTypes.DisconnectUseCase).to(StubUseCase);
+ rebind(discoveryTypes.GetConnectedDeviceUseCase).to(StubUseCase);
+ rebind(discoveryTypes.ListenToKnownDevicesUseCase).to(StubUseCase);
+ rebind(discoveryTypes.ListenToConnectedDeviceUseCase).to(StubUseCase);
+ rebind(discoveryTypes.ListConnectedDevicesUseCase).to(StubUseCase);
}
});
diff --git a/packages/device-management-kit/src/internal/discovery/di/discoveryTypes.ts b/packages/device-management-kit/src/internal/discovery/di/discoveryTypes.ts
new file mode 100644
index 000000000..60a7f01e1
--- /dev/null
+++ b/packages/device-management-kit/src/internal/discovery/di/discoveryTypes.ts
@@ -0,0 +1,10 @@
+export const discoveryTypes = {
+ StartDiscoveringUseCase: Symbol.for("StartDiscoveringUseCase"),
+ StopDiscoveringUseCase: Symbol.for("StopDiscoveringUseCase"),
+ ConnectUseCase: Symbol.for("ConnectUseCase"),
+ DisconnectUseCase: Symbol.for("DisconnectUseCase"),
+ GetConnectedDeviceUseCase: Symbol.for("GetConnectedDeviceUseCase"),
+ ListenToKnownDevicesUseCase: Symbol.for("ListenToKnownDevicesUseCase"),
+ ListenToConnectedDeviceUseCase: Symbol.for("ListenToConnectedDeviceUseCase"),
+ ListConnectedDevicesUseCase: Symbol.for("ListConnectedDevicesUseCase"),
+};
diff --git a/packages/core/src/internal/discovery/use-case/ConnectUseCase.test.ts b/packages/device-management-kit/src/internal/discovery/use-case/ConnectUseCase.test.ts
similarity index 51%
rename from packages/core/src/internal/discovery/use-case/ConnectUseCase.test.ts
rename to packages/device-management-kit/src/internal/discovery/use-case/ConnectUseCase.test.ts
index 62cf8394c..87c60167d 100644
--- a/packages/core/src/internal/discovery/use-case/ConnectUseCase.test.ts
+++ b/packages/device-management-kit/src/internal/discovery/use-case/ConnectUseCase.test.ts
@@ -2,25 +2,28 @@ import { Left, Right } from "purify-ts";
import * as uuid from "uuid";
jest.mock("uuid");
-import { DeviceModelDataSource } from "@internal/device-model/data/DeviceModelDataSource";
+import { type DeviceModel } from "@api/device/DeviceModel";
+import { type DiscoveredDevice } from "@api/transport/model/DiscoveredDevice";
+import { type DeviceModelDataSource } from "@internal/device-model/data/DeviceModelDataSource";
import { DefaultDeviceSessionService } from "@internal/device-session/service/DefaultDeviceSessionService";
-import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
+import { type DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource";
-import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
+import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService";
-import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
-import { UnknownDeviceError } from "@internal/usb/model/Errors";
-import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnectedDevice.stub";
-import { usbHidDeviceConnectionFactoryStubBuilder } from "@internal/usb/service/UsbHidDeviceConnectionFactory.stub";
-import { WebUsbHidTransport } from "@internal/usb/transport/WebUsbHidTransport";
+import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
+import { UnknownDeviceError } from "@internal/transport/model/Errors";
+import { connectedDeviceStubBuilder } from "@internal/transport/model/InternalConnectedDevice.stub";
+import { usbHidDeviceConnectionFactoryStubBuilder } from "@internal/transport/usb/service/UsbHidDeviceConnectionFactory.stub";
+import { WebUsbHidTransport } from "@internal/transport/usb/transport/WebUsbHidTransport";
import { ConnectUseCase } from "./ConnectUseCase";
jest.mock("@internal/manager-api/data/AxiosManagerApiDataSource");
-let transport: WebUsbHidTransport;
+// TODO test several transports
+let transports: WebUsbHidTransport[];
let logger: LoggerPublisherService;
let sessionService: DeviceSessionService;
let managerApi: ManagerApiService;
@@ -28,58 +31,75 @@ let managerApiDataSource: ManagerApiDataSource;
const fakeSessionId = "fakeSessionId";
describe("ConnectUseCase", () => {
+ const stubDiscoveredDevice: DiscoveredDevice = {
+ id: "",
+ deviceModel: {} as DeviceModel,
+ transport: "USB",
+ };
const stubConnectedDevice = connectedDeviceStubBuilder({ id: "1" });
const tag = "logger-tag";
beforeAll(() => {
logger = new DefaultLoggerPublisherService([], tag);
jest.spyOn(uuid, "v4").mockReturnValue(fakeSessionId);
- transport = new WebUsbHidTransport(
- {} as DeviceModelDataSource,
- () => logger,
- usbHidDeviceConnectionFactoryStubBuilder(),
- );
+ transports = [
+ new WebUsbHidTransport(
+ {} as DeviceModelDataSource,
+ () => logger,
+ usbHidDeviceConnectionFactoryStubBuilder(),
+ ),
+ ];
sessionService = new DefaultDeviceSessionService(() => logger);
managerApiDataSource = new AxiosManagerApiDataSource({
managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
});
managerApi = new DefaultManagerApiService(managerApiDataSource);
});
+ afterEach(() => {
+ for (const session of sessionService.getDeviceSessions()) {
+ sessionService.removeDeviceSession(session.id);
+ }
+ });
+
afterAll(() => {
jest.restoreAllMocks();
});
test("If connect use case encounter an error, return it", async () => {
jest
- .spyOn(transport, "connect")
+ .spyOn(transports[0]!, "connect")
.mockResolvedValue(Left(new UnknownDeviceError()));
const usecase = new ConnectUseCase(
- transport,
+ transports,
sessionService,
() => logger,
managerApi,
);
- await expect(usecase.execute({ deviceId: "" })).rejects.toBeInstanceOf(
- UnknownDeviceError,
- );
+ await expect(
+ usecase.execute({ device: stubDiscoveredDevice }),
+ ).rejects.toBeInstanceOf(UnknownDeviceError);
});
test("If connect is in success, return a deviceSession id", async () => {
jest
- .spyOn(transport, "connect")
+ .spyOn(transports[0]!, "connect")
.mockResolvedValue(Promise.resolve(Right(stubConnectedDevice)));
const usecase = new ConnectUseCase(
- transport,
+ transports,
sessionService,
() => logger,
managerApi,
);
- const sessionId = await usecase.execute({ deviceId: "" });
+ const sessionId = await usecase.execute({
+ device: stubDiscoveredDevice,
+ });
expect(sessionId).toBe(fakeSessionId);
+ sessionService.removeDeviceSession(sessionId);
});
});
diff --git a/packages/core/src/internal/discovery/use-case/ConnectUseCase.ts b/packages/device-management-kit/src/internal/discovery/use-case/ConnectUseCase.ts
similarity index 71%
rename from packages/core/src/internal/discovery/use-case/ConnectUseCase.ts
rename to packages/device-management-kit/src/internal/discovery/use-case/ConnectUseCase.ts
index f460aac5d..d12771e3b 100644
--- a/packages/core/src/internal/discovery/use-case/ConnectUseCase.ts
+++ b/packages/device-management-kit/src/internal/discovery/use-case/ConnectUseCase.ts
@@ -1,6 +1,8 @@
-import { inject, injectable } from "inversify";
+import { inject, injectable, multiInject } from "inversify";
import { DeviceSessionId } from "@api/device-session/types";
+import { DiscoveredDevice } from "@api/transport/model/DiscoveredDevice";
+import type { Transport } from "@api/transport/model/Transport";
import { DeviceId } from "@api/types";
import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes";
import { DeviceSession } from "@internal/device-session/model/DeviceSession";
@@ -9,8 +11,8 @@ import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes";
import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import { managerApiTypes } from "@internal/manager-api/di/managerApiTypes";
import type { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
-import { usbDiTypes } from "@internal/usb/di/usbDiTypes";
-import type { UsbHidTransport } from "@internal/usb/transport/UsbHidTransport";
+import { transportDiTypes } from "@internal/transport/di/transportDiTypes";
+import { TransportNotSupportedError } from "@internal/transport/model/Errors";
/**
* The arguments for the ConnectUseCase.
@@ -19,23 +21,23 @@ export type ConnectUseCaseArgs = {
/**
* UUID of the device got from device discovery `StartDiscoveringUseCase`
*/
- deviceId: DeviceId;
+ device: DiscoveredDevice;
};
/**
- * Connects to a discovered device via USB HID (and later BLE).
+ * Connects to a discovered device.
*/
@injectable()
export class ConnectUseCase {
- private readonly _usbHidTransport: UsbHidTransport;
+ private readonly _transports: Transport[];
private readonly _sessionService: DeviceSessionService;
private readonly _loggerFactory: (tag: string) => LoggerPublisherService;
private readonly _managerApi: ManagerApiService;
private readonly _logger: LoggerPublisherService;
constructor(
- @inject(usbDiTypes.UsbHidTransport)
- usbHidTransport: UsbHidTransport,
+ @multiInject(transportDiTypes.Transport)
+ transports: Transport[],
@inject(deviceSessionTypes.DeviceSessionService)
sessionService: DeviceSessionService,
@inject(loggerTypes.LoggerPublisherServiceFactory)
@@ -44,7 +46,7 @@ export class ConnectUseCase {
managerApi: ManagerApiService,
) {
this._sessionService = sessionService;
- this._usbHidTransport = usbHidTransport;
+ this._transports = transports;
this._loggerFactory = loggerFactory;
this._logger = loggerFactory("ConnectUseCase");
this._managerApi = managerApi;
@@ -58,16 +60,22 @@ export class ConnectUseCase {
});
}
- async execute({ deviceId }: ConnectUseCaseArgs): Promise {
- const either = await this._usbHidTransport.connect({
- deviceId,
+ async execute({ device }: ConnectUseCaseArgs): Promise {
+ const transport = this._transports.find(
+ (t) => t.getIdentifier() === device.transport,
+ );
+ if (!transport) {
+ throw new TransportNotSupportedError(new Error("Unknown transport"));
+ }
+ const either = await transport.connect({
+ deviceId: device.id,
onDisconnect: (dId) => this.handleDeviceDisconnect(dId),
});
return either.caseOf({
Left: (error) => {
this._logger.error("Error connecting to device", {
- data: { deviceId, error },
+ data: { deviceId: device.id, error },
});
throw error;
},
diff --git a/packages/core/src/internal/discovery/use-case/DisconnectUseCase.test.ts b/packages/device-management-kit/src/internal/discovery/use-case/DisconnectUseCase.test.ts
similarity index 75%
rename from packages/core/src/internal/discovery/use-case/DisconnectUseCase.test.ts
rename to packages/device-management-kit/src/internal/discovery/use-case/DisconnectUseCase.test.ts
index 49b18d3ce..11cb0cda1 100644
--- a/packages/core/src/internal/discovery/use-case/DisconnectUseCase.test.ts
+++ b/packages/device-management-kit/src/internal/discovery/use-case/DisconnectUseCase.test.ts
@@ -1,23 +1,24 @@
import { Left, Right } from "purify-ts";
-import { DeviceModelDataSource } from "@internal/device-model/data/DeviceModelDataSource";
+import { type DeviceModelDataSource } from "@internal/device-model/data/DeviceModelDataSource";
import { deviceSessionStubBuilder } from "@internal/device-session/model/DeviceSession.stub";
import { DeviceSessionNotFound } from "@internal/device-session/model/Errors";
import { DefaultDeviceSessionService } from "@internal/device-session/service/DefaultDeviceSessionService";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource";
-import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
+import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService";
-import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
-import { DisconnectError } from "@internal/usb/model/Errors";
-import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnectedDevice.stub";
-import { usbHidDeviceConnectionFactoryStubBuilder } from "@internal/usb/service/UsbHidDeviceConnectionFactory.stub";
-import { WebUsbHidTransport } from "@internal/usb/transport/WebUsbHidTransport";
+import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
+import { DisconnectError } from "@internal/transport/model/Errors";
+import { connectedDeviceStubBuilder } from "@internal/transport/model/InternalConnectedDevice.stub";
+import { usbHidDeviceConnectionFactoryStubBuilder } from "@internal/transport/usb/service/UsbHidDeviceConnectionFactory.stub";
+import { WebUsbHidTransport } from "@internal/transport/usb/transport/WebUsbHidTransport";
import { DisconnectUseCase } from "./DisconnectUseCase";
let sessionService: DefaultDeviceSessionService;
-let usbHidTransport: WebUsbHidTransport;
+// TODO test several transports
+let transports: WebUsbHidTransport[] = [];
const loggerFactory = jest
.fn()
.mockReturnValue(
@@ -31,11 +32,13 @@ const sessionId = "sessionId";
describe("DisconnectUseCase", () => {
beforeAll(() => {
- usbHidTransport = new WebUsbHidTransport(
- {} as DeviceModelDataSource,
- loggerFactory,
- usbHidDeviceConnectionFactoryStubBuilder(),
- );
+ transports = [
+ new WebUsbHidTransport(
+ {} as DeviceModelDataSource,
+ loggerFactory,
+ usbHidDeviceConnectionFactoryStubBuilder(),
+ ),
+ ];
sessionService = new DefaultDeviceSessionService(loggerFactory);
});
@@ -44,6 +47,7 @@ describe("DisconnectUseCase", () => {
const connectedDevice = connectedDeviceStubBuilder();
managerApiDataSource = new AxiosManagerApiDataSource({
managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
});
managerApi = new DefaultManagerApiService(managerApiDataSource);
const deviceSession = deviceSessionStubBuilder(
@@ -60,10 +64,10 @@ describe("DisconnectUseCase", () => {
jest.spyOn(deviceSession, "close");
jest.spyOn(sessionService, "removeDeviceSession");
jest
- .spyOn(usbHidTransport, "disconnect")
+ .spyOn(transports[0]!, "disconnect")
.mockImplementation(() => Promise.resolve(Right(void 0)));
const disconnectUseCase = new DisconnectUseCase(
- usbHidTransport,
+ transports,
sessionService,
loggerFactory,
);
@@ -73,7 +77,7 @@ describe("DisconnectUseCase", () => {
// Then
expect(deviceSession.close).toHaveBeenCalled();
expect(sessionService.removeDeviceSession).toHaveBeenCalledWith(sessionId);
- expect(usbHidTransport.disconnect).toHaveBeenCalledWith({
+ expect(transports[0]!.disconnect).toHaveBeenCalledWith({
connectedDevice,
});
});
@@ -81,7 +85,7 @@ describe("DisconnectUseCase", () => {
it("should throw an error when deviceSession not found", async () => {
// Given
const disconnectUseCase = new DisconnectUseCase(
- usbHidTransport,
+ transports,
sessionService,
loggerFactory,
);
@@ -108,10 +112,10 @@ describe("DisconnectUseCase", () => {
),
);
jest
- .spyOn(usbHidTransport, "disconnect")
+ .spyOn(transports[0]!, "disconnect")
.mockResolvedValue(Promise.resolve(Left(new DisconnectError())));
const disconnectUseCase = new DisconnectUseCase(
- usbHidTransport,
+ transports,
sessionService,
loggerFactory,
);
diff --git a/packages/core/src/internal/discovery/use-case/DisconnectUseCase.ts b/packages/device-management-kit/src/internal/discovery/use-case/DisconnectUseCase.ts
similarity index 67%
rename from packages/core/src/internal/discovery/use-case/DisconnectUseCase.ts
rename to packages/device-management-kit/src/internal/discovery/use-case/DisconnectUseCase.ts
index 143df7036..dfe0aabb1 100644
--- a/packages/core/src/internal/discovery/use-case/DisconnectUseCase.ts
+++ b/packages/device-management-kit/src/internal/discovery/use-case/DisconnectUseCase.ts
@@ -1,42 +1,43 @@
-import { inject, injectable } from "inversify";
+import { inject, injectable, multiInject } from "inversify";
+import type { Transport } from "@api/transport/model/Transport";
import type { DeviceSessionId } from "@api/types";
import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes";
import type { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes";
import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
-import { usbDiTypes } from "@internal/usb/di/usbDiTypes";
-import type { UsbHidTransport } from "@internal/usb/transport/UsbHidTransport";
+import { transportDiTypes } from "@internal/transport/di/transportDiTypes";
+import { TransportNotSupportedError } from "@internal/transport/model/Errors";
/**
* The arguments for the DisconnectUseCase.
*/
export type DisconnectUseCaseArgs = {
/**
- * Device session identifier from `DeviceSdk.connect`.
+ * Device session identifier from `DeviceManagementKit.connect`.
*/
sessionId: DeviceSessionId;
};
/**
- * Disconnects to a discovered device via USB HID (and later BLE).
+ * Disconnects to a discovered device.
*/
@injectable()
export class DisconnectUseCase {
- private readonly _usbHidTransport: UsbHidTransport;
+ private readonly _transports: Transport[];
private readonly _sessionService: DeviceSessionService;
private readonly _logger: LoggerPublisherService;
constructor(
- @inject(usbDiTypes.UsbHidTransport)
- usbHidTransport: UsbHidTransport,
+ @multiInject(transportDiTypes.Transport)
+ transports: Transport[],
@inject(deviceSessionTypes.DeviceSessionService)
sessionService: DeviceSessionService,
@inject(loggerTypes.LoggerPublisherServiceFactory)
loggerFactory: (tag: string) => LoggerPublisherService,
) {
this._sessionService = sessionService;
- this._usbHidTransport = usbHidTransport;
+ this._transports = transports;
this._logger = loggerFactory("DisconnectUseCase");
}
@@ -51,11 +52,17 @@ export class DisconnectUseCase {
throw error;
},
Right: async (deviceSession) => {
- deviceSession.close();
+ const transportIdentifier = deviceSession.connectedDevice.transport;
+ const transport = this._transports.find(
+ (t) => t.getIdentifier() === transportIdentifier,
+ );
+ if (!transport) {
+ throw new TransportNotSupportedError(new Error("Unknown transport"));
+ }
+ deviceSession.close();
this._sessionService.removeDeviceSession(sessionId);
-
- await this._usbHidTransport
+ await transport
.disconnect({
connectedDevice: deviceSession.connectedDevice,
})
diff --git a/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.test.ts b/packages/device-management-kit/src/internal/discovery/use-case/GetConnectedDeviceUseCase.test.ts
similarity index 77%
rename from packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.test.ts
rename to packages/device-management-kit/src/internal/discovery/use-case/GetConnectedDeviceUseCase.test.ts
index bf9fac291..7c4e755e2 100644
--- a/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.test.ts
+++ b/packages/device-management-kit/src/internal/discovery/use-case/GetConnectedDeviceUseCase.test.ts
@@ -1,14 +1,15 @@
+import { ConnectedDevice } from "@api/transport/model/ConnectedDevice";
import { deviceSessionStubBuilder } from "@internal/device-session/model/DeviceSession.stub";
import { DefaultDeviceSessionService } from "@internal/device-session/service/DefaultDeviceSessionService";
-import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
+import { type DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource";
-import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
+import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService";
-import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
-import { GetConnectedDeviceUseCase } from "@internal/usb/use-case/GetConnectedDeviceUseCase";
-import { ConnectedDevice } from "@root/src";
+import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
+
+import { GetConnectedDeviceUseCase } from "./GetConnectedDeviceUseCase";
jest.mock("@internal/manager-api/data/AxiosManagerApiDataSource");
@@ -27,6 +28,7 @@ describe("GetConnectedDevice", () => {
);
managerApiDataSource = new AxiosManagerApiDataSource({
managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
});
managerApi = new DefaultManagerApiService(managerApiDataSource);
sessionService = new DefaultDeviceSessionService(() => logger);
@@ -47,6 +49,8 @@ describe("GetConnectedDevice", () => {
sessionId: fakeSessionId,
});
+ deviceSession.close();
+
// then
expect(response).toBeInstanceOf(ConnectedDevice);
});
@@ -66,10 +70,13 @@ describe("GetConnectedDevice", () => {
sessionId: fakeSessionId,
});
+ deviceSession.close();
+
// then
expect(response).toStrictEqual(
new ConnectedDevice({
internalConnectedDevice: deviceSession.connectedDevice,
+ sessionId: fakeSessionId,
}),
);
});
diff --git a/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.ts b/packages/device-management-kit/src/internal/discovery/use-case/GetConnectedDeviceUseCase.ts
similarity index 93%
rename from packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.ts
rename to packages/device-management-kit/src/internal/discovery/use-case/GetConnectedDeviceUseCase.ts
index 4a56fa5b5..a8ff1ccdf 100644
--- a/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.ts
+++ b/packages/device-management-kit/src/internal/discovery/use-case/GetConnectedDeviceUseCase.ts
@@ -1,7 +1,7 @@
import { inject, injectable } from "inversify";
import { DeviceSessionId } from "@api/device-session/types";
-import { ConnectedDevice } from "@api/usb/model/ConnectedDevice";
+import { ConnectedDevice } from "@api/transport/model/ConnectedDevice";
import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes";
import type { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes";
@@ -37,6 +37,7 @@ export class GetConnectedDeviceUseCase {
Right: (deviceSession) =>
new ConnectedDevice({
internalConnectedDevice: deviceSession.connectedDevice,
+ sessionId: deviceSession.id,
}),
Left: (error) => {
this._logger.error("Error getting session", {
diff --git a/packages/core/src/internal/device-session/use-case/ListDeviceSessionsUseCase.test.ts b/packages/device-management-kit/src/internal/discovery/use-case/ListConnectedDevicesUseCase.test.ts
similarity index 60%
rename from packages/core/src/internal/device-session/use-case/ListDeviceSessionsUseCase.test.ts
rename to packages/device-management-kit/src/internal/discovery/use-case/ListConnectedDevicesUseCase.test.ts
index 2ecbc9105..86995de87 100644
--- a/packages/core/src/internal/device-session/use-case/ListDeviceSessionsUseCase.test.ts
+++ b/packages/device-management-kit/src/internal/discovery/use-case/ListConnectedDevicesUseCase.test.ts
@@ -1,14 +1,14 @@
import { deviceSessionStubBuilder } from "@internal/device-session/model/DeviceSession.stub";
import { DefaultDeviceSessionService } from "@internal/device-session/service/DefaultDeviceSessionService";
-import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
+import { type DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
+import { ListConnectedDevicesUseCase } from "@internal/discovery/use-case/ListConnectedDevicesUseCase";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource";
-import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
+import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService";
-import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
-
-import { ListDeviceSessionsUseCase } from "./ListDeviceSessionsUseCase";
+import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
+import { ConnectedDevice } from "@root/src";
let logger: LoggerPublisherService;
let sessionService: DeviceSessionService;
@@ -23,6 +23,7 @@ describe("ListDeviceSessionsUseCase", () => {
);
managerApiDataSource = new AxiosManagerApiDataSource({
managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
});
managerApi = new DefaultManagerApiService(managerApiDataSource);
sessionService = new DefaultDeviceSessionService(() => logger);
@@ -42,18 +43,36 @@ describe("ListDeviceSessionsUseCase", () => {
);
sessionService.addDeviceSession(deviceSession1);
sessionService.addDeviceSession(deviceSession2);
- const useCase = new ListDeviceSessionsUseCase(sessionService, () => logger);
+ const useCase = new ListConnectedDevicesUseCase(
+ sessionService,
+ () => logger,
+ );
// when
const response = useCase.execute();
+ deviceSession1.close();
+ deviceSession2.close();
+
// then
- expect(response).toStrictEqual([deviceSession1, deviceSession2]);
+ expect(response).toStrictEqual([
+ new ConnectedDevice({
+ internalConnectedDevice: deviceSession1.connectedDevice,
+ sessionId: deviceSession1.id,
+ }),
+ new ConnectedDevice({
+ internalConnectedDevice: deviceSession2.connectedDevice,
+ sessionId: deviceSession2.id,
+ }),
+ ]);
});
it("should return empty array if no device sessions", () => {
// given
- const useCase = new ListDeviceSessionsUseCase(sessionService, () => logger);
+ const useCase = new ListConnectedDevicesUseCase(
+ sessionService,
+ () => logger,
+ );
// when
const response = useCase.execute();
diff --git a/packages/core/src/internal/device-session/use-case/ListDeviceSessionsUseCase.ts b/packages/device-management-kit/src/internal/discovery/use-case/ListConnectedDevicesUseCase.ts
similarity index 61%
rename from packages/core/src/internal/device-session/use-case/ListDeviceSessionsUseCase.ts
rename to packages/device-management-kit/src/internal/discovery/use-case/ListConnectedDevicesUseCase.ts
index 9a4cf678f..e675aa8c3 100644
--- a/packages/core/src/internal/device-session/use-case/ListDeviceSessionsUseCase.ts
+++ b/packages/device-management-kit/src/internal/discovery/use-case/ListConnectedDevicesUseCase.ts
@@ -1,16 +1,16 @@
import { inject, injectable } from "inversify";
+import { ConnectedDevice } from "@api/transport/model/ConnectedDevice";
import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes";
-import { DeviceSession } from "@internal/device-session/model/DeviceSession";
import type { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes";
import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
/**
- * List all device sessions.
+ * List all connected devices.
*/
@injectable()
-export class ListDeviceSessionsUseCase {
+export class ListConnectedDevicesUseCase {
private readonly _sessionService: DeviceSessionService;
private readonly _logger: LoggerPublisherService;
@@ -21,11 +21,17 @@ export class ListDeviceSessionsUseCase {
loggerFactory: (tag: string) => LoggerPublisherService,
) {
this._sessionService = sessionService;
- this._logger = loggerFactory("ListDeviceSessionsUseCase");
+ this._logger = loggerFactory("ListConnectedDeviceUseCase");
}
- execute(): DeviceSession[] {
- this._logger.info("Listing device sessions");
- return this._sessionService.getDeviceSessions();
+ execute(): ConnectedDevice[] {
+ this._logger.info("Listing connected devices");
+ return this._sessionService.getDeviceSessions().map(
+ (session) =>
+ new ConnectedDevice({
+ internalConnectedDevice: session.connectedDevice,
+ sessionId: session.id,
+ }),
+ );
}
}
diff --git a/packages/device-management-kit/src/internal/discovery/use-case/ListenToConnectedDeviceUseCase.test.ts b/packages/device-management-kit/src/internal/discovery/use-case/ListenToConnectedDeviceUseCase.test.ts
new file mode 100644
index 000000000..bf6cc2dcd
--- /dev/null
+++ b/packages/device-management-kit/src/internal/discovery/use-case/ListenToConnectedDeviceUseCase.test.ts
@@ -0,0 +1,74 @@
+import { ConnectedDevice } from "@api/transport/model/ConnectedDevice";
+import { deviceSessionStubBuilder } from "@internal/device-session/model/DeviceSession.stub";
+import { DefaultDeviceSessionService } from "@internal/device-session/service/DefaultDeviceSessionService";
+import { type DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
+import { ListenToConnectedDeviceUseCase } from "@internal/discovery/use-case/ListenToConnectedDeviceUseCase";
+import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource";
+import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
+import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService";
+import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
+import { connectedDeviceStubBuilder } from "@internal/transport/model/InternalConnectedDevice.stub";
+
+jest.mock("@internal/manager-api/data/AxiosManagerApiDataSource");
+
+let logger: LoggerPublisherService;
+let sessionService: DeviceSessionService;
+let managerApiDataSource: ManagerApiDataSource;
+let managerApi: ManagerApiService;
+
+const fakeSessionId = "test-list-connected-device-session-id";
+
+describe("ListenToConnectedDevice", () => {
+ beforeEach(() => {
+ logger = new DefaultLoggerPublisherService(
+ [],
+ "listen-to-connected-device-use-case",
+ );
+ managerApiDataSource = new AxiosManagerApiDataSource({
+ managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
+ });
+ managerApi = new DefaultManagerApiService(managerApiDataSource);
+ sessionService = new DefaultDeviceSessionService(() => logger);
+ });
+
+ it("should emit an instance of ConnectedDevice", (done) => {
+ // given
+ const connectedDevice = connectedDeviceStubBuilder({
+ id: "test-list-connected-device-id",
+ });
+ const deviceSession = deviceSessionStubBuilder(
+ { id: fakeSessionId, connectedDevice },
+ () => logger,
+ managerApi,
+ );
+ const observable = new ListenToConnectedDeviceUseCase(
+ sessionService,
+ () => logger,
+ ).execute();
+
+ const subscription = observable.subscribe({
+ next(emittedConnectedDevice) {
+ // then
+ expect(emittedConnectedDevice).toEqual(
+ new ConnectedDevice({
+ internalConnectedDevice: connectedDevice,
+ sessionId: fakeSessionId,
+ }),
+ );
+ terminate();
+ },
+ });
+
+ function terminate() {
+ subscription.unsubscribe();
+ deviceSession.close();
+ done();
+ }
+
+ // when
+ sessionService.addDeviceSession(deviceSession);
+ });
+});
diff --git a/packages/device-management-kit/src/internal/discovery/use-case/ListenToConnectedDeviceUseCase.ts b/packages/device-management-kit/src/internal/discovery/use-case/ListenToConnectedDeviceUseCase.ts
new file mode 100644
index 000000000..082f4e489
--- /dev/null
+++ b/packages/device-management-kit/src/internal/discovery/use-case/ListenToConnectedDeviceUseCase.ts
@@ -0,0 +1,38 @@
+import { inject, injectable } from "inversify";
+import { map, Observable } from "rxjs";
+
+import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes";
+import type { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
+import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes";
+import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { ConnectedDevice } from "@root/src";
+
+/**
+ * Listen to connected devices
+ */
+@injectable()
+export class ListenToConnectedDeviceUseCase {
+ private readonly _logger: LoggerPublisherService;
+
+ constructor(
+ @inject(deviceSessionTypes.DeviceSessionService)
+ private readonly _sessionService: DeviceSessionService,
+ @inject(loggerTypes.LoggerPublisherServiceFactory)
+ loggerFactory: (tag: string) => LoggerPublisherService,
+ ) {
+ this._logger = loggerFactory("ListenToConnectedDeviceUseCase");
+ }
+
+ execute(): Observable {
+ this._logger.info("Observe connected devices");
+ return this._sessionService.sessionsObs.pipe(
+ map(
+ (deviceSession) =>
+ new ConnectedDevice({
+ internalConnectedDevice: deviceSession.connectedDevice,
+ sessionId: deviceSession.id,
+ }),
+ ),
+ );
+ }
+}
diff --git a/packages/device-management-kit/src/internal/discovery/use-case/ListenToKnownDevicesUseCase.test.ts b/packages/device-management-kit/src/internal/discovery/use-case/ListenToKnownDevicesUseCase.test.ts
new file mode 100644
index 000000000..bf14ec63e
--- /dev/null
+++ b/packages/device-management-kit/src/internal/discovery/use-case/ListenToKnownDevicesUseCase.test.ts
@@ -0,0 +1,303 @@
+import { Subject } from "rxjs";
+
+import { type DeviceId, type DeviceModel } from "@api/device/DeviceModel";
+import { type DiscoveredDevice, type Transport } from "@api/types";
+import { deviceModelStubBuilder } from "@internal/device-model/model/DeviceModel.stub";
+import { type InternalDiscoveredDevice } from "@internal/transport/model/InternalDiscoveredDevice";
+
+import { ListenToKnownDevicesUseCase } from "./ListenToKnownDevicesUseCase";
+
+function makeMockTransport(props: Partial): Transport {
+ return {
+ listenToKnownDevices: jest.fn(),
+ connect: jest.fn(),
+ disconnect: jest.fn(),
+ startDiscovering: jest.fn(),
+ stopDiscovering: jest.fn(),
+ getIdentifier: jest.fn(),
+ isSupported: jest.fn(),
+ ...props,
+ };
+}
+
+const mockInternalDeviceModel = deviceModelStubBuilder();
+function makeMockDeviceModel(id: DeviceId): DeviceModel {
+ return {
+ id,
+ model: mockInternalDeviceModel.id,
+ name: mockInternalDeviceModel.productName,
+ };
+}
+
+function setup2MockTransports() {
+ const transportAKnownDevicesSubject = new Subject<
+ InternalDiscoveredDevice[]
+ >();
+ const transportBKnownDevicesSubject = new Subject<
+ InternalDiscoveredDevice[]
+ >();
+ const transportA = makeMockTransport({
+ listenToKnownDevices: () => transportAKnownDevicesSubject.asObservable(),
+ });
+ const transportB = makeMockTransport({
+ listenToKnownDevices: () => transportBKnownDevicesSubject.asObservable(),
+ });
+ return {
+ transportAKnownDevicesSubject,
+ transportBKnownDevicesSubject,
+ transportA,
+ transportB,
+ };
+}
+
+function makeMockInternalDiscoveredDevice(
+ id: string,
+): InternalDiscoveredDevice {
+ return {
+ id,
+ deviceModel: mockInternalDeviceModel,
+ transport: "mock",
+ };
+}
+
+describe("ListenToKnownDevicesUseCase", () => {
+ describe("when no transports are available", () => {
+ it("should return no discovered devices", (done) => {
+ const useCase = new ListenToKnownDevicesUseCase([]);
+
+ const observedDiscoveredDevices: DiscoveredDevice[][] = [];
+ useCase.execute().subscribe({
+ next: (devices) => {
+ observedDiscoveredDevices.push(devices);
+ },
+ complete: () => {
+ try {
+ expect(observedDiscoveredDevices).toEqual([[]]);
+ done();
+ } catch (error) {
+ done(error);
+ }
+ },
+ error: (error) => {
+ done(error);
+ },
+ });
+ });
+ });
+
+ describe("when one transport is available", () => {
+ it("should return discovered devices from one transport", () => {
+ const { transportA, transportAKnownDevicesSubject } =
+ setup2MockTransports();
+
+ const observedDiscoveredDevices: DiscoveredDevice[][] = [];
+ new ListenToKnownDevicesUseCase([transportA])
+ .execute()
+ .subscribe((devices) => {
+ observedDiscoveredDevices.push(devices);
+ });
+
+ // When transportA emits 1 known device
+ transportAKnownDevicesSubject.next([
+ makeMockInternalDiscoveredDevice("transportA-device1"),
+ ]);
+
+ expect(observedDiscoveredDevices[0]).toEqual([
+ {
+ id: "transportA-device1",
+ deviceModel: makeMockDeviceModel("transportA-device1"),
+ transport: "mock",
+ },
+ ]);
+
+ // When transportA emits 2 known devices
+ transportAKnownDevicesSubject.next([
+ makeMockInternalDiscoveredDevice("transportA-device1"),
+ makeMockInternalDiscoveredDevice("transportA-device2"),
+ ]);
+
+ expect(observedDiscoveredDevices[1]).toEqual([
+ {
+ id: "transportA-device1",
+ deviceModel: makeMockDeviceModel("transportA-device1"),
+ transport: "mock",
+ },
+ {
+ id: "transportA-device2",
+ deviceModel: makeMockDeviceModel("transportA-device2"),
+ transport: "mock",
+ },
+ ]);
+
+ // When transportA emits 1 known device (device1 disconnects)
+ transportAKnownDevicesSubject.next([
+ makeMockInternalDiscoveredDevice("transportA-device2"),
+ ]);
+
+ expect(observedDiscoveredDevices[2]).toEqual([
+ {
+ id: "transportA-device2",
+ deviceModel: makeMockDeviceModel("transportA-device2"),
+ transport: "mock",
+ },
+ ]);
+
+ // When transportA emits 0 known devices (device2 disconnects)
+ transportAKnownDevicesSubject.next([]);
+
+ expect(observedDiscoveredDevices[3]).toEqual([]);
+ });
+ });
+
+ describe("when multiple transports are available", () => {
+ it("should return discovered devices from one of the transports as soon as it emits", () => {
+ const { transportAKnownDevicesSubject, transportA, transportB } =
+ setup2MockTransports();
+
+ const observedDiscoveredDevices: DiscoveredDevice[][] = [];
+
+ const onError = jest.fn();
+ const onComplete = jest.fn();
+
+ new ListenToKnownDevicesUseCase([transportA, transportB])
+ .execute()
+ .subscribe({
+ next: (devices) => {
+ observedDiscoveredDevices.push(devices);
+ },
+ error: onError,
+ complete: onComplete,
+ });
+
+ // When transportA emits 1 known device
+ transportAKnownDevicesSubject.next([
+ makeMockInternalDiscoveredDevice("transportA-device1"),
+ ]);
+
+ expect(observedDiscoveredDevices[0]).toEqual([
+ {
+ id: "transportA-device1",
+ deviceModel: makeMockDeviceModel("transportA-device1"),
+ transport: "mock",
+ },
+ ]);
+
+ // When transport A listen observable completes
+ transportAKnownDevicesSubject.complete();
+
+ expect(onError).not.toHaveBeenCalled();
+ expect(onComplete).not.toHaveBeenCalled(); // Should not complete yet because transportB has not completed
+ });
+
+ it("should combine discovered devices from multiple transports", () => {
+ const {
+ transportAKnownDevicesSubject,
+ transportBKnownDevicesSubject,
+ transportA,
+ transportB,
+ } = setup2MockTransports();
+
+ const observedDiscoveredDevices: DiscoveredDevice[][] = [];
+
+ const onError = jest.fn();
+ const onComplete = jest.fn();
+ new ListenToKnownDevicesUseCase([transportA, transportB])
+ .execute()
+ .subscribe({
+ next: (devices) => {
+ observedDiscoveredDevices.push(devices);
+ },
+ error: onError,
+ complete: onComplete,
+ });
+
+ // When transportA emits 1 known device
+ transportAKnownDevicesSubject.next([
+ makeMockInternalDiscoveredDevice("transportA-device1"),
+ ]);
+
+ expect(observedDiscoveredDevices[0]).toEqual([
+ {
+ id: "transportA-device1",
+ deviceModel: makeMockDeviceModel("transportA-device1"),
+ transport: "mock",
+ },
+ ]);
+
+ // When transportB emits 1 known device
+ transportBKnownDevicesSubject.next([
+ makeMockInternalDiscoveredDevice("transportB-device1"),
+ ]);
+
+ expect(observedDiscoveredDevices[1]).toEqual([
+ {
+ id: "transportA-device1",
+ deviceModel: makeMockDeviceModel("transportA-device1"),
+ transport: "mock",
+ },
+ {
+ id: "transportB-device1",
+ deviceModel: makeMockDeviceModel("transportB-device1"),
+ transport: "mock",
+ },
+ ]);
+
+ // When transportB emits 2 known devices
+ transportBKnownDevicesSubject.next([
+ makeMockInternalDiscoveredDevice("transportB-device1"),
+ makeMockInternalDiscoveredDevice("transportB-device2"),
+ ]);
+
+ expect(observedDiscoveredDevices[2]).toEqual([
+ {
+ id: "transportA-device1",
+ deviceModel: makeMockDeviceModel("transportA-device1"),
+ transport: "mock",
+ },
+ {
+ id: "transportB-device1",
+ deviceModel: makeMockDeviceModel("transportB-device1"),
+ transport: "mock",
+ },
+ {
+ id: "transportB-device2",
+ deviceModel: makeMockDeviceModel("transportB-device2"),
+ transport: "mock",
+ },
+ ]);
+
+ // When transportA emits 0 known devices
+ transportAKnownDevicesSubject.next([]);
+
+ expect(observedDiscoveredDevices[3]).toEqual([
+ {
+ id: "transportB-device1",
+ deviceModel: makeMockDeviceModel("transportB-device1"),
+ transport: "mock",
+ },
+ {
+ id: "transportB-device2",
+ deviceModel: makeMockDeviceModel("transportB-device2"),
+ transport: "mock",
+ },
+ ]);
+
+ // When transport A listen observable completes
+ transportAKnownDevicesSubject.complete();
+
+ expect(onError).not.toHaveBeenCalled();
+ expect(onComplete).not.toHaveBeenCalled(); // Should not complete yet because transportB has not completed
+
+ // When transport B emits 0 known devices
+ transportBKnownDevicesSubject.next([]);
+
+ expect(observedDiscoveredDevices[4]).toEqual([]);
+
+ // When transport B listen observable completes
+ transportBKnownDevicesSubject.complete();
+
+ expect(onError).not.toHaveBeenCalled();
+ expect(onComplete).toHaveBeenCalled(); // Should complete now because all transports have completed
+ });
+ });
+});
diff --git a/packages/device-management-kit/src/internal/discovery/use-case/ListenToKnownDevicesUseCase.ts b/packages/device-management-kit/src/internal/discovery/use-case/ListenToKnownDevicesUseCase.ts
new file mode 100644
index 000000000..1b0560622
--- /dev/null
+++ b/packages/device-management-kit/src/internal/discovery/use-case/ListenToKnownDevicesUseCase.ts
@@ -0,0 +1,74 @@
+import { injectable, multiInject } from "inversify";
+import { from, map, merge, Observable, scan } from "rxjs";
+
+import { DeviceModel } from "@api/device/DeviceModel";
+import type { Transport } from "@api/transport/model/Transport";
+import { DiscoveredDevice } from "@api/types";
+import { transportDiTypes } from "@internal/transport/di/transportDiTypes";
+import { InternalDiscoveredDevice } from "@internal/transport/model/InternalDiscoveredDevice";
+
+/**
+ * Listen to list of known discovered devices (and later BLE).
+ */
+@injectable()
+export class ListenToKnownDevicesUseCase {
+ private readonly _transports: Transport[];
+ constructor(
+ @multiInject(transportDiTypes.Transport)
+ transports: Transport[],
+ ) {
+ this._transports = transports;
+ }
+
+ private mapInternalDiscoveredDeviceToDiscoveredDevice(
+ discoveredDevice: InternalDiscoveredDevice,
+ ): DiscoveredDevice {
+ return {
+ id: discoveredDevice.id,
+ deviceModel: new DeviceModel({
+ id: discoveredDevice.id,
+ model: discoveredDevice.deviceModel.id,
+ name: discoveredDevice.deviceModel.productName,
+ }),
+ transport: discoveredDevice.transport,
+ };
+ }
+
+ execute(): Observable {
+ if (this._transports.length === 0) {
+ return from([[]]);
+ }
+
+ /**
+ * Note: we're not using combineLatest because combineLatest will
+ * - wait for all observables to emit at least once before emitting.
+ * - complete as soon as one of the observables completes.
+ * Some transports will just return an empty array and complete.
+ * We want to keep listening to all transports until all have completed.
+ */
+
+ const observablesWithIndex = this._transports.map((transport, index) =>
+ transport.listenToKnownDevices().pipe(
+ map((arr) => ({
+ index,
+ arr,
+ })),
+ ),
+ );
+
+ return merge(...observablesWithIndex).pipe(
+ scan(
+ (acc, { index, arr }) => {
+ acc[index] = arr;
+ return acc;
+ },
+ {} as { [key: number]: Array },
+ ),
+ map((acc) =>
+ Object.values(acc)
+ .flat()
+ .map(this.mapInternalDiscoveredDeviceToDiscoveredDevice),
+ ),
+ );
+ }
+}
diff --git a/packages/core/src/internal/discovery/use-case/StartDiscoveringUseCase.test.ts b/packages/device-management-kit/src/internal/discovery/use-case/StartDiscoveringUseCase.test.ts
similarity index 69%
rename from packages/core/src/internal/discovery/use-case/StartDiscoveringUseCase.test.ts
rename to packages/device-management-kit/src/internal/discovery/use-case/StartDiscoveringUseCase.test.ts
index 3ee6fab44..dd31dbaec 100644
--- a/packages/core/src/internal/discovery/use-case/StartDiscoveringUseCase.test.ts
+++ b/packages/device-management-kit/src/internal/discovery/use-case/StartDiscoveringUseCase.test.ts
@@ -1,14 +1,14 @@
import { of } from "rxjs";
import { DeviceModel } from "@api/device/DeviceModel";
-import { DeviceModelId, DiscoveredDevice } from "@api/types";
-import { DeviceModelDataSource } from "@internal/device-model/data/DeviceModelDataSource";
-import { InternalDeviceModel } from "@internal/device-model/model/DeviceModel";
+import { type DeviceModelId, type DiscoveredDevice } from "@api/types";
+import { type DeviceModelDataSource } from "@internal/device-model/data/DeviceModelDataSource";
+import { type InternalDeviceModel } from "@internal/device-model/model/DeviceModel";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
-import { InternalDiscoveredDevice } from "@internal/usb/model/InternalDiscoveredDevice";
-import { usbHidDeviceConnectionFactoryStubBuilder } from "@internal/usb/service/UsbHidDeviceConnectionFactory.stub";
-import { WebUsbHidTransport } from "@internal/usb/transport/WebUsbHidTransport";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { type InternalDiscoveredDevice } from "@internal/transport/model/InternalDiscoveredDevice";
+import { usbHidDeviceConnectionFactoryStubBuilder } from "@internal/transport/usb/service/UsbHidDeviceConnectionFactory.stub";
+import { WebUsbHidTransport } from "@internal/transport/usb/transport/WebUsbHidTransport";
import { StartDiscoveringUseCase } from "./StartDiscoveringUseCase";
@@ -22,6 +22,7 @@ describe("StartDiscoveringUseCase", () => {
id: "nanoSP" as DeviceModelId,
productName: "productName",
} as InternalDeviceModel,
+ transport: "USB",
};
const tag = "logger-tag";
@@ -45,15 +46,16 @@ describe("StartDiscoveringUseCase", () => {
jest
.spyOn(transport, "startDiscovering")
.mockImplementation(mockedStartDiscovering);
- const usecase = new StartDiscoveringUseCase(transport);
+ const usecase = new StartDiscoveringUseCase([transport]);
- const discover = usecase.execute();
+ const discover = usecase.execute({ transport: "USB" });
expect(mockedStartDiscovering).toHaveBeenCalled();
discover.subscribe({
next: (discoveredDevice) => {
expect(discoveredDevice).toStrictEqual({
id: "internal-discovered-device-id",
+ transport: "USB",
deviceModel: new DeviceModel({
id: "internal-discovered-device-id",
model: "nanoSP" as DeviceModelId,
diff --git a/packages/device-management-kit/src/internal/discovery/use-case/StartDiscoveringUseCase.ts b/packages/device-management-kit/src/internal/discovery/use-case/StartDiscoveringUseCase.ts
new file mode 100644
index 000000000..56b47b404
--- /dev/null
+++ b/packages/device-management-kit/src/internal/discovery/use-case/StartDiscoveringUseCase.ts
@@ -0,0 +1,71 @@
+import { injectable, multiInject } from "inversify";
+import { map, mergeMap, Observable, of } from "rxjs";
+
+import { DeviceModel } from "@api/device/DeviceModel";
+import { DiscoveredDevice } from "@api/transport/model/DiscoveredDevice";
+import type { Transport } from "@api/transport/model/Transport";
+import { TransportIdentifier } from "@api/transport/model/TransportIdentifier";
+import { transportDiTypes } from "@internal/transport/di/transportDiTypes";
+import { TransportNotSupportedError } from "@internal/transport/model/Errors";
+import { InternalDiscoveredDevice } from "@internal/transport/model/InternalDiscoveredDevice";
+
+export type StartDiscoveringUseCaseArgs = {
+ /**
+ * Identifier of the transport to start discovering devices.
+ * Can be undefined to discover all available transports in parralel.
+ */
+ transport?: TransportIdentifier;
+};
+
+/**
+ * Starts discovering devices connected.
+ *
+ * For the WebHID implementation, this use-case needs to be called as a result of an user interaction (button "click" event for ex).
+ */
+@injectable()
+export class StartDiscoveringUseCase {
+ constructor(
+ @multiInject(transportDiTypes.Transport)
+ private transports: Transport[],
+ ) {}
+
+ private mapDiscoveredDevice(
+ device: InternalDiscoveredDevice,
+ ): DiscoveredDevice {
+ const deviceModel = new DeviceModel({
+ id: device.id,
+ model: device.deviceModel.id,
+ name: device.deviceModel.productName,
+ });
+ return {
+ id: device.id,
+ deviceModel,
+ transport: device.transport,
+ };
+ }
+
+ execute({
+ transport,
+ }: StartDiscoveringUseCaseArgs): Observable {
+ if (transport) {
+ const instance = this.transports.find(
+ (t) => t.getIdentifier() === transport,
+ );
+ if (!instance) {
+ throw new TransportNotSupportedError(new Error("Unknown transport"));
+ }
+ return instance
+ .startDiscovering()
+ .pipe(map((device) => this.mapDiscoveredDevice(device)));
+ } else {
+ // Discover from all transports in parallel
+ return of(...this.transports).pipe(
+ mergeMap((instance) =>
+ instance
+ .startDiscovering()
+ .pipe(map((device) => this.mapDiscoveredDevice(device))),
+ ),
+ );
+ }
+ }
+}
diff --git a/packages/core/src/internal/discovery/use-case/StopDiscoveringUseCase.test.ts b/packages/device-management-kit/src/internal/discovery/use-case/StopDiscoveringUseCase.test.ts
similarity index 50%
rename from packages/core/src/internal/discovery/use-case/StopDiscoveringUseCase.test.ts
rename to packages/device-management-kit/src/internal/discovery/use-case/StopDiscoveringUseCase.test.ts
index a43d56cf5..7551ca79e 100644
--- a/packages/core/src/internal/discovery/use-case/StopDiscoveringUseCase.test.ts
+++ b/packages/device-management-kit/src/internal/discovery/use-case/StopDiscoveringUseCase.test.ts
@@ -1,23 +1,26 @@
-import { DeviceModelDataSource } from "@internal/device-model/data/DeviceModelDataSource";
+import { type DeviceModelDataSource } from "@internal/device-model/data/DeviceModelDataSource";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
-import { usbHidDeviceConnectionFactoryStubBuilder } from "@internal/usb/service/UsbHidDeviceConnectionFactory.stub";
-import { WebUsbHidTransport } from "@internal/usb/transport/WebUsbHidTransport";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { usbHidDeviceConnectionFactoryStubBuilder } from "@internal/transport/usb/service/UsbHidDeviceConnectionFactory.stub";
+import { WebUsbHidTransport } from "@internal/transport/usb/transport/WebUsbHidTransport";
import { StopDiscoveringUseCase } from "./StopDiscoveringUseCase";
-let transport: WebUsbHidTransport;
+// TODO test several transports
+let transports: WebUsbHidTransport[];
let logger: LoggerPublisherService;
const tag = "logger-tag";
describe("StopDiscoveringUseCase", () => {
beforeEach(() => {
logger = new DefaultLoggerPublisherService([], tag);
- transport = new WebUsbHidTransport(
- {} as DeviceModelDataSource,
- () => logger,
- usbHidDeviceConnectionFactoryStubBuilder(),
- );
+ transports = [
+ new WebUsbHidTransport(
+ {} as DeviceModelDataSource,
+ () => logger,
+ usbHidDeviceConnectionFactoryStubBuilder(),
+ ),
+ ];
});
afterEach(() => {
@@ -27,9 +30,9 @@ describe("StopDiscoveringUseCase", () => {
test("should call stop discovering", () => {
const mockedStopDiscovering = jest.fn();
jest
- .spyOn(transport, "stopDiscovering")
+ .spyOn(transports[0]!, "stopDiscovering")
.mockImplementation(mockedStopDiscovering);
- const usecase = new StopDiscoveringUseCase(transport);
+ const usecase = new StopDiscoveringUseCase(transports);
usecase.execute();
diff --git a/packages/device-management-kit/src/internal/discovery/use-case/StopDiscoveringUseCase.ts b/packages/device-management-kit/src/internal/discovery/use-case/StopDiscoveringUseCase.ts
new file mode 100644
index 000000000..d99868901
--- /dev/null
+++ b/packages/device-management-kit/src/internal/discovery/use-case/StopDiscoveringUseCase.ts
@@ -0,0 +1,21 @@
+import { injectable, multiInject } from "inversify";
+
+import type { Transport } from "@api/transport/model/Transport";
+import { transportDiTypes } from "@internal/transport/di/transportDiTypes";
+
+/**
+ * Stops discovering devices connected.
+ */
+@injectable()
+export class StopDiscoveringUseCase {
+ constructor(
+ @multiInject(transportDiTypes.Transport)
+ private transports: Transport[],
+ ) {}
+
+ execute(): void {
+ for (const transport of this.transports) {
+ transport.stopDiscovering();
+ }
+ }
+}
diff --git a/packages/core/src/internal/logger-publisher/di/loggerModule.test.ts b/packages/device-management-kit/src/internal/logger-publisher/di/loggerModule.test.ts
similarity index 100%
rename from packages/core/src/internal/logger-publisher/di/loggerModule.test.ts
rename to packages/device-management-kit/src/internal/logger-publisher/di/loggerModule.test.ts
diff --git a/packages/core/src/internal/logger-publisher/di/loggerModule.ts b/packages/device-management-kit/src/internal/logger-publisher/di/loggerModule.ts
similarity index 73%
rename from packages/core/src/internal/logger-publisher/di/loggerModule.ts
rename to packages/device-management-kit/src/internal/logger-publisher/di/loggerModule.ts
index 72e00dbe9..ec22495a1 100644
--- a/packages/core/src/internal/logger-publisher/di/loggerModule.ts
+++ b/packages/device-management-kit/src/internal/logger-publisher/di/loggerModule.ts
@@ -1,8 +1,8 @@
-import { ContainerModule, interfaces } from "inversify";
+import { ContainerModule, type interfaces } from "inversify";
-import { LoggerSubscriberService } from "@api/logger-subscriber/service/LoggerSubscriberService";
+import { type LoggerSubscriberService } from "@api/logger-subscriber/service/LoggerSubscriberService";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import { loggerTypes } from "./loggerTypes";
diff --git a/packages/core/src/internal/logger-publisher/di/loggerTypes.ts b/packages/device-management-kit/src/internal/logger-publisher/di/loggerTypes.ts
similarity index 100%
rename from packages/core/src/internal/logger-publisher/di/loggerTypes.ts
rename to packages/device-management-kit/src/internal/logger-publisher/di/loggerTypes.ts
diff --git a/packages/core/src/internal/logger-publisher/model/LogPublisherOptions.ts b/packages/device-management-kit/src/internal/logger-publisher/model/LogPublisherOptions.ts
similarity index 100%
rename from packages/core/src/internal/logger-publisher/model/LogPublisherOptions.ts
rename to packages/device-management-kit/src/internal/logger-publisher/model/LogPublisherOptions.ts
diff --git a/packages/core/src/internal/logger-publisher/service/DefaultLoggerPublisherService.test.ts b/packages/device-management-kit/src/internal/logger-publisher/service/DefaultLoggerPublisherService.test.ts
similarity index 100%
rename from packages/core/src/internal/logger-publisher/service/DefaultLoggerPublisherService.test.ts
rename to packages/device-management-kit/src/internal/logger-publisher/service/DefaultLoggerPublisherService.test.ts
diff --git a/packages/core/src/internal/logger-publisher/service/DefaultLoggerPublisherService.ts b/packages/device-management-kit/src/internal/logger-publisher/service/DefaultLoggerPublisherService.ts
similarity index 100%
rename from packages/core/src/internal/logger-publisher/service/DefaultLoggerPublisherService.ts
rename to packages/device-management-kit/src/internal/logger-publisher/service/DefaultLoggerPublisherService.ts
diff --git a/packages/core/src/internal/logger-publisher/service/LoggerPublisherService.ts b/packages/device-management-kit/src/internal/logger-publisher/service/LoggerPublisherService.ts
similarity index 62%
rename from packages/core/src/internal/logger-publisher/service/LoggerPublisherService.ts
rename to packages/device-management-kit/src/internal/logger-publisher/service/LoggerPublisherService.ts
index 69197406f..7bb19c746 100644
--- a/packages/core/src/internal/logger-publisher/service/LoggerPublisherService.ts
+++ b/packages/device-management-kit/src/internal/logger-publisher/service/LoggerPublisherService.ts
@@ -1,5 +1,5 @@
-import { LoggerSubscriberService } from "@api/logger-subscriber/service/LoggerSubscriberService";
-import { LogPublisherOptions } from "@internal/logger-publisher/model/LogPublisherOptions";
+import { type LoggerSubscriberService } from "@api/logger-subscriber/service/LoggerSubscriberService";
+import { type LogPublisherOptions } from "@internal/logger-publisher/model/LogPublisherOptions";
export interface LoggerPublisherService {
subscribers: LoggerSubscriberService[];
diff --git a/packages/core/src/internal/logger-publisher/service/__mocks__/DefaultLoggerService.ts b/packages/device-management-kit/src/internal/logger-publisher/service/__mocks__/DefaultLoggerService.ts
similarity index 76%
rename from packages/core/src/internal/logger-publisher/service/__mocks__/DefaultLoggerService.ts
rename to packages/device-management-kit/src/internal/logger-publisher/service/__mocks__/DefaultLoggerService.ts
index ffd95cca2..04da66364 100644
--- a/packages/core/src/internal/logger-publisher/service/__mocks__/DefaultLoggerService.ts
+++ b/packages/device-management-kit/src/internal/logger-publisher/service/__mocks__/DefaultLoggerService.ts
@@ -1,4 +1,4 @@
-import { LoggerSubscriberService } from "@api/logger-subscriber/service/LoggerSubscriberService";
+import { type LoggerSubscriberService } from "@api/logger-subscriber/service/LoggerSubscriberService";
import type { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
export class DefaultLoggerPublisherService implements LoggerPublisherService {
diff --git a/packages/core/src/internal/manager-api/data/AxiosManagerApiDataSource.test.ts b/packages/device-management-kit/src/internal/manager-api/data/AxiosManagerApiDataSource.test.ts
similarity index 89%
rename from packages/core/src/internal/manager-api/data/AxiosManagerApiDataSource.test.ts
rename to packages/device-management-kit/src/internal/manager-api/data/AxiosManagerApiDataSource.test.ts
index f9368d439..89ed41226 100644
--- a/packages/core/src/internal/manager-api/data/AxiosManagerApiDataSource.test.ts
+++ b/packages/device-management-kit/src/internal/manager-api/data/AxiosManagerApiDataSource.test.ts
@@ -7,7 +7,10 @@ import {
CUSTOM_LOCK_SCREEN_APP,
CUSTOM_LOCK_SCREEN_APP_METADATA,
} from "@api/device-action/__test-utils__/data";
-import { DEFAULT_MANAGER_API_BASE_URL } from "@internal/manager-api//model/Const";
+import {
+ DEFAULT_MANAGER_API_BASE_URL,
+ DEFAULT_MOCK_SERVER_BASE_URL,
+} from "@internal/manager-api//model/Const";
import { HttpFetchApiError } from "@internal/manager-api/model/Errors";
import { AxiosManagerApiDataSource } from "./AxiosManagerApiDataSource";
@@ -20,6 +23,7 @@ describe("AxiosManagerApiDataSource", () => {
it("with BTC app, should return the metadata", async () => {
const api = new AxiosManagerApiDataSource({
managerApiUrl: DEFAULT_MANAGER_API_BASE_URL,
+ mockUrl: DEFAULT_MOCK_SERVER_BASE_URL,
});
jest.spyOn(axios, "post").mockResolvedValue({
@@ -36,6 +40,7 @@ describe("AxiosManagerApiDataSource", () => {
it("with no apps, should return an empty list", async () => {
const api = new AxiosManagerApiDataSource({
managerApiUrl: DEFAULT_MANAGER_API_BASE_URL,
+ mockUrl: DEFAULT_MOCK_SERVER_BASE_URL,
});
jest.spyOn(axios, "post").mockResolvedValue({
@@ -52,6 +57,7 @@ describe("AxiosManagerApiDataSource", () => {
it("with BTC app and custom lock screen, should return the metadata", async () => {
const api = new AxiosManagerApiDataSource({
managerApiUrl: DEFAULT_MANAGER_API_BASE_URL,
+ mockUrl: DEFAULT_MOCK_SERVER_BASE_URL,
});
jest.spyOn(axios, "post").mockResolvedValue({
@@ -75,6 +81,7 @@ describe("AxiosManagerApiDataSource", () => {
it("should throw an error if the request fails", async () => {
const api = new AxiosManagerApiDataSource({
managerApiUrl: DEFAULT_MANAGER_API_BASE_URL,
+ mockUrl: DEFAULT_MOCK_SERVER_BASE_URL,
});
const err = new Error("fetch error");
jest.spyOn(axios, "post").mockRejectedValue(err);
diff --git a/packages/core/src/internal/manager-api/data/AxiosManagerApiDataSource.ts b/packages/device-management-kit/src/internal/manager-api/data/AxiosManagerApiDataSource.ts
similarity index 94%
rename from packages/core/src/internal/manager-api/data/AxiosManagerApiDataSource.ts
rename to packages/device-management-kit/src/internal/manager-api/data/AxiosManagerApiDataSource.ts
index c586991a3..8b380f4fd 100644
--- a/packages/core/src/internal/manager-api/data/AxiosManagerApiDataSource.ts
+++ b/packages/device-management-kit/src/internal/manager-api/data/AxiosManagerApiDataSource.ts
@@ -2,7 +2,7 @@ import axios from "axios";
import { inject, injectable } from "inversify";
import { EitherAsync } from "purify-ts";
-import { type SdkConfig } from "@api/SdkConfig";
+import { type DmkConfig } from "@api/DmkConfig";
import { managerApiTypes } from "@internal/manager-api/di/managerApiTypes";
import { HttpFetchApiError } from "@internal/manager-api/model/Errors";
import {
@@ -16,7 +16,7 @@ import { ApplicationDto, AppTypeDto } from "./ManagerApiDto";
@injectable()
export class AxiosManagerApiDataSource implements ManagerApiDataSource {
private readonly baseUrl: string;
- constructor(@inject(managerApiTypes.SdkConfig) config: SdkConfig) {
+ constructor(@inject(managerApiTypes.DmkConfig) config: DmkConfig) {
this.baseUrl = config.managerApiUrl;
}
diff --git a/packages/device-management-kit/src/internal/manager-api/data/ManagerApiDataSource.ts b/packages/device-management-kit/src/internal/manager-api/data/ManagerApiDataSource.ts
new file mode 100644
index 000000000..f7c89c6b7
--- /dev/null
+++ b/packages/device-management-kit/src/internal/manager-api/data/ManagerApiDataSource.ts
@@ -0,0 +1,10 @@
+import { type EitherAsync } from "purify-ts";
+
+import { type HttpFetchApiError } from "@internal/manager-api/model/Errors";
+import { type Application } from "@internal/manager-api/model/ManagerApiType";
+
+export interface ManagerApiDataSource {
+ getAppsByHash(
+ hashes: string[],
+ ): EitherAsync>;
+}
diff --git a/packages/core/src/internal/manager-api/data/ManagerApiDto.ts b/packages/device-management-kit/src/internal/manager-api/data/ManagerApiDto.ts
similarity index 100%
rename from packages/core/src/internal/manager-api/data/ManagerApiDto.ts
rename to packages/device-management-kit/src/internal/manager-api/data/ManagerApiDto.ts
diff --git a/packages/core/src/internal/manager-api/data/__mocks__/AxiosManagerApiDataSource.ts b/packages/device-management-kit/src/internal/manager-api/data/__mocks__/AxiosManagerApiDataSource.ts
similarity index 53%
rename from packages/core/src/internal/manager-api/data/__mocks__/AxiosManagerApiDataSource.ts
rename to packages/device-management-kit/src/internal/manager-api/data/__mocks__/AxiosManagerApiDataSource.ts
index 1f3c0076a..850337d47 100644
--- a/packages/core/src/internal/manager-api/data/__mocks__/AxiosManagerApiDataSource.ts
+++ b/packages/device-management-kit/src/internal/manager-api/data/__mocks__/AxiosManagerApiDataSource.ts
@@ -1,4 +1,4 @@
-import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
+import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
export class AxiosManagerApiDataSource implements ManagerApiDataSource {
getAppsByHash = jest.fn();
diff --git a/packages/core/src/internal/manager-api/di/managerApiModule.test.ts b/packages/device-management-kit/src/internal/manager-api/di/managerApiModule.test.ts
similarity index 77%
rename from packages/core/src/internal/manager-api/di/managerApiModule.test.ts
rename to packages/device-management-kit/src/internal/manager-api/di/managerApiModule.test.ts
index db5e73882..f0425dea6 100644
--- a/packages/core/src/internal/manager-api/di/managerApiModule.test.ts
+++ b/packages/device-management-kit/src/internal/manager-api/di/managerApiModule.test.ts
@@ -15,7 +15,10 @@ describe("managerApiModuleFactory", () => {
beforeEach(() => {
mod = managerApiModuleFactory({
stub: false,
- config: { managerApiUrl: "http://fake.url" },
+ config: {
+ managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
+ },
});
container = new Container();
container.load(mod);
@@ -36,8 +39,11 @@ describe("managerApiModuleFactory", () => {
);
expect(managerApiService).toBeInstanceOf(DefaultManagerApiService);
- const config = container.get(managerApiTypes.SdkConfig);
- expect(config).toEqual({ managerApiUrl: "http://fake.url" });
+ const config = container.get(managerApiTypes.DmkConfig);
+ expect(config).toEqual({
+ managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
+ });
});
});
@@ -47,7 +53,10 @@ describe("managerApiModuleFactory", () => {
beforeEach(() => {
mod = managerApiModuleFactory({
stub: true,
- config: { managerApiUrl: "http://fake.url" },
+ config: {
+ managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
+ },
});
container = new Container();
container.load(mod);
@@ -68,8 +77,11 @@ describe("managerApiModuleFactory", () => {
);
expect(managerApiService).toBeInstanceOf(StubUseCase);
- const config = container.get(managerApiTypes.SdkConfig);
- expect(config).toEqual({ managerApiUrl: "http://fake.url" });
+ const config = container.get(managerApiTypes.DmkConfig);
+ expect(config).toEqual({
+ managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
+ });
});
});
});
diff --git a/packages/core/src/internal/manager-api/di/managerApiModule.ts b/packages/device-management-kit/src/internal/manager-api/di/managerApiModule.ts
similarity index 86%
rename from packages/core/src/internal/manager-api/di/managerApiModule.ts
rename to packages/device-management-kit/src/internal/manager-api/di/managerApiModule.ts
index f0fed0209..0e4ae5512 100644
--- a/packages/core/src/internal/manager-api/di/managerApiModule.ts
+++ b/packages/device-management-kit/src/internal/manager-api/di/managerApiModule.ts
@@ -1,6 +1,6 @@
import { ContainerModule } from "inversify";
-import { SdkConfig } from "@api/SdkConfig";
+import { type DmkConfig } from "@api/DmkConfig";
import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource";
import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService";
import { StubUseCase } from "@root/src/di.stub";
@@ -9,12 +9,12 @@ import { managerApiTypes } from "./managerApiTypes";
type FactoryProps = {
stub?: boolean;
- config: SdkConfig;
+ config: DmkConfig;
};
export const managerApiModuleFactory = ({ stub, config }: FactoryProps) =>
new ContainerModule((bind, _unbind, _isBound, rebind) => {
- bind(managerApiTypes.SdkConfig).toConstantValue(config);
+ bind(managerApiTypes.DmkConfig).toConstantValue(config);
bind(managerApiTypes.ManagerApiDataSource).to(AxiosManagerApiDataSource);
bind(managerApiTypes.ManagerApiService).to(DefaultManagerApiService);
diff --git a/packages/core/src/internal/manager-api/di/managerApiTypes.ts b/packages/device-management-kit/src/internal/manager-api/di/managerApiTypes.ts
similarity index 75%
rename from packages/core/src/internal/manager-api/di/managerApiTypes.ts
rename to packages/device-management-kit/src/internal/manager-api/di/managerApiTypes.ts
index c2a68cc2c..09fbc59df 100644
--- a/packages/core/src/internal/manager-api/di/managerApiTypes.ts
+++ b/packages/device-management-kit/src/internal/manager-api/di/managerApiTypes.ts
@@ -1,5 +1,5 @@
export const managerApiTypes = {
ManagerApiService: Symbol.for("ManagerApiService"),
ManagerApiDataSource: Symbol.for("ManagerApiDataSource"),
- SdkConfig: Symbol.for("SdkConfig"),
+ DmkConfig: Symbol.for("ManagerApiDmkConfig"),
};
diff --git a/packages/core/src/internal/manager-api/model/Const.ts b/packages/device-management-kit/src/internal/manager-api/model/Const.ts
similarity index 56%
rename from packages/core/src/internal/manager-api/model/Const.ts
rename to packages/device-management-kit/src/internal/manager-api/model/Const.ts
index 6097ce49a..72d42d269 100644
--- a/packages/core/src/internal/manager-api/model/Const.ts
+++ b/packages/device-management-kit/src/internal/manager-api/model/Const.ts
@@ -1,2 +1,3 @@
export const DEFAULT_MANAGER_API_BASE_URL =
"https://manager.api.live.ledger.com/api";
+export const DEFAULT_MOCK_SERVER_BASE_URL = "http://localhost:8080";
diff --git a/packages/core/src/internal/manager-api/model/Errors.ts b/packages/device-management-kit/src/internal/manager-api/model/Errors.ts
similarity index 58%
rename from packages/core/src/internal/manager-api/model/Errors.ts
rename to packages/device-management-kit/src/internal/manager-api/model/Errors.ts
index 7d8f117be..915f76559 100644
--- a/packages/core/src/internal/manager-api/model/Errors.ts
+++ b/packages/device-management-kit/src/internal/manager-api/model/Errors.ts
@@ -1,6 +1,6 @@
-import { SdkError } from "@api/Error";
+import { type DmkError } from "@api/Error";
-export class HttpFetchApiError implements SdkError {
+export class HttpFetchApiError implements DmkError {
_tag = "FetchError";
originalError?: unknown;
diff --git a/packages/core/src/internal/manager-api/model/ManagerApiType.ts b/packages/device-management-kit/src/internal/manager-api/model/ManagerApiType.ts
similarity index 100%
rename from packages/core/src/internal/manager-api/model/ManagerApiType.ts
rename to packages/device-management-kit/src/internal/manager-api/model/ManagerApiType.ts
diff --git a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts b/packages/device-management-kit/src/internal/manager-api/service/DefaultManagerApiService.test.ts
similarity index 92%
rename from packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts
rename to packages/device-management-kit/src/internal/manager-api/service/DefaultManagerApiService.test.ts
index d2c2f45c5..997c38f01 100644
--- a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.test.ts
+++ b/packages/device-management-kit/src/internal/manager-api/service/DefaultManagerApiService.test.ts
@@ -6,12 +6,15 @@ import {
ETH_APP,
ETH_APP_METADATA,
} from "@api/device-action/__test-utils__/data";
-import { DEFAULT_MANAGER_API_BASE_URL } from "@internal/manager-api//model/Const";
+import {
+ DEFAULT_MANAGER_API_BASE_URL,
+ DEFAULT_MOCK_SERVER_BASE_URL,
+} from "@internal/manager-api//model/Const";
import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource";
import { HttpFetchApiError } from "@internal/manager-api/model/Errors";
import { DefaultManagerApiService } from "./DefaultManagerApiService";
-import { ManagerApiService } from "./ManagerApiService";
+import { type ManagerApiService } from "./ManagerApiService";
jest.mock("@internal/manager-api/data/AxiosManagerApiDataSource");
let dataSource: jest.Mocked;
@@ -20,6 +23,7 @@ describe("ManagerApiService", () => {
beforeEach(() => {
dataSource = new AxiosManagerApiDataSource({
managerApiUrl: DEFAULT_MANAGER_API_BASE_URL,
+ mockUrl: DEFAULT_MOCK_SERVER_BASE_URL,
}) as jest.Mocked;
service = new DefaultManagerApiService(dataSource);
});
diff --git a/packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts b/packages/device-management-kit/src/internal/manager-api/service/DefaultManagerApiService.ts
similarity index 100%
rename from packages/core/src/internal/manager-api/service/DefaultManagerApiService.ts
rename to packages/device-management-kit/src/internal/manager-api/service/DefaultManagerApiService.ts
diff --git a/packages/device-management-kit/src/internal/manager-api/service/ManagerApiService.ts b/packages/device-management-kit/src/internal/manager-api/service/ManagerApiService.ts
new file mode 100644
index 000000000..966ac55c4
--- /dev/null
+++ b/packages/device-management-kit/src/internal/manager-api/service/ManagerApiService.ts
@@ -0,0 +1,11 @@
+import { type EitherAsync } from "purify-ts";
+
+import { type ListAppsResponse } from "@api/command/os/ListAppsCommand";
+import { type HttpFetchApiError } from "@internal/manager-api/model/Errors";
+import { type Application } from "@internal/manager-api/model/ManagerApiType";
+
+export interface ManagerApiService {
+ getAppsByHash(
+ apps: ListAppsResponse,
+ ): EitherAsync>;
+}
diff --git a/packages/core/src/internal/send/di/sendModule.test.ts b/packages/device-management-kit/src/internal/send/di/sendModule.test.ts
similarity index 100%
rename from packages/core/src/internal/send/di/sendModule.test.ts
rename to packages/device-management-kit/src/internal/send/di/sendModule.test.ts
diff --git a/packages/core/src/internal/send/di/sendModule.ts b/packages/device-management-kit/src/internal/send/di/sendModule.ts
similarity index 100%
rename from packages/core/src/internal/send/di/sendModule.ts
rename to packages/device-management-kit/src/internal/send/di/sendModule.ts
diff --git a/packages/core/src/internal/send/di/sendTypes.ts b/packages/device-management-kit/src/internal/send/di/sendTypes.ts
similarity index 100%
rename from packages/core/src/internal/send/di/sendTypes.ts
rename to packages/device-management-kit/src/internal/send/di/sendTypes.ts
diff --git a/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts b/packages/device-management-kit/src/internal/send/use-case/SendApduUseCase.test.ts
similarity index 84%
rename from packages/core/src/internal/send/use-case/SendApduUseCase.test.ts
rename to packages/device-management-kit/src/internal/send/use-case/SendApduUseCase.test.ts
index 2c8284ed7..d1f8e4f31 100644
--- a/packages/core/src/internal/send/use-case/SendApduUseCase.test.ts
+++ b/packages/device-management-kit/src/internal/send/use-case/SendApduUseCase.test.ts
@@ -6,15 +6,15 @@ import {
ReceiverApduError,
} from "@internal/device-session/model/Errors";
import { DefaultDeviceSessionService } from "@internal/device-session/service/DefaultDeviceSessionService";
-import { DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
+import { type DeviceSessionService } from "@internal/device-session/service/DeviceSessionService";
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
-import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
+import { type LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import { AxiosManagerApiDataSource } from "@internal/manager-api/data/AxiosManagerApiDataSource";
-import { ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
+import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";
import { DefaultManagerApiService } from "@internal/manager-api/service/DefaultManagerApiService";
-import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
+import { type ManagerApiService } from "@internal/manager-api/service/ManagerApiService";
import { SendApduUseCase } from "@internal/send/use-case/SendApduUseCase";
-import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnectedDevice.stub";
+import { connectedDeviceStubBuilder } from "@internal/transport/model/InternalConnectedDevice.stub";
jest.mock("@internal/manager-api/data/AxiosManagerApiDataSource");
@@ -30,6 +30,7 @@ describe("SendApduUseCase", () => {
sessionService = new DefaultDeviceSessionService(() => logger);
managerApiDataSource = new AxiosManagerApiDataSource({
managerApiUrl: "http://fake.url",
+ mockUrl: "http://fake-mock.url",
});
managerApi = new DefaultManagerApiService(managerApiDataSource);
});
@@ -50,6 +51,7 @@ describe("SendApduUseCase", () => {
apdu: new Uint8Array([0x00, 0x01, 0x02, 0x03]),
});
+ deviceSession.close();
// then
expect(deviceSession.connectedDevice.sendApdu).toHaveBeenCalledTimes(1);
expect(response).toBeDefined();
@@ -90,6 +92,8 @@ describe("SendApduUseCase", () => {
apdu: new Uint8Array([0x00, 0x01, 0x02, 0x03]),
});
+ deviceSession.close();
+
// then
await expect(response).rejects.toBeInstanceOf(ReceiverApduError);
});
diff --git a/packages/core/src/internal/send/use-case/SendApduUseCase.ts b/packages/device-management-kit/src/internal/send/use-case/SendApduUseCase.ts
similarity index 97%
rename from packages/core/src/internal/send/use-case/SendApduUseCase.ts
rename to packages/device-management-kit/src/internal/send/use-case/SendApduUseCase.ts
index 84ff963d0..d730d2001 100644
--- a/packages/core/src/internal/send/use-case/SendApduUseCase.ts
+++ b/packages/device-management-kit/src/internal/send/use-case/SendApduUseCase.ts
@@ -12,7 +12,7 @@ import { LoggerPublisherService } from "@internal/logger-publisher/service/Logge
*/
export type SendApduUseCaseArgs = {
/**
- * Device session identifier from `DeviceSdk.connect`.
+ * Device session identifier from `DeviceManagementKit.connect`.
*/
sessionId: DeviceSessionId;
/**
diff --git a/packages/device-management-kit/src/internal/transport/ble/di/bleDiTypes.ts b/packages/device-management-kit/src/internal/transport/ble/di/bleDiTypes.ts
new file mode 100644
index 000000000..1b25bc98e
--- /dev/null
+++ b/packages/device-management-kit/src/internal/transport/ble/di/bleDiTypes.ts
@@ -0,0 +1,3 @@
+export const bleDiTypes = {
+ BleDeviceConnectionFactory: Symbol.for("BleDeviceConnectionFactory"),
+};
diff --git a/packages/device-management-kit/src/internal/transport/ble/di/bleModule.test.ts b/packages/device-management-kit/src/internal/transport/ble/di/bleModule.test.ts
new file mode 100644
index 000000000..f55f1f483
--- /dev/null
+++ b/packages/device-management-kit/src/internal/transport/ble/di/bleModule.test.ts
@@ -0,0 +1,26 @@
+import { Container } from "inversify";
+
+import { deviceModelModuleFactory } from "@internal/device-model/di/deviceModelModule";
+import { deviceSessionModuleFactory } from "@internal/device-session/di/deviceSessionModule";
+import { loggerModuleFactory } from "@internal/logger-publisher/di/loggerModule";
+
+import { bleModuleFactory } from "./bleModule";
+
+describe("bleModuleFactory", () => {
+ let container: Container;
+ let mod: ReturnType;
+ beforeEach(() => {
+ mod = bleModuleFactory();
+ container = new Container();
+ container.load(loggerModuleFactory());
+ container.load(
+ mod,
+ deviceModelModuleFactory({ stub: false }),
+ deviceSessionModuleFactory(),
+ );
+ });
+
+ it("should return the usb module", () => {
+ expect(mod).toBeDefined();
+ });
+});
diff --git a/packages/device-management-kit/src/internal/transport/ble/di/bleModule.ts b/packages/device-management-kit/src/internal/transport/ble/di/bleModule.ts
new file mode 100644
index 000000000..ae18d5100
--- /dev/null
+++ b/packages/device-management-kit/src/internal/transport/ble/di/bleModule.ts
@@ -0,0 +1,10 @@
+import { ContainerModule } from "inversify";
+
+import { BleDeviceConnectionFactory } from "@internal/transport/ble/service/BleDeviceConnectionFactory";
+
+import { bleDiTypes } from "./bleDiTypes";
+
+export const bleModuleFactory = () =>
+ new ContainerModule((bind, _unbind, _isBound, _rebind) => {
+ bind(bleDiTypes.BleDeviceConnectionFactory).to(BleDeviceConnectionFactory);
+ });
diff --git a/packages/device-management-kit/src/internal/transport/ble/model/BleDevice.stub.ts b/packages/device-management-kit/src/internal/transport/ble/model/BleDevice.stub.ts
new file mode 100644
index 000000000..ad58e5b41
--- /dev/null
+++ b/packages/device-management-kit/src/internal/transport/ble/model/BleDevice.stub.ts
@@ -0,0 +1,64 @@
+const bleDeviceWithoutGatt: BluetoothDevice = {
+ name: "Ledger Nano X",
+ id: "42",
+ forget: jest.fn(),
+ watchAdvertisements: jest.fn(),
+ dispatchEvent: jest.fn(),
+ watchingAdvertisements: false,
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ onadvertisementreceived: jest.fn(),
+ ongattserverdisconnected: jest.fn(),
+ oncharacteristicvaluechanged: jest.fn(),
+ onserviceadded: jest.fn(),
+ onservicechanged: jest.fn(),
+ onserviceremoved: jest.fn(),
+};
+
+const bluetoothGattPrimaryService: BluetoothRemoteGATTService = {
+ device: bleDeviceWithoutGatt,
+ uuid: "13d63400-2c97-0004-0000-4c6564676572",
+ isPrimary: true,
+ getCharacteristic: jest.fn(() =>
+ Promise.resolve(bleCharacteristicStubBuilder()),
+ ),
+ getCharacteristics: jest.fn(),
+ getIncludedService: jest.fn(),
+ getIncludedServices: jest.fn(),
+ addEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ removeEventListener: jest.fn(),
+ oncharacteristicvaluechanged: jest.fn(),
+ onserviceadded: jest.fn(),
+ onservicechanged: jest.fn(),
+ onserviceremoved: jest.fn(),
+};
+
+export const bleCharacteristicStubBuilder = (
+ props: Partial