From 442991a1d7b3e3a271a108fbb38c1c9b655bb5f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Ga=C3=A1l?= Date: Tue, 19 May 2026 15:35:01 +0200 Subject: [PATCH 1/2] chore: pnpm upgraded to v11 --- .npmrc | 4 ---- README.md | 2 +- apps/svelte/.npmrc | 1 - package.json | 24 ------------------------ pnpm-lock.yaml | 18 +++++++++++++++++- pnpm-workspace.yaml | 21 +++++++++++++++++++++ 6 files changed, 39 insertions(+), 31 deletions(-) delete mode 100644 .npmrc delete mode 100644 apps/svelte/.npmrc diff --git a/.npmrc b/.npmrc deleted file mode 100644 index ba4df202..00000000 --- a/.npmrc +++ /dev/null @@ -1,4 +0,0 @@ -always-auth=true -shell-emulator=true -auto-install-peers=false -shamefully-hoist=true diff --git a/README.md b/README.md index 5d2908c5..d90987e0 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ This is a pnpm monorepo. It contains: ## Prerequisites - [Node.js](https://nodejs.org/) LTS -- [pnpm](https://pnpm.io/) 10+ +- [pnpm](https://pnpm.io/) 11+ --- diff --git a/apps/svelte/.npmrc b/apps/svelte/.npmrc deleted file mode 100644 index b6f27f13..00000000 --- a/apps/svelte/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/package.json b/package.json index 640f5c55..5c4dc6af 100644 --- a/package.json +++ b/package.json @@ -192,29 +192,5 @@ "vue-tsc": "^3.1.7", "webpack": "~5.99.9", "zone.js": "~0.16.0" - }, - "pnpm": { - "overrides": { - "@strivacity/sdk-angular": "workspace:*", - "@strivacity/sdk-core": "workspace:*", - "@strivacity/sdk-next": "workspace:*", - "@strivacity/sdk-nuxt": "workspace:*", - "@strivacity/sdk-svelte": "workspace:*", - "@strivacity/sdk-react": "workspace:*", - "@strivacity/sdk-remix": "workspace:*", - "@strivacity/sdk-vue": "workspace:*", - "@strivacity/testing": "workspace:*" - }, - "ignoredBuiltDependencies": [ - "lmdb", - "msgpackr-extract", - "sharp" - ], - "onlyBuiltDependencies": [ - "esbuild", - "msw", - "nx", - "simple-git-hooks" - ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1485ea7..fdc0e726 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -392,6 +392,12 @@ importers: specifier: workspace:* version: link:../../packages/sdk-angular + apps/angular-standalone: + dependencies: + '@strivacity/sdk-angular': + specifier: workspace:* + version: link:../../packages/sdk-angular + apps/backend: dependencies: '@strivacity/sdk-vue': @@ -5365,6 +5371,7 @@ packages: '@xmldom/xmldom@0.8.10': resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -7384,6 +7391,7 @@ packages: git-raw-commits@4.0.0: resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} engines: {node: '>=16'} + deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead. hasBin: true git-up@8.0.0: @@ -7411,11 +7419,13 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.0.3: resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@13.0.0: @@ -7425,7 +7435,7 @@ packages: glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} @@ -9022,6 +9032,7 @@ packages: next@16.0.7: resolution: {integrity: sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==} engines: {node: '>=20.9.0'} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -11541,10 +11552,12 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me tar@7.5.2: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me terser-webpack-plugin@5.3.14: resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} @@ -11941,6 +11954,7 @@ packages: unplugin-vue-router@0.16.1: resolution: {integrity: sha512-7A7gUVzLIYMBrBPKk8l4lZoZXDOrO8+etw6/RTrqG3OzpLUUZEXJFUW7+OyMIpQK93sEbdkR2z9ZNNl/r32FMw==} + deprecated: 'Merged into vuejs/router. Migrate: https://router.vuejs.org/guide/migration/v4-to-v5.html' peerDependencies: '@vue/compiler-sfc': ^3.5.17 vue-router: ^4.6.0 @@ -12066,6 +12080,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uvu@0.5.6: @@ -12570,6 +12585,7 @@ packages: whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f3768ac7..2d10030c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,24 @@ packages: - 'apps/*' - 'packages/*' - 'tools/*' +overrides: + '@strivacity/sdk-angular': workspace:* + '@strivacity/sdk-core': workspace:* + '@strivacity/sdk-next': workspace:* + '@strivacity/sdk-nuxt': workspace:* + '@strivacity/sdk-svelte': workspace:* + '@strivacity/sdk-react': workspace:* + '@strivacity/sdk-remix': workspace:* + '@strivacity/sdk-vue': workspace:* + '@strivacity/testing': workspace:* +shellEmulator: true +autoInstallPeers: false +shamefullyHoist: true +allowBuilds: + esbuild: true + msw: true + nx: true + simple-git-hooks: true + lmdb: false + msgpackr-extract: false + sharp: false From f24675cb9dde4d17673adb5ec154393ed1764674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Ga=C3=A1l?= Date: Tue, 19 May 2026 15:34:42 +0200 Subject: [PATCH 2/2] chore: module based angular example app added --- apps/angular-standalone/.stylelintrc.js | 4 + apps/angular-standalone/README.md | 316 +++++++ apps/angular-standalone/eslint.config.js | 36 + apps/angular-standalone/package.json | 11 + apps/angular-standalone/project.json | 55 ++ .../src/app/app.component.html | 21 + .../src/app/app.component.scss | 59 ++ .../src/app/app.component.ts | 28 + apps/angular-standalone/src/app/app.config.ts | 34 + apps/angular-standalone/src/app/app.routes.ts | 20 + .../src/app/components/widgets/README.md | 870 ++++++++++++++++++ .../widgets/checkbox/checkbox.widget.html | 18 + .../widgets/checkbox/checkbox.widget.scss | 27 + .../widgets/checkbox/checkbox.widget.ts | 48 + .../widgets/close/close.widget.html | 13 + .../widgets/close/close.widget.scss | 54 ++ .../components/widgets/close/close.widget.ts | 43 + .../components/widgets/date/date.widget.html | 33 + .../components/widgets/date/date.widget.scss | 49 + .../components/widgets/date/date.widget.ts | 142 +++ .../src/app/components/widgets/index.ts | 41 + .../widgets/input/input.widget.html | 22 + .../widgets/input/input.widget.scss | 44 + .../components/widgets/input/input.widget.ts | 79 ++ .../widgets/layout/layout.widget.html | 13 + .../widgets/layout/layout.widget.scss | 30 + .../widgets/layout/layout.widget.ts | 39 + .../widgets/loading/loading.widget.html | 3 + .../widgets/loading/loading.widget.scss | 45 + .../widgets/loading/loading.widget.ts | 19 + .../multiselect/multiselect.widget.html | 24 + .../multiselect/multiselect.widget.scss | 31 + .../widgets/multiselect/multiselect.widget.ts | 51 + .../widgets/passcode/passcode.widget.html | 20 + .../widgets/passcode/passcode.widget.scss | 44 + .../widgets/passcode/passcode.widget.ts | 62 ++ .../passkey-enroll/passkey-enroll.widget.html | 5 + .../passkey-enroll/passkey-enroll.widget.scss | 47 + .../passkey-enroll/passkey-enroll.widget.ts | 51 + .../passkey-login/passkey-login.widget.html | 5 + .../passkey-login/passkey-login.widget.scss | 47 + .../passkey-login/passkey-login.widget.ts | 51 + .../widgets/password/password.widget.html | 7 + .../widgets/password/password.widget.scss | 44 + .../widgets/password/password.widget.ts | 53 ++ .../widgets/phone/phone.widget.html | 19 + .../widgets/phone/phone.widget.scss | 44 + .../components/widgets/phone/phone.widget.ts | 69 ++ .../widgets/select/select.widget.html | 43 + .../widgets/select/select.widget.scss | 30 + .../widgets/select/select.widget.ts | 52 ++ .../widgets/static/static.widget.html | 5 + .../widgets/static/static.widget.scss | 18 + .../widgets/static/static.widget.ts | 21 + .../widgets/submit/submit.widget.html | 14 + .../widgets/submit/submit.widget.scss | 54 ++ .../widgets/submit/submit.widget.ts | 49 + .../webauthn-enroll.widget.html | 5 + .../webauthn-enroll.widget.scss | 47 + .../webauthn-enroll/webauthn-enroll.widget.ts | 51 + .../webauthn-login/webauthn-login.widget.html | 5 + .../webauthn-login/webauthn-login.widget.scss | 47 + .../webauthn-login/webauthn-login.widget.ts | 51 + .../src/app/pages/callback/callback.page.html | 11 + .../src/app/pages/callback/callback.page.ts | 48 + .../src/app/pages/callback/index.ts | 1 + .../src/app/pages/entry/entry.page.html | 3 + .../src/app/pages/entry/entry.page.ts | 31 + .../src/app/pages/entry/index.ts | 1 + .../src/app/pages/home/home.page.html | 3 + .../src/app/pages/home/home.page.ts | 8 + .../src/app/pages/home/index.ts | 1 + .../src/app/pages/login/index.ts | 1 + .../src/app/pages/login/login.page.html | 33 + .../src/app/pages/login/login.page.ts | 105 +++ .../src/app/pages/logout/index.ts | 1 + .../src/app/pages/logout/logout.page.html | 3 + .../src/app/pages/logout/logout.page.ts | 30 + .../src/app/pages/profile/index.ts | 1 + .../src/app/pages/profile/profile.page.html | 34 + .../src/app/pages/profile/profile.page.ts | 33 + .../src/app/pages/register/index.ts | 1 + .../src/app/pages/register/register.page.html | 33 + .../src/app/pages/register/register.page.ts | 106 +++ .../src/app/pages/revoke/index.ts | 1 + .../src/app/pages/revoke/revoke.page.html | 3 + .../src/app/pages/revoke/revoke.page.ts | 30 + apps/angular-standalone/src/index.html | 13 + apps/angular-standalone/src/main.ts | 7 + apps/angular-standalone/tsconfig.app.json | 8 + apps/angular-standalone/tsconfig.json | 27 + apps/angular-standalone/webpack.config.cjs | 21 + apps/angular/src/app/app.component.ts | 4 +- apps/angular/src/app/app.config.ts | 21 - apps/angular/src/app/app.module.ts | 83 ++ .../widgets/checkbox/checkbox.widget.ts | 4 +- .../components/widgets/close/close.widget.ts | 4 +- .../components/widgets/date/date.widget.ts | 4 +- .../components/widgets/input/input.widget.ts | 4 +- .../widgets/layout/layout.widget.ts | 4 +- .../widgets/loading/loading.widget.ts | 4 +- .../widgets/multiselect/multiselect.widget.ts | 4 +- .../widgets/passcode/passcode.widget.ts | 4 +- .../passkey-enroll/passkey-enroll.widget.ts | 4 +- .../passkey-login/passkey-login.widget.ts | 4 +- .../widgets/password/password.widget.ts | 4 +- .../components/widgets/phone/phone.widget.ts | 4 +- .../widgets/select/select.widget.ts | 4 +- .../widgets/static/static.widget.ts | 4 +- .../widgets/submit/submit.widget.ts | 4 +- .../webauthn-enroll/webauthn-enroll.widget.ts | 4 +- .../webauthn-login/webauthn-login.widget.ts | 4 +- .../src/app/pages/callback/callback.page.ts | 2 +- .../angular/src/app/pages/entry/entry.page.ts | 2 +- apps/angular/src/app/pages/home/home.page.ts | 2 +- .../angular/src/app/pages/login/login.page.ts | 5 +- .../src/app/pages/logout/logout.page.ts | 2 +- .../src/app/pages/profile/profile.page.ts | 4 +- .../src/app/pages/register/register.page.ts | 5 +- .../src/app/pages/revoke/revoke.page.ts | 2 +- apps/angular/src/main.ts | 8 +- package.json | 2 + 122 files changed, 4097 insertions(+), 93 deletions(-) create mode 100644 apps/angular-standalone/.stylelintrc.js create mode 100644 apps/angular-standalone/README.md create mode 100644 apps/angular-standalone/eslint.config.js create mode 100644 apps/angular-standalone/package.json create mode 100644 apps/angular-standalone/project.json create mode 100644 apps/angular-standalone/src/app/app.component.html create mode 100644 apps/angular-standalone/src/app/app.component.scss create mode 100644 apps/angular-standalone/src/app/app.component.ts create mode 100644 apps/angular-standalone/src/app/app.config.ts create mode 100644 apps/angular-standalone/src/app/app.routes.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/README.md create mode 100644 apps/angular-standalone/src/app/components/widgets/checkbox/checkbox.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/checkbox/checkbox.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/checkbox/checkbox.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/close/close.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/close/close.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/close/close.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/date/date.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/date/date.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/date/date.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/index.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/input/input.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/input/input.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/input/input.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/layout/layout.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/layout/layout.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/layout/layout.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/loading/loading.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/loading/loading.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/loading/loading.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/multiselect/multiselect.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/multiselect/multiselect.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/multiselect/multiselect.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/passcode/passcode.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/passcode/passcode.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/passcode/passcode.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/passkey-login/passkey-login.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/passkey-login/passkey-login.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/passkey-login/passkey-login.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/password/password.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/password/password.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/password/password.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/phone/phone.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/phone/phone.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/phone/phone.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/select/select.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/select/select.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/select/select.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/static/static.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/static/static.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/static/static.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/submit/submit.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/submit/submit.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/submit/submit.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.ts create mode 100644 apps/angular-standalone/src/app/components/widgets/webauthn-login/webauthn-login.widget.html create mode 100644 apps/angular-standalone/src/app/components/widgets/webauthn-login/webauthn-login.widget.scss create mode 100644 apps/angular-standalone/src/app/components/widgets/webauthn-login/webauthn-login.widget.ts create mode 100644 apps/angular-standalone/src/app/pages/callback/callback.page.html create mode 100644 apps/angular-standalone/src/app/pages/callback/callback.page.ts create mode 100644 apps/angular-standalone/src/app/pages/callback/index.ts create mode 100644 apps/angular-standalone/src/app/pages/entry/entry.page.html create mode 100644 apps/angular-standalone/src/app/pages/entry/entry.page.ts create mode 100644 apps/angular-standalone/src/app/pages/entry/index.ts create mode 100644 apps/angular-standalone/src/app/pages/home/home.page.html create mode 100644 apps/angular-standalone/src/app/pages/home/home.page.ts create mode 100644 apps/angular-standalone/src/app/pages/home/index.ts create mode 100644 apps/angular-standalone/src/app/pages/login/index.ts create mode 100644 apps/angular-standalone/src/app/pages/login/login.page.html create mode 100644 apps/angular-standalone/src/app/pages/login/login.page.ts create mode 100644 apps/angular-standalone/src/app/pages/logout/index.ts create mode 100644 apps/angular-standalone/src/app/pages/logout/logout.page.html create mode 100644 apps/angular-standalone/src/app/pages/logout/logout.page.ts create mode 100644 apps/angular-standalone/src/app/pages/profile/index.ts create mode 100644 apps/angular-standalone/src/app/pages/profile/profile.page.html create mode 100644 apps/angular-standalone/src/app/pages/profile/profile.page.ts create mode 100644 apps/angular-standalone/src/app/pages/register/index.ts create mode 100644 apps/angular-standalone/src/app/pages/register/register.page.html create mode 100644 apps/angular-standalone/src/app/pages/register/register.page.ts create mode 100644 apps/angular-standalone/src/app/pages/revoke/index.ts create mode 100644 apps/angular-standalone/src/app/pages/revoke/revoke.page.html create mode 100644 apps/angular-standalone/src/app/pages/revoke/revoke.page.ts create mode 100644 apps/angular-standalone/src/index.html create mode 100644 apps/angular-standalone/src/main.ts create mode 100644 apps/angular-standalone/tsconfig.app.json create mode 100644 apps/angular-standalone/tsconfig.json create mode 100644 apps/angular-standalone/webpack.config.cjs create mode 100644 apps/angular/src/app/app.module.ts diff --git a/apps/angular-standalone/.stylelintrc.js b/apps/angular-standalone/.stylelintrc.js new file mode 100644 index 00000000..f0dcca8b --- /dev/null +++ b/apps/angular-standalone/.stylelintrc.js @@ -0,0 +1,4 @@ +export default { + extends: ['../../.stylelintrc.js'], + ignoreFiles: ['!**/*', './dist/**'], +}; diff --git a/apps/angular-standalone/README.md b/apps/angular-standalone/README.md new file mode 100644 index 00000000..e0a17c8e --- /dev/null +++ b/apps/angular-standalone/README.md @@ -0,0 +1,316 @@ +# Strivacity SDK - Angular Example App + +This example application demonstrates how to integrate the [@strivacity/sdk-angular](https://github.com/Strivacity/sdk-js/tree/main/packages/sdk-angular) SDK into an Angular 19 application using standalone components and Angular Router. It covers all supported authentication modes (`redirect`, `popup`, `native`, `embedded`) and shows how to structure route-based authentication flows. + +See our [Developer Portal](https://www.strivacity.com/learn-support/developer-hub) to get started with developing with the Strivacity product. + +## Overview + +The SDK is registered via `provideStrivacity()` in `app.config.ts` and exposes the `StrivacityAuthService`, which is injected into standalone components using Angular's dependency injection. Angular Router is used for client-side navigation with route-based authentication guards. + +## Requirements + +- Angular: 19+ +- Node.js: 20 LTS+ + +## Install + +```bash +pnpm install +``` + +Create a `.env.local` file in the repository root: + +```env +VITE_MODE=redirect +VITE_ISSUER=your-cluster-domain +VITE_CLIENT_ID=your-client-id +VITE_SCOPES=openid profile email +VITE_REDIRECT_URI=http://localhost:4200/callback +``` + +Then start the development server: + +```bash +pnpm app:angular:serve +``` + +## Usage + +### Initialization + +The SDK is registered as an Angular provider in `src/app/app.config.ts` using `provideStrivacity()`. All environment variables are read from the Vite environment and passed directly. The app also dynamically imports `bundle.js` from the configured issuer domain, which registers the Strivacity web components (``, ``, ``, etc.) used in `embedded` mode: + +```typescript +import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { provideStrivacity, DefaultLogging } from '@strivacity/sdk-angular'; +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideStrivacity({ + mode: import.meta.env.VITE_MODE, + issuer: import.meta.env.VITE_ISSUER, + scopes: import.meta.env.VITE_SCOPES.split(' '), + clientId: import.meta.env.VITE_CLIENT_ID, + redirectUri: import.meta.env.VITE_REDIRECT_URI, + storageTokenName: 'sty.session.angular', + logging: DefaultLogging, + }), + ], +}; +``` + +Components access authentication state by injecting `StrivacityAuthService`. The service exposes Angular Signals for `loading`, `isAuthenticated`, `idTokenClaims`, and other state properties: + +```typescript +import { Component, SkipSelf, computed } from '@angular/core'; +import { StrivacityAuthService } from '@strivacity/sdk-angular'; + +@Component({ standalone: true, selector: 'app-home', templateUrl: './home.page.html' }) +export class HomeComponent { + loading = this.authService.loading; + isAuthenticated = this.authService.isAuthenticated; + userName = computed(() => this.authService.idTokenClaims()?.given_name ?? ''); + + constructor(@SkipSelf() protected authService: StrivacityAuthService) {} +} +``` + +### Login / Register + +`src/app/pages/login/login.page.ts` and `src/app/pages/register/register.page.ts` initiate the authentication flow. The active mode determines how the UI is rendered: + +- **`redirect`** — `login()` is called on init; the user is taken to the identity provider in the same window. +- **`popup`** — `login()` is called on init; authentication happens in a popup window. +- **`native`** — The `StyLoginRenderer` component renders the login UI inline using your custom widget components. +- **`embedded`** — The `` web component (loaded via `bundle.js` from the cluster) takes over rendering. + +`src/app/pages/callback/callback.page.ts` handles the response from the identity provider. It calls `handleCallback()` and navigates to `/profile` on success: + +```typescript +import { Component, SkipSelf } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { StrivacityAuthService } from '@strivacity/sdk-angular'; + +@Component({ standalone: true, selector: 'app-callback-page', templateUrl: './callback.page.html' }) +export class CallbackPage { + readonly subscription = new Subscription(); + + constructor( + protected router: Router, + @SkipSelf() protected strivacityAuthService: StrivacityAuthService, + ) {} + + ngOnInit(): void { + const url = new URL(location.href); + + if (url.searchParams.has('session_id')) { + void this.router.navigate(['/login'], { queryParams: url.searchParams }); + return; + } + + this.subscription.add( + this.strivacityAuthService.handleCallback().subscribe({ + next: () => void this.router.navigateByUrl('/profile'), + error: (err) => console.error('Error during callback handling:', err), + }), + ); + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } +} +``` + +### Refresh token + +Token refresh runs automatically when the SDK detects an expired access token. The `refresh` method is also available on the service for manual invocation: + +```typescript +import { Component, SkipSelf } from '@angular/core'; +import { StrivacityAuthService } from '@strivacity/sdk-angular'; + +@Component({ standalone: true, selector: 'app-profile', templateUrl: './profile.page.html' }) +export class ProfilePage { + constructor(@SkipSelf() protected authService: StrivacityAuthService) {} + + refresh() { + this.authService.refresh().subscribe(); + } +} +``` + +### Revoke session / logout + +`src/app/pages/revoke/revoke.page.ts` revokes the current session tokens without a full logout. It calls `revoke()` and then navigates to the home page: + +```typescript +import { Component, SkipSelf } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { StrivacityAuthService } from '@strivacity/sdk-angular'; + +@Component({ standalone: true, selector: 'app-revoke-page', templateUrl: './revoke.page.html' }) +export class RevokePage { + readonly subscription = new Subscription(); + + constructor( + protected router: Router, + @SkipSelf() protected strivacityAuthService: StrivacityAuthService, + ) {} + + ngOnInit(): void { + this.strivacityAuthService.revoke().subscribe({ + next: () => void this.router.navigateByUrl('/'), + }); + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } +} +``` + +`src/app/pages/logout/logout.page.ts` performs a full logout. When the user is authenticated, `logout()` is called with `postLogoutRedirectUri` set to the application origin; otherwise the user is immediately redirected home: + +```typescript +import { Component, SkipSelf } from '@angular/core'; +import { Router } from '@angular/router'; +import { firstValueFrom } from 'rxjs'; +import { StrivacityAuthService } from '@strivacity/sdk-angular'; + +@Component({ standalone: true, selector: 'app-logout-page', templateUrl: './logout.page.html' }) +export class LogoutPage { + constructor( + protected router: Router, + @SkipSelf() protected strivacityAuthService: StrivacityAuthService, + ) {} + + async ngOnInit(): Promise { + if (this.strivacityAuthService.isAuthenticated()) { + await firstValueFrom(this.strivacityAuthService.logout({ postLogoutRedirectUri: window.location.origin })); + } else { + void this.router.navigateByUrl('/'); + } + } +} +``` + +### Resume an externally-initiated flow + +`src/app/pages/entry/entry.page.ts` handles flows started by an external process (e.g. password reset, magic link, invite). On init it calls `entry()`, which processes the incoming URL and returns a `session_id` (and optionally a `short_app_id`). These are forwarded as query parameters to `/callback` to resume the flow; if no data is returned the user is redirected to the home page: + +```typescript +import { Component, SkipSelf } from '@angular/core'; +import { Router } from '@angular/router'; +import { StrivacityAuthService } from '@strivacity/sdk-angular'; + +@Component({ standalone: true, selector: 'app-entry-page', templateUrl: './entry.page.html' }) +export class EntryPage { + constructor( + protected router: Router, + @SkipSelf() protected strivacityAuthService: StrivacityAuthService, + ) {} + + ngOnInit() { + this.strivacityAuthService.entry().subscribe({ + next: (data) => { + if (data && Object.keys(data).length > 0) { + void this.router.navigate(['/callback'], { queryParams: new URLSearchParams(data) }); + } else { + void this.router.navigate(['/']); + } + }, + error: (error) => { + alert(error); + void this.router.navigate(['/']); + }, + }); + } +} +``` + +The callback page detects the `session_id` parameter and forwards it to `/login` to continue the native or embedded flow, instead of running the standard `handleCallback()` path. + +The login page extracts `session_id`, `short_app_id`, and `language` from the URL on load, cleans up the URL, and passes them to the renderer. When a `session_id` is present the renderer calls `startSession(sessionId)` to resume the existing flow instead of starting a new login. When a `language` parameter is present it is passed to the renderer via `[language]`; the resolved language is emitted back via `(languageChange)`. + +You can also navigate directly to `/login?session_id=` to resume a flow without going through the entry page, which is useful when the `session_id` is obtained through your own backend logic. + +## Logging + +Enable the built-in console logger by passing `logging: DefaultLogging` to the SDK options: + +```typescript +import { provideStrivacity, DefaultLogging } from '@strivacity/sdk-angular'; + +provideStrivacity({ + // ...other options + logging: DefaultLogging, +}); +``` + +The default logger writes to the browser console and prefixes messages with the `xEventId` correlation ID when available. + +To use a custom logger, implement the `SDKLogging` interface and register your class in the SDK options: + +```typescript +import type { SDKLogging } from '@strivacity/sdk-angular'; + +export class MyLogger implements SDKLogging { + xEventId?: string; + + debug(message: string): void { + console.debug(this.xEventId ? `(${this.xEventId}) ${message}` : message); + } + info(message: string): void { + console.info(this.xEventId ? `(${this.xEventId}) ${message}` : message); + } + warn(message: string): void { + console.warn(this.xEventId ? `(${this.xEventId}) ${message}` : message); + } + error(message: string, error: Error): void { + console.error(this.xEventId ? `(${this.xEventId}) ${message}` : message, error); + } +} +``` + +```typescript +import { provideStrivacity } from '@strivacity/sdk-angular'; +import { MyLogger } from './logging/MyLogger'; + +provideStrivacity({ + // ...other options + logging: MyLogger, +}); +``` + +## Pages + +| Page | Path | Description | +| -------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Home | `src/app/pages/home/home.page.ts` | Public landing page. Displays user info when authenticated. | +| Login | `src/app/pages/login/login.page.ts` | Entry point for the authentication flow. Accepts optional `session_id` and `short_app_id` URL parameters to resume an existing flow instead of starting a new one. An optional `language` URL parameter is passed to the renderer via `[language]`; the resolved language is emitted back via `(languageChange)`. | +| Register | `src/app/pages/register/register.page.ts` | Entry point for the registration flow. Mirrors the login page structure with an extra `prompt: create` parameter. Also accepts an optional `language` URL parameter, passed to the renderer in the same way. | +| Callback | `src/app/pages/callback/callback.page.ts` | Handles the identity provider's redirect response. Routes to the login page when a `session_id` is present, otherwise completes the standard authorization flow. | +| Entry | `src/app/pages/entry/entry.page.ts` | Entry point for externally-initiated flows (e.g. password reset). Processes the incoming URL and routes to the appropriate next step. | +| Profile | `src/app/pages/profile/profile.page.ts` | Protected page showing the authenticated user's session details and token information. | +| Revoke | `src/app/pages/revoke/revoke.page.ts` | Invalidates the current session tokens without a full logout and returns the user to the home page. | +| Logout | `src/app/pages/logout/logout.page.ts` | Terminates the user's session and redirects to the home page after logout. | + +## Vulnerability Reporting + +The [Guidelines for responsible disclosure](https://www.strivacity.com/report-a-security-issue) details the procedure for disclosing security issues. Please do not report security vulnerabilities on the public issue tracker. + +## License + +This example app is available under the MIT License. See the [LICENSE](https://github.com/Strivacity/sdk-js/blob/main/LICENSE) file for more info. + +## Contributing + +Please see our [contributing guide](https://github.com/Strivacity/sdk-js/blob/main/CONTRIBUTING.md). diff --git a/apps/angular-standalone/eslint.config.js b/apps/angular-standalone/eslint.config.js new file mode 100644 index 00000000..b8b0a28f --- /dev/null +++ b/apps/angular-standalone/eslint.config.js @@ -0,0 +1,36 @@ +import angular from '@angular-eslint/eslint-plugin'; +import angularTemplate from '@angular-eslint/eslint-plugin-template'; +import { defineAngularConfig } from '../../eslint.config.base.js'; + +export default defineAngularConfig( + { + ignores: ['webpack.config.cjs'], + }, + { + files: ['./src/**/*.ts'], + plugins: { + '@angular-eslint': angular, + '@angular-eslint/template': angularTemplate, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'app', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'app', + style: 'kebab-case', + }, + ], + }, + }, +); diff --git a/apps/angular-standalone/package.json b/apps/angular-standalone/package.json new file mode 100644 index 00000000..541b90bd --- /dev/null +++ b/apps/angular-standalone/package.json @@ -0,0 +1,11 @@ +{ + "private": true, + "name": "@strivacity/app-angular", + "version": "1.0.11", + "type": "module", + "license": "MIT", + "author": "strivacity ", + "dependencies": { + "@strivacity/sdk-angular": "2.2.0" + } +} diff --git a/apps/angular-standalone/project.json b/apps/angular-standalone/project.json new file mode 100644 index 00000000..e9fc1407 --- /dev/null +++ b/apps/angular-standalone/project.json @@ -0,0 +1,55 @@ +{ + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "name": "app-angular-standalone", + "projectType": "application", + "sourceRoot": "apps/angular-standalone/src", + "targets": { + "build": { + "builder": "", + "executor": "@angular-builders/custom-webpack:browser", + "dependsOn": ["sdk-angular:build"], + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "apps/angular-standalone/dist", + "deleteOutputPath": false, + "index": "apps/angular-standalone/src/index.html", + "main": "apps/angular-standalone/src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "apps/angular-standalone/tsconfig.json", + "customWebpackConfig": { + "path": "apps/angular-standalone/webpack.config.cjs" + } + }, + "configurations": { + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "development" + }, + "serve": { + "executor": "@angular-builders/custom-webpack:dev-server", + "dependsOn": ["sdk-angular:build"], + "configurations": { + "production": { + "buildTarget": "app-angular:build:production" + }, + "development": { + "buildTarget": "app-angular:build:development" + } + }, + "defaultConfiguration": "development" + }, + "serve-static": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "app-angular:build", + "port": 4200, + "staticFilePath": "dist/apps/angular-standalone/browser", + "spa": true + } + } + } +} diff --git a/apps/angular-standalone/src/app/app.component.html b/apps/angular-standalone/src/app/app.component.html new file mode 100644 index 00000000..b7cdbb1a --- /dev/null +++ b/apps/angular-standalone/src/app/app.component.html @@ -0,0 +1,21 @@ +
+
+ @if (isAuthenticated) { + Welcome, {{ name }}! + } @else if (loading) { + Loading... + } +
+
+ Home + @if (isAuthenticated) { + Profile + Revoke + Logout + } @else { + Login + Register + } +
+
+ diff --git a/apps/angular-standalone/src/app/app.component.scss b/apps/angular-standalone/src/app/app.component.scss new file mode 100644 index 00000000..379becee --- /dev/null +++ b/apps/angular-standalone/src/app/app.component.scss @@ -0,0 +1,59 @@ +// stylelint-disable selector-pseudo-element-no-unknown + +:host { + display: block; + font-family: + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Open Sans', + 'Helvetica Neue', + sans-serif; + max-width: 1200px; + margin: 0 auto; +} + +header { + display: flex; + align-items: center; + padding: 1rem; + border-block-end: 1px solid rgb(0 0 0 / 15%); + + > div { + display: flex; + align-items: center; + gap: 1rem; + + + div { + margin-inline-start: auto; + } + } +} + +::ng-deep { + section { + padding: 1rem; + } + + section:has(sty-login) { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + sty-language-selector { + margin-block-start: 1rem; + } + + .login-renderer { + display: block; + margin: 0 auto; + width: 360px; + } +} diff --git a/apps/angular-standalone/src/app/app.component.ts b/apps/angular-standalone/src/app/app.component.ts new file mode 100644 index 00000000..16ea1660 --- /dev/null +++ b/apps/angular-standalone/src/app/app.component.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; +import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; +import { StrivacityAuthService } from '@strivacity/sdk-angular'; + +@Component({ + standalone: true, + selector: 'app-root', + templateUrl: './app.component.html', + styleUrl: './app.component.scss', + providers: [StrivacityAuthService], + imports: [RouterOutlet, RouterLink, RouterLinkActive], +}) +export class AppComponent { + loading = true; + isAuthenticated = false; + name = ''; + + constructor(protected strivacityAuthService: StrivacityAuthService) { + // eslint-disable-next-line @typescript-eslint/no-implied-eval + new Function('url', 'return import(url)')(`${this.strivacityAuthService.sdk.options.issuer}/assets/components/bundle.js`); + + this.strivacityAuthService.session$.subscribe((session) => { + this.loading = session.loading; + this.isAuthenticated = session.isAuthenticated; + this.name = `${session.idTokenClaims?.given_name ?? ''} ${session.idTokenClaims?.family_name ?? ''}`; + }); + } +} diff --git a/apps/angular-standalone/src/app/app.config.ts b/apps/angular-standalone/src/app/app.config.ts new file mode 100644 index 00000000..3414535f --- /dev/null +++ b/apps/angular-standalone/src/app/app.config.ts @@ -0,0 +1,34 @@ +import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { provideStrivacity, DefaultLogging } from '@strivacity/sdk-angular'; +import { routes } from './app.routes'; + +export interface ImportMeta { + env: { + VITE_MODE: 'redirect' | 'popup' | 'native'; + VITE_ISSUER: string; + VITE_SCOPES: string; + VITE_CLIENT_ID: string; + VITE_REDIRECT_URI: string; + VITE_LOGIN_HINT: string; + VITE_ACR_VALUES: string; + VITE_UI_LOCALES: string; + VITE_AUDIENCES: string; + }; +} + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideStrivacity({ + mode: (import.meta as unknown as ImportMeta).env.VITE_MODE, + issuer: (import.meta as unknown as ImportMeta).env.VITE_ISSUER, + scopes: (import.meta as unknown as ImportMeta).env.VITE_SCOPES.split(' '), + clientId: (import.meta as unknown as ImportMeta).env.VITE_CLIENT_ID, + redirectUri: (import.meta as unknown as ImportMeta).env.VITE_REDIRECT_URI, + storageTokenName: 'sty.session.angular', + logging: DefaultLogging, + }), + ], +}; diff --git a/apps/angular-standalone/src/app/app.routes.ts b/apps/angular-standalone/src/app/app.routes.ts new file mode 100644 index 00000000..38ffd6c0 --- /dev/null +++ b/apps/angular-standalone/src/app/app.routes.ts @@ -0,0 +1,20 @@ +import { Routes } from '@angular/router'; +import { CallbackPage } from './pages/callback'; +import { HomePage } from './pages/home'; +import { LoginPage } from './pages/login'; +import { LogoutPage } from './pages/logout'; +import { ProfilePage } from './pages/profile'; +import { RegisterPage } from './pages/register'; +import { RevokePage } from './pages/revoke'; +import { EntryPage } from './pages/entry'; + +export const routes: Routes = [ + { path: '', component: HomePage }, + { path: 'callback', component: CallbackPage }, + { path: 'login', component: LoginPage }, + { path: 'logout', component: LogoutPage }, + { path: 'profile', component: ProfilePage }, + { path: 'register', component: RegisterPage }, + { path: 'revoke', component: RevokePage }, + { path: 'entry', component: EntryPage }, +]; diff --git a/apps/angular-standalone/src/app/components/widgets/README.md b/apps/angular-standalone/src/app/components/widgets/README.md new file mode 100644 index 00000000..d5a05657 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/README.md @@ -0,0 +1,870 @@ +# Widgets + +Widgets are reusable Angular components for building forms and interactive elements. They are customizable and flexible for various use cases. + +## Layout Widget + +The Layout Widget lets you create custom layouts for your forms. + +### Properties + +- `formId`: The form ID. +- `type`: Layout type (e.g. `'vertical'`, `'horizontal'`). +- `tag`: HTML tag (default: `'div'`). + +### How to customize + +```typescript +@Component({ + selector: 'custom-layout-widget', + template: ` + +

Identifier Form

+ +
+ + + + `, +}) +export class CustomLayoutWidget { + @Input() formId!: string; +} +``` + +## Static Widget + +Displays static content (text or HTML). + +### Properties + +- `formId`: The form ID. +- `config`: Static widget config. + +### How to customize + +```typescript +@Component({ + selector: 'custom-static-widget', + template: ` + + +

Identifier Form Static Text

+

{{ config.value }}

+
+ +
{{ config.value }}
+
+
+ +
+
+ `, +}) +export class CustomStaticWidget { + @Input() formId!: string; + @Input() config!: any; +} +``` + +## Input Widget + +Input field for text, email, number, etc. + +### Properties + +- `formId`: The form ID. +- `config`: Input widget config. + +### How to customize + +```typescript +@Component({ + selector: 'custom-input-widget', + template: ` + + + +
Additional text for identifier input
+
+ + + + + `, +}) +export class CustomInputWidget { + @Input() formId!: string; + @Input() config!: any; + onChange(event: Event) { + // handle change + } +} +``` + +## Submit Widget + +### Properties + +- `formId`: The form ID. +- `config`: Submit widget config. + +### How to customize + +```typescript +@Component({ + selector: 'custom-submit-widget', + template: ` + + + +
Additional text for identifier form submit button
+
+ + + +
+ + + {{ formId === 'additionalActions/registration' && config.id === 'submit' ? 'Register now' : config.label }} + + + `, +}) +export class CustomSubmitWidget { + @Input() formId!: string; + @Input() config!: any; +} +``` + +## Checkbox Widget + +Checkbox input. + +### Properties + +- `formId`: The form ID. +- `config`: Checkbox widget config. + +### How to customize + +```typescript +@Component({ + selector: 'custom-checkbox-widget', + template: ` + + + +
Extra info for identifier checkbox
+
+ + + + + `, +}) +export class CustomCheckboxWidget { + @Input() formId!: string; + @Input() config!: any; +} +``` + +## Date Widget + +Date input field (native or custom fieldset). + +### Properties + +- `formId`: The form ID. +- `config`: Date widget config. + +### How to customize + +```typescript +@Component({ + selector: 'custom-date-widget', + template: ` + + +
Extra info for identifier date field
+
+ + + + `, +}) +export class CustomDateWidget { + @Input() formId!: string; + @Input() config!: any; +} +``` + +## Password Widget + +Password input field. + +### Properties + +- `formId`: The form ID. +- `config`: Password widget config. + +### How to customize + +```typescript +@Component({ + selector: 'custom-password-widget', + template: ` + + +
Password requirements for identifier form
+
+ + + + `, +}) +export class CustomPasswordWidget { + @Input() formId!: string; + @Input() config!: any; +} +``` + +## Passcode Widget + +Numeric code input. + +### Properties + +- `formId`: The form ID. +- `config`: Passcode widget config. + +### How to customize + +```typescript +@Component({ + selector: 'custom-passcode-widget', + template: ` + + +
Extra info for identifier passcode
+
+ + + + `, +}) +export class CustomPasscodeWidget { + @Input() formId!: string; + @Input() config!: any; +} +``` + +## Phone Widget + +Phone number input field. + +### Properties + +- `formId`: The form ID. +- `config`: Phone widget config. + +### How to customize + +```typescript +@Component({ + selector: 'custom-phone-widget', + template: ` + + +
Extra info for identifier phone field
+
+ + + + `, +}) +export class CustomPhoneWidget { + @Input() formId!: string; + @Input() config!: any; +} +``` + +## Select Widget + +Dropdown or radio group selection. + +### Properties + +- `formId`: The form ID. +- `config`: Select widget config. + +### How to customize + +```typescript +@Component({ + selector: 'custom-select-widget', + template: ` + + +
Extra info for identifier select
+
+ + + + `, +}) +export class CustomSelectWidget { + @Input() formId!: string; + @Input() config!: any; +} +``` + +## MultiSelect Widget + +Multiple selection (checkboxes or dropdown). + +### Properties + +- `formId`: The form ID. +- `config`: MultiSelect widget config. + +### How to customize + +```typescript +@Component({ + selector: 'custom-multiselect-widget', + template: ` + + +
+

{{ option.label }}

+ +
+ +
+
Extra info for identifier multi-select
+
+ + +
+

{{ option.label }}

+ +
+ +
+
+ `, +}) +export class CustomMultiSelectWidget { + @Input() formId!: string; + @Input() config!: any; +} +``` + +## Passkey Login Widget + +The Passkey Login Widget allows users to authenticate using passkeys (WebAuthn credentials). It uses the `getCredential` function to retrieve authentication data. + +### Properties + +- `formId`: The form ID. +- `config`: Passkey login widget config, which includes: + - `id`: The unique identifier for the passkey login widget. + - `type`: The type of the widget. + - `label`: The label displayed on the passkey login widget. + - `assertionOptions`: WebAuthn assertion options for authentication. + - `render`: Rendering options: + - `type`: The type of login control, either `'button'` or `'link'`. + +### How to customize + +```typescript +@Component({ + selector: 'custom-passkey-login-widget', + template: ` + + + + + {{ config.label }} + + +
Additional content for identifier passkey login
+
+ + + + + {{ config.label }} + + + + `, +}) +export class CustomPasskeyLoginWidget { + @Input() formId!: string; + @Input() config!: any; + + get disabled() { + return !!this.widgetService.loading$.value; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + async onClick() { + if (this.disabled) { + return; + } + + try { + const response = await getCredential(this.config.assertionOptions); + this.widgetService.setFormValue(this.formId, this.config.id, response); + await this.widgetService.submitForm(this.formId); + } catch (error) { + console.error(error); + alert('Authentication failed. Please try again.'); + } + } + + async onKeyDown(event: KeyboardEvent) { + if (['Enter', 'Space'].includes(event.code)) { + await this.onClick(); + } + } +} +``` + +## Passkey Enroll Widget + +The Passkey Enroll Widget allows users to register new passkeys. It uses the `createCredential` function to create new WebAuthn credentials. + +### Properties + +- `formId`: The form ID. +- `config`: Passkey enroll widget config, which includes: + - `id`: The unique identifier for the passkey enroll widget. + - `type`: The type of the widget. + - `label`: The label displayed on the passkey enroll widget. + - `enrollOptions`: WebAuthn creation options for registration. + - `render`: Rendering options: + - `type`: The type of enroll control, either `'button'` or `'link'`. + +### How to customize + +```typescript +@Component({ + selector: 'custom-passkey-enroll-widget', + template: ` + + + + + {{ config.label }} + + +
Additional content for identifier passkey enrollment
+
+ + + + + {{ config.label }} + + + + `, +}) +export class CustomPasskeyEnrollWidget { + @Input() formId!: string; + @Input() config!: any; + + get disabled() { + return !!this.widgetService.loading$.value; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + async onClick() { + if (this.disabled) { + return; + } + + try { + const response = await createCredential(this.config.enrollOptions); + this.widgetService.setFormValue(this.formId, this.config.id, response); + await this.widgetService.submitForm(this.formId); + } catch (error) { + console.error(error); + alert('Enrollment failed. Please try again.'); + } + } + + async onKeyDown(event: KeyboardEvent) { + if (['Enter', 'Space'].includes(event.code)) { + await this.onClick(); + } + } +} +``` + +## WebAuthn Login Widget + +The WebAuthn Login Widget provides WebAuthn-based authentication functionality, similar to the Passkey Login Widget but specifically for WebAuthn protocols. + +### Properties + +- `formId`: The form ID. +- `config`: WebAuthn login widget config, which includes: + - `id`: The unique identifier for the webauthn login widget. + - `type`: The type of the widget. + - `label`: The label displayed on the webauthn login widget. + - `assertionOptions`: WebAuthn assertion options for authentication. + - `render`: Rendering options: + - `type`: The type of login control, either `'button'` or `'link'`. + +### How to customize + +```typescript +@Component({ + selector: 'custom-webauthn-login-widget', + template: ` + + + + + {{ config.label }} + + +
Additional content for identifier WebAuthn login
+
+ + + + + {{ config.label }} + + + + `, +}) +export class CustomWebAuthnLoginWidget { + @Input() formId!: string; + @Input() config!: any; + + get disabled() { + return !!this.widgetService.loading$.value; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + async onClick() { + if (this.disabled) { + return; + } + + try { + const response = await getCredential(this.config.assertionOptions); + this.widgetService.setFormValue(this.formId, this.config.id, response); + await this.widgetService.submitForm(this.formId); + } catch (error) { + console.error(error); + alert('Authentication failed. Please try again.'); + } + } + + async onKeyDown(event: KeyboardEvent) { + if (['Enter', 'Space'].includes(event.code)) { + await this.onClick(); + } + } +} +``` + +## WebAuthn Enroll Widget + +The WebAuthn Enroll Widget allows users to register new WebAuthn credentials, similar to the Passkey Enroll Widget but specifically for WebAuthn protocols. + +### Properties + +- `formId`: The form ID. +- `config`: WebAuthn enroll widget config, which includes: + - `id`: The unique identifier for the webauthn enroll widget. + - `type`: The type of the widget. + - `label`: The label displayed on the webauthn enroll widget. + - `enrollOptions`: WebAuthn creation options for registration. + - `render`: Rendering options: + - `type`: The type of enroll control, either `'button'` or `'link'`. + +### How to customize + +```typescript +@Component({ + selector: 'custom-webauthn-enroll-widget', + template: ` + + + + + {{ config.label }} + + +
Additional content for identifier WebAuthn enrollment
+
+ + + + + {{ config.label }} + + + + `, +}) +export class CustomWebAuthnEnrollWidget { + @Input() formId!: string; + @Input() config!: any; + + get disabled() { + return !!this.widgetService.loading$.value; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + async onClick() { + if (this.disabled) { + return; + } + + try { + const response = await createCredential(this.config.enrollOptions); + this.widgetService.setFormValue(this.formId, this.config.id, response); + await this.widgetService.submitForm(this.formId); + } catch (error) { + console.error(error); + alert('Enrollment failed. Please try again.'); + } + } + + async onKeyDown(event: KeyboardEvent) { + if (['Enter', 'Space'].includes(event.code)) { + await this.onClick(); + } + } +} +``` + +## Credential Management Functions + +The passkey and WebAuthn widgets rely on two core functions for credential management: + +### `createCredential(credentialOptions)` + +This function is used for registering new credentials (passkeys/WebAuthn). It: + +- Takes `PublicKeyCredentialCreationOptions` as input +- Handles Base64URL encoding/decoding of credential data +- Uses the browser's WebAuthn API to create new credentials +- Returns `AttestationCredentialData` containing the new credential information +- Includes error handling for unsupported credential types + +### `getCredential(credentialOptions, conditional?)` + +This function is used for authenticating with existing credentials. It: + +- Takes `PublicKeyCredentialRequestOptions` as input and an optional conditional flag +- Handles Base64URL encoding/decoding of assertion data +- Uses the browser's WebAuthn API to get existing credentials +- Returns `AssertionCredentialData` containing the authentication response +- Supports conditional UI for passive credential selection +- Includes error handling for authentication failures + +Both functions are imported from `@strivacity/sdk-angular` and handle the low-level WebAuthn protocol details, making it easy to integrate passkey authentication into your Angular applications. + +## Loading Widget + +Animated loading icon. + +### Properties + +- `diameter`: Icon diameter (default: 80). +- `stroke`: Stroke width (default: 2.5). + +### Example + +```typescript +@Component({ + selector: 'custom-loading-widget', + template: ` + + `, +}) +export class CustomLoadingWidget {} +``` diff --git a/apps/angular-standalone/src/app/components/widgets/checkbox/checkbox.widget.html b/apps/angular-standalone/src/app/components/widgets/checkbox/checkbox.widget.html new file mode 100644 index 00000000..13110777 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/checkbox/checkbox.widget.html @@ -0,0 +1,18 @@ +
+ + @if (config.label) { + + } +
+@if (errorMessage) { +{{ errorMessage }} +} diff --git a/apps/angular-standalone/src/app/components/widgets/checkbox/checkbox.widget.scss b/apps/angular-standalone/src/app/components/widgets/checkbox/checkbox.widget.scss new file mode 100644 index 00000000..13de185c --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/checkbox/checkbox.widget.scss @@ -0,0 +1,27 @@ +[data-widget='checkbox'] { + display: flex; + flex-direction: column; + margin-block-start: 1rem; + + .label { + margin-block-end: 0.25rem; + } + + input { + transform: scale(1.4); + margin-inline-end: 0.5rem; + accent-color: #5d21ab; + outline-color: #5d21ab; + + &:disabled { + background: hsl(0deg 0% 98%); + border-color: hsl(0deg 0% 90%); + cursor: not-allowed; + } + } + + .error { + margin-block-start: 0.25rem; + color: #e40c0c; + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/checkbox/checkbox.widget.ts b/apps/angular-standalone/src/app/components/widgets/checkbox/checkbox.widget.ts new file mode 100644 index 00000000..a6da493e --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/checkbox/checkbox.widget.ts @@ -0,0 +1,48 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { CheckboxWidget as CheckboxWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-checkbox-widget', + templateUrl: './checkbox.widget.html', + styleUrls: ['./checkbox.widget.scss'], + host: { + 'data-widget': 'checkbox', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class CheckboxWidget { + @Input() formId!: string; + @Input() config!: CheckboxWidgetConfig; + + get disabled() { + return !!this.widgetService.loading$.value || !!this.config.readonly; + } + + get value() { + return this.widgetService.forms$.value[this.formId]?.[this.config.id] ?? this.config.value; + } + + get errorMessage() { + return this.widgetService.messages$.value[this.formId]?.[this.config.id]?.text; + } + + get validator() { + return this.config.validator; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + onChange(event: Event) { + if (this.disabled) { + return; + } + + this.widgetService.setFormValue(this.formId, this.config.id, (event.target as HTMLInputElement).checked); + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/close/close.widget.html b/apps/angular-standalone/src/app/components/widgets/close/close.widget.html new file mode 100644 index 00000000..c7bb7431 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/close/close.widget.html @@ -0,0 +1,13 @@ +@if (config.render?.type === 'button') { + +} @else { +{{ config.label }} +} diff --git a/apps/angular-standalone/src/app/components/widgets/close/close.widget.scss b/apps/angular-standalone/src/app/components/widgets/close/close.widget.scss new file mode 100644 index 00000000..acfe068d --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/close/close.widget.scss @@ -0,0 +1,54 @@ +[data-widget='close'] { + display: block; + + button, + a { + display: block; + width: 100%; + cursor: pointer; + outline-color: #5d21ab; + } + + &[data-type='button'] { + button { + display: inline-flex; + align-items: center; + justify-content: center; + margin-block: 1rem; + min-height: 2.25rem; + padding: 0.5rem 1rem; + font-size: 1rem; + box-shadow: rgb(0 0 0 / 5%) 0 1px 2px 0; + border: 1px solid rgb(0 0 0 / 15%); + border-radius: 4px; + position: relative; + + &:disabled { + cursor: not-allowed; + background-color: #f5f5f5 !important; + color: #bdbdbd !important; + border-color: #e0e0e0; + box-shadow: none; + } + } + } + + &[data-type='link'] { + a { + display: inline-block; + text-align: center; + color: #5d21ab; + + &:hover { + color: oklch(from #5d21ab l calc(l * 0.85) c h); + } + } + } + + + [data-widget='close'] { + button, + a { + margin-top: 1rem; + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/close/close.widget.ts b/apps/angular-standalone/src/app/components/widgets/close/close.widget.ts new file mode 100644 index 00000000..dfc7846f --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/close/close.widget.ts @@ -0,0 +1,43 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { CloseWidget as CloseWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-close-widget', + templateUrl: './close.widget.html', + styleUrls: ['./close.widget.scss'], + host: { + 'data-widget': 'close', + '[attr.data-type]': 'config.render?.type', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class CloseWidget { + @Input() formId!: string; + @Input() config!: CloseWidgetConfig; + + get disabled() { + return !!this.widgetService.loading$.value; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + onClose() { + if (this.disabled) { + return; + } + + this.widgetService.triggerClose(); + } + + onKeyDown(event: KeyboardEvent) { + if (['Enter', 'Space'].includes(event.code)) { + this.onClose(); + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/date/date.widget.html b/apps/angular-standalone/src/app/components/widgets/date/date.widget.html new file mode 100644 index 00000000..3443481c --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/date/date.widget.html @@ -0,0 +1,33 @@ +@if (config.label) { + +} @if (config.render?.type === 'fieldSet') { +
+ @for (field of format; track field) { + + } +
+} @else { + +} @if (errorMessage) { +{{ errorMessage }} +} diff --git a/apps/angular-standalone/src/app/components/widgets/date/date.widget.scss b/apps/angular-standalone/src/app/components/widgets/date/date.widget.scss new file mode 100644 index 00000000..572879c5 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/date/date.widget.scss @@ -0,0 +1,49 @@ +[data-widget='date'] { + display: flex; + flex-direction: column; + margin-block-start: 1rem; + + .fieldset { + display: grid; + gap: 0.5rem; + } + + .label { + margin-block-end: 0.25rem; + } + + input { + position: relative; + display: flex; + align-items: center; + height: 2.5rem; + padding-inline: 0.5rem; + font-size: 1rem; + background: hsl(0deg 0% 100%); + border-width: 1px; + border-style: solid; + border-color: hsl(0deg 0% 90%); + border-radius: 4px; + box-shadow: rgb(0 0 0 / 5%) 0 1px 2px 0; + transition: + color 150ms ease-in-out, + background 150ms ease-in-out, + border-color 150ms ease-in-out; + + &:focus { + outline: none; + border-color: #5d21ab; + } + + &:read-only { + background: hsl(0deg 0% 98%); + border-color: hsl(0deg 0% 90%); + cursor: not-allowed; + } + } + + .error { + margin-block-start: 0.25rem; + color: #e40c0c; + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/date/date.widget.ts b/apps/angular-standalone/src/app/components/widgets/date/date.widget.ts new file mode 100644 index 00000000..d5e6c495 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/date/date.widget.ts @@ -0,0 +1,142 @@ +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { DateWidget as DateWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService } from '@strivacity/sdk-angular'; +import { DateTime } from 'luxon'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-date-widget', + templateUrl: './date.widget.html', + styleUrls: ['./date.widget.scss'], + host: { + 'data-widget': 'date', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class DateWidget implements OnInit { + @Input() formId!: string; + @Input() config!: DateWidgetConfig; + + year = ''; + month = ''; + day = ''; + + readonly fieldLengths = { year: 4, month: 2, day: 2 }; + readonly placeholders = { + year: 'YYYY', + month: 'MM', + day: 'DD', + }; + readonly format: Array<'year' | 'month' | 'day'> = ['year', 'month', 'day']; + + get disabled() { + return !!this.widgetService.loading$.value || !!this.config.readonly; + } + + get value() { + return (this.widgetService.forms$.value[this.formId]?.[this.config.id] as string) ?? this.config.value ?? ''; + } + + get errorMessage() { + return this.widgetService.messages$.value[this.formId]?.[this.config.id]?.text; + } + + get validator() { + return this.config.validator; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + ngOnInit() { + // Default value handling + if (this.value && this.value.length > 0) { + this.widgetService.setFormValue(this.formId, this.config.id, this.value); + } + this.setInitialValues(); + } + + private setInitialValues() { + if (!this.value) { + return; + } + + const date = DateTime.fromISO(this.value); + + if (date.isValid) { + const [y, m, d] = this.value.split('-'); + this.year = y || ''; + this.month = m || ''; + this.day = d || ''; + } + } + + getFieldValue(field: 'year' | 'month' | 'day'): string { + switch (field) { + case 'year': + return this.year; + case 'month': + return this.month; + case 'day': + return this.day; + default: + return ''; + } + } + + onFieldsetChange(event: Event) { + if (this.disabled) { + return; + } + + const input = event.target as HTMLInputElement; + const field = input.name as 'year' | 'month' | 'day'; + const inputValue = input.value; + + if (field === 'year') { + this.year = inputValue; + } else if (field === 'month') { + this.month = inputValue; + } else if (field === 'day') { + this.day = inputValue; + } + + if (this.year.length === 4 && this.month.length >= 1 && this.day.length >= 1) { + const dateValue = DateTime.fromObject({ + year: Number(this.year), + month: Number(this.month), + day: Number(this.day), + }); + + if (dateValue.isValid) { + this.widgetService.setFormValue(this.formId, this.config.id, dateValue.toISODate()); + } + } + } + + onChange(event: Event) { + if (this.disabled) { + return; + } + + this.widgetService.setFormValue(this.formId, this.config.id, (event.target as HTMLInputElement).value); + } + + async onKeyDown(event: KeyboardEvent) { + if (this.disabled || event.key !== 'Enter') { + return; + } + + event.stopPropagation(); + + const input = event.target as HTMLInputElement; + + if (input.reportValidity()) { + this.widgetService.setFormValue(this.formId, this.config.id, (event.target as HTMLInputElement).value); + await this.widgetService.submitForm(this.formId); + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/index.ts b/apps/angular-standalone/src/app/components/widgets/index.ts new file mode 100644 index 00000000..dbd96561 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/index.ts @@ -0,0 +1,41 @@ +import { Type } from '@angular/core'; +import { WidgetType } from '@strivacity/sdk-angular'; + +import { CheckboxWidget } from './checkbox/checkbox.widget'; +import { DateWidget } from './date/date.widget'; +import { InputWidget } from './input/input.widget'; +import { LayoutWidget } from './layout/layout.widget'; +import { LoadingWidget } from './loading/loading.widget'; +import { MultiSelectWidget } from './multiselect/multiselect.widget'; +import { PasscodeWidget } from './passcode/passcode.widget'; +import { PasswordWidget } from './password/password.widget'; +import { PhoneWidget } from './phone/phone.widget'; +import { SelectWidget } from './select/select.widget'; +import { StaticWidget } from './static/static.widget'; +import { SubmitWidget } from './submit/submit.widget'; +import { CloseWidget } from './close/close.widget'; +import { PasskeyEnrollWidget } from './passkey-enroll/passkey-enroll.widget'; +import { PasskeyLoginWidget } from './passkey-login/passkey-login.widget'; +import { WebAuthnEnrollWidget } from './webauthn-enroll/webauthn-enroll.widget'; +import { WebAuthnLoginWidget } from './webauthn-login/webauthn-login.widget'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const widgets: Record> = { + checkbox: CheckboxWidget, + date: DateWidget, + input: InputWidget, + layout: LayoutWidget, + loading: LoadingWidget, + passcode: PasscodeWidget, + password: PasswordWidget, + phone: PhoneWidget, + select: SelectWidget, + multiSelect: MultiSelectWidget, + static: StaticWidget, + submit: SubmitWidget, + close: CloseWidget, + passkeyEnroll: PasskeyEnrollWidget, + passkeyLogin: PasskeyLoginWidget, + webauthnEnroll: WebAuthnEnrollWidget, + webauthnLogin: WebAuthnLoginWidget, +}; diff --git a/apps/angular-standalone/src/app/components/widgets/input/input.widget.html b/apps/angular-standalone/src/app/components/widgets/input/input.widget.html new file mode 100644 index 00000000..69c4ab2e --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/input/input.widget.html @@ -0,0 +1,22 @@ +@if (config.label) { + +} + +@if (errorMessage) { +{{ errorMessage }} +} diff --git a/apps/angular-standalone/src/app/components/widgets/input/input.widget.scss b/apps/angular-standalone/src/app/components/widgets/input/input.widget.scss new file mode 100644 index 00000000..222e6e5e --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/input/input.widget.scss @@ -0,0 +1,44 @@ +[data-widget='input'] { + display: flex; + flex-direction: column; + margin-block-start: 1rem; + + .label { + margin-block-end: 0.25rem; + } + + input { + position: relative; + display: flex; + align-items: center; + height: 2.5rem; + padding-inline: 0.5rem; + font-size: 1rem; + background: hsl(0deg 0% 100%); + border-width: 1px; + border-style: solid; + border-color: hsl(0deg 0% 90%); + border-radius: 4px; + box-shadow: rgb(0 0 0 / 5%) 0 1px 2px 0; + transition: + color 150ms ease-in-out, + background 150ms ease-in-out, + border-color 150ms ease-in-out; + + &:focus { + outline: none; + border-color: #5d21ab; + } + + &:read-only { + color: hsl(0deg 0% 50%); + border-color: hsl(0deg 0% 90%); + cursor: not-allowed; + } + } + + .error { + margin-block-start: 0.25rem; + color: #e40c0c; + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/input/input.widget.ts b/apps/angular-standalone/src/app/components/widgets/input/input.widget.ts new file mode 100644 index 00000000..5e97a9b6 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/input/input.widget.ts @@ -0,0 +1,79 @@ +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { InputWidget as InputWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-input-widget', + templateUrl: './input.widget.html', + styleUrls: ['./input.widget.scss'], + host: { + 'data-widget': 'input', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class InputWidget implements OnInit { + @Input() formId!: string; + @Input() config!: InputWidgetConfig; + + get disabled() { + return !!this.widgetService.loading$.value || !!this.config.readonly; + } + + get value() { + return this.widgetService.forms$.value[this.formId]?.[this.config.id] ?? this.config.value; + } + + get errorMessage() { + return this.widgetService.messages$.value[this.formId]?.[this.config.id]?.text; + } + + get validator() { + return this.config.validator; + } + + get autocomplete() { + if (this.config.autocomplete && this.config.render?.autocompleteHint) { + return `${this.config.autocomplete} ${this.config.render.autocompleteHint}`; + } else if (this.config.autocomplete) { + return this.config.autocomplete; + } else { + return 'on'; + } + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + ngOnInit() { + // Default value handling + const value = this.value as string; + if (value && value.length > 0) { + this.widgetService.setFormValue(this.formId, this.config.id, value); + } + } + + onChange(event: Event) { + if (this.disabled) { + return; + } + + this.widgetService.setFormValue(this.formId, this.config.id, (event.target as HTMLInputElement).value); + } + + async onKeyDown(event: KeyboardEvent) { + if (this.disabled) { + return; + } + + const input = event.target as HTMLInputElement; + + if (input.reportValidity()) { + this.widgetService.setFormValue(this.formId, this.config.id, (event.target as HTMLInputElement).value); + await this.widgetService.submitForm(this.formId); + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/layout/layout.widget.html b/apps/angular-standalone/src/app/components/widgets/layout/layout.widget.html new file mode 100644 index 00000000..d1fe5b82 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/layout/layout.widget.html @@ -0,0 +1,13 @@ + + + + +@if (tag === 'form') { +
+ +
+} @else { +
+ +
+} diff --git a/apps/angular-standalone/src/app/components/widgets/layout/layout.widget.scss b/apps/angular-standalone/src/app/components/widgets/layout/layout.widget.scss new file mode 100644 index 00000000..c393280b --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/layout/layout.widget.scss @@ -0,0 +1,30 @@ +[data-widget='layout'] { + > div, + > form { + display: flex; + flex-direction: column; + width: 100%; + margin-block-end: 1rem; + } + + &[data-type='horizontal'] { + > div, + > form { + flex-direction: row; + align-items: center; + justify-content: center; + + * + * { + margin-inline-start: 0.25rem; + } + } + } + + &[data-type='vertical'] { + > div, + > form { + flex-direction: column; + justify-content: center; + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/layout/layout.widget.ts b/apps/angular-standalone/src/app/components/widgets/layout/layout.widget.ts new file mode 100644 index 00000000..7c818aba --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/layout/layout.widget.ts @@ -0,0 +1,39 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { LayoutWidget as LayoutWidgetType } from '@strivacity/sdk-core'; +import { StrivacityWidgetService } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-layout-widget', + templateUrl: './layout.widget.html', + styleUrls: ['./layout.widget.scss'], + host: { + 'data-widget': 'layout', + '[attr.data-form]': 'formId', + '[attr.data-type]': 'type', + }, +}) +export class LayoutWidget { + @Input() formId!: string; + @Input() type!: LayoutWidgetType['type']; + @Input() tag = 'div'; + + get disabled() { + return !!this.widgetService.loading$.value; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + async onSubmit(event: Event) { + event.preventDefault(); + + if (this.disabled) { + return; + } + + await this.widgetService.submitForm(this.formId); + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/loading/loading.widget.html b/apps/angular-standalone/src/app/components/widgets/loading/loading.widget.html new file mode 100644 index 00000000..efc62bd6 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/loading/loading.widget.html @@ -0,0 +1,3 @@ + diff --git a/apps/angular-standalone/src/app/components/widgets/loading/loading.widget.scss b/apps/angular-standalone/src/app/components/widgets/loading/loading.widget.scss new file mode 100644 index 00000000..58c48eff --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/loading/loading.widget.scss @@ -0,0 +1,45 @@ +@keyframes loading-rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes loading-dash { + 0% { + stroke-dasharray: 1, 200; + stroke-dashoffset: 0; + } + + 50% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -40px; + } + + 100% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -120px; + } +} + +[data-loader] { + position: relative; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + + .spinner { + fill: none; + animation: var(--sty-loader-spinner-animation, loading-rotate 2000ms linear infinite); + animation-direction: normal; + } + + .circle { + stroke: currentColor; + stroke-dasharray: 90, 150; + stroke-dashoffset: 0; + stroke-linecap: round; + animation: var(--sty-loader-circle-animation, loading-dash 1500ms ease-in-out infinite); + animation-direction: normal; + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/loading/loading.widget.ts b/apps/angular-standalone/src/app/components/widgets/loading/loading.widget.ts new file mode 100644 index 00000000..f85abb94 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/loading/loading.widget.ts @@ -0,0 +1,19 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-loading-widget', + templateUrl: './loading.widget.html', + styleUrls: ['./loading.widget.scss'], + host: { + 'data-loader': '', + 'data-widget': 'loading', + }, +}) +export class LoadingWidget { + @Input() diameter: number = 80; + @Input() stroke: number = 2.5; +} diff --git a/apps/angular-standalone/src/app/components/widgets/multiselect/multiselect.widget.html b/apps/angular-standalone/src/app/components/widgets/multiselect/multiselect.widget.html new file mode 100644 index 00000000..62cc4252 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/multiselect/multiselect.widget.html @@ -0,0 +1,24 @@ +@for (option of config.options; track $index) { +
+ @if (option.type === 'group') { +

{{ option.label }}

+ @for (subOption of option.options; track subOption.value) { +
+ + +
+ } } @else { + + + } +
+} @if (errorMessage) { +{{ errorMessage }} +} diff --git a/apps/angular-standalone/src/app/components/widgets/multiselect/multiselect.widget.scss b/apps/angular-standalone/src/app/components/widgets/multiselect/multiselect.widget.scss new file mode 100644 index 00000000..aa2952b7 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/multiselect/multiselect.widget.scss @@ -0,0 +1,31 @@ +[data-widget='multiselect'] { + display: flex; + flex-direction: column; + margin-block-start: 1rem; + + .label { + margin-block-end: 0.25rem; + } + + input { + transform: scale(1.4); + margin-inline-end: 0.5rem; + accent-color: #5d21ab; + outline-color: #5d21ab; + + &:disabled { + background: hsl(0deg 0% 98%); + border-color: hsl(0deg 0% 90%); + cursor: not-allowed; + } + } + + .error { + margin-block-start: 0.25rem; + color: #e40c0c; + } + + .item + .item { + margin-block-start: 0.5rem; + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/multiselect/multiselect.widget.ts b/apps/angular-standalone/src/app/components/widgets/multiselect/multiselect.widget.ts new file mode 100644 index 00000000..d361365d --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/multiselect/multiselect.widget.ts @@ -0,0 +1,51 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { MultiSelectWidget as MultiSelectWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-multiselect-widget', + templateUrl: './multiselect.widget.html', + styleUrls: ['./multiselect.widget.scss'], + host: { + 'data-widget': 'multiselect', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class MultiSelectWidget { + @Input() formId!: string; + @Input() config!: MultiSelectWidgetConfig; + + get disabled() { + return !!this.widgetService.loading$.value || !!this.config.readonly; + } + + get values() { + return this.widgetService?.forms$.value[this.formId]?.[this.config.id] || []; + } + + get errorMessage() { + return this.widgetService.messages$.value[this.formId]?.[this.config.id]?.text; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) { + if (this.config.value?.length) { + this.widgetService.setFormValue(this.formId, this.config.id, this.config.value); + } + } + + onChange(event: Event) { + if (this.disabled) { + return; + } + + const values = (this.widgetService.forms$.value[this.formId]?.[this.config.id] as Array) || []; + const value = (event.target as HTMLInputElement).value; + + this.widgetService.setFormValue(this.formId, this.config.id, values.includes(value) ? values.filter((v: string) => v !== value) : [...values, value]); + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/passcode/passcode.widget.html b/apps/angular-standalone/src/app/components/widgets/passcode/passcode.widget.html new file mode 100644 index 00000000..c95fb164 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/passcode/passcode.widget.html @@ -0,0 +1,20 @@ +@if (config.label) { + +} + +@if (errorMessage) { +{{ errorMessage }} +} diff --git a/apps/angular-standalone/src/app/components/widgets/passcode/passcode.widget.scss b/apps/angular-standalone/src/app/components/widgets/passcode/passcode.widget.scss new file mode 100644 index 00000000..2056cdc9 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/passcode/passcode.widget.scss @@ -0,0 +1,44 @@ +[data-widget='passcode'] { + display: flex; + flex-direction: column; + margin-block-start: 1rem; + + .label { + margin-block-end: 0.25rem; + } + + input { + position: relative; + display: flex; + align-items: center; + height: 2.5rem; + padding-inline: 0.5rem; + font-size: 1rem; + background: hsl(0deg 0% 100%); + border-width: 1px; + border-style: solid; + border-color: hsl(0deg 0% 90%); + border-radius: 4px; + box-shadow: rgb(0 0 0 / 5%) 0 1px 2px 0; + transition: + color 150ms ease-in-out, + background 150ms ease-in-out, + border-color 150ms ease-in-out; + + &:focus { + outline: none; + border-color: #5d21ab; + } + + &:read-only { + color: hsl(0deg 0% 50%); + border-color: hsl(0deg 0% 90%); + cursor: not-allowed; + } + } + + .error { + margin-block-start: 0.25rem; + color: #e40c0c; + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/passcode/passcode.widget.ts b/apps/angular-standalone/src/app/components/widgets/passcode/passcode.widget.ts new file mode 100644 index 00000000..54e03280 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/passcode/passcode.widget.ts @@ -0,0 +1,62 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { PasscodeWidget as PasscodeWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-passcode-widget', + templateUrl: './passcode.widget.html', + styleUrls: ['./passcode.widget.scss'], + host: { + 'data-widget': 'passcode', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class PasscodeWidget { + @Input() formId!: string; + @Input() config!: PasscodeWidgetConfig; + + get disabled() { + return !!this.widgetService.loading$.value; + } + + get errorMessage() { + return this.widgetService.messages$.value[this.formId]?.[this.config.id]?.text; + } + + get validator() { + return this.config.validator; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + onInput(event: Event) { + const inputElement = event.target as HTMLInputElement; + inputElement.value = inputElement.value.replace(/\D/g, ''); + } + + onChange(event: Event) { + if (this.disabled) { + return; + } + + this.widgetService.setFormValue(this.formId, this.config.id, (event.target as HTMLInputElement).value); + } + + async onKeyDown(event: KeyboardEvent) { + if (this.disabled) { + return; + } + + const input = event.target as HTMLInputElement; + + if (input.reportValidity()) { + this.widgetService.setFormValue(this.formId, this.config.id, (event.target as HTMLInputElement).value); + await this.widgetService.submitForm(this.formId); + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.html b/apps/angular-standalone/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.html new file mode 100644 index 00000000..99841c19 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.html @@ -0,0 +1,5 @@ +@if (config.render?.type === 'button') { + +} @else { +{{ config.label }} +} diff --git a/apps/angular-standalone/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.scss b/apps/angular-standalone/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.scss new file mode 100644 index 00000000..4bd61864 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.scss @@ -0,0 +1,47 @@ +[data-widget='passkeyEnroll'] { + display: block; + + button, + a { + display: block; + width: 100%; + cursor: pointer; + outline-color: #5d21ab; + } + + &[data-type='button'] { + button { + display: inline-flex; + align-items: center; + justify-content: center; + margin-block: 1rem; + min-height: 2.25rem; + padding: 0.5rem 1rem; + font-size: 1rem; + box-shadow: rgb(0 0 0 / 5%) 0 1px 2px 0; + border: 1px solid rgb(0 0 0 / 15%); + border-radius: 4px; + position: relative; + + &:disabled { + cursor: not-allowed; + background-color: #f5f5f5 !important; + color: #bdbdbd !important; + border-color: #e0e0e0; + box-shadow: none; + } + } + } + + &[data-type='link'] { + a { + display: inline-block; + text-align: center; + color: #5d21ab; + + &:hover { + color: oklch(from #5d21ab l calc(l * 0.85) c h); + } + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.ts b/apps/angular-standalone/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.ts new file mode 100644 index 00000000..1a916f5b --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.ts @@ -0,0 +1,51 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { PasskeyEnrollWidget as PasskeyEnrollWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService, createCredential } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-passkey-enroll-widget', + templateUrl: './passkey-enroll.widget.html', + styleUrls: ['./passkey-enroll.widget.scss'], + host: { + 'data-widget': 'passkeyEnroll', + '[attr.data-type]': 'config.render?.type', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class PasskeyEnrollWidget { + @Input() formId!: string; + @Input() config!: PasskeyEnrollWidgetConfig; + + get disabled() { + return !!this.widgetService.loading$.value; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + async onClick() { + if (this.disabled) { + return; + } + + try { + const response = await createCredential(this.config.enrollOptions); + this.widgetService.setFormValue(this.formId, this.config.id, response); + await this.widgetService.submitForm(this.formId); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + alert('Enrollment failed. Please try again.'); + } + } + + async onKeyDown(event: KeyboardEvent) { + if (['Enter', 'Space'].includes(event.code)) { + await this.onClick(); + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/passkey-login/passkey-login.widget.html b/apps/angular-standalone/src/app/components/widgets/passkey-login/passkey-login.widget.html new file mode 100644 index 00000000..99841c19 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/passkey-login/passkey-login.widget.html @@ -0,0 +1,5 @@ +@if (config.render?.type === 'button') { + +} @else { +{{ config.label }} +} diff --git a/apps/angular-standalone/src/app/components/widgets/passkey-login/passkey-login.widget.scss b/apps/angular-standalone/src/app/components/widgets/passkey-login/passkey-login.widget.scss new file mode 100644 index 00000000..3146dfac --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/passkey-login/passkey-login.widget.scss @@ -0,0 +1,47 @@ +[data-widget='passkeyLogin'] { + display: block; + + button, + a { + display: block; + width: 100%; + cursor: pointer; + outline-color: #5d21ab; + } + + &[data-type='button'] { + button { + display: inline-flex; + align-items: center; + justify-content: center; + margin-block: 1rem; + min-height: 2.25rem; + padding: 0.5rem 1rem; + font-size: 1rem; + box-shadow: rgb(0 0 0 / 5%) 0 1px 2px 0; + border: 1px solid rgb(0 0 0 / 15%); + border-radius: 4px; + position: relative; + + &:disabled { + cursor: not-allowed; + background-color: #f5f5f5 !important; + color: #bdbdbd !important; + border-color: #e0e0e0; + box-shadow: none; + } + } + } + + &[data-type='link'] { + a { + display: inline-block; + text-align: center; + color: #5d21ab; + + &:hover { + color: oklch(from #5d21ab l calc(l * 0.85) c h); + } + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/passkey-login/passkey-login.widget.ts b/apps/angular-standalone/src/app/components/widgets/passkey-login/passkey-login.widget.ts new file mode 100644 index 00000000..d94dfd52 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/passkey-login/passkey-login.widget.ts @@ -0,0 +1,51 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { PasskeyLoginWidget as PasskeyLoginWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService, getCredential } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-passkey-login-widget', + templateUrl: './passkey-login.widget.html', + styleUrls: ['./passkey-login.widget.scss'], + host: { + 'data-widget': 'passkeyLogin', + '[attr.data-type]': 'config.render?.type', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class PasskeyLoginWidget { + @Input() formId!: string; + @Input() config!: PasskeyLoginWidgetConfig; + + get disabled() { + return !!this.widgetService.loading$.value; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + async onClick() { + if (this.disabled) { + return; + } + + try { + const response = await getCredential(this.config.assertionOptions); + this.widgetService.setFormValue(this.formId, this.config.id, response); + await this.widgetService.submitForm(this.formId); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + alert('Authentication failed. Please try again.'); + } + } + + async onKeyDown(event: KeyboardEvent) { + if (['Enter', 'Space'].includes(event.code)) { + await this.onClick(); + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/password/password.widget.html b/apps/angular-standalone/src/app/components/widgets/password/password.widget.html new file mode 100644 index 00000000..9464ff46 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/password/password.widget.html @@ -0,0 +1,7 @@ +@if (config.label) { + +} + +@if (errorMessage) { +{{ errorMessage }} +} diff --git a/apps/angular-standalone/src/app/components/widgets/password/password.widget.scss b/apps/angular-standalone/src/app/components/widgets/password/password.widget.scss new file mode 100644 index 00000000..43602428 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/password/password.widget.scss @@ -0,0 +1,44 @@ +[data-widget='password'] { + display: flex; + flex-direction: column; + margin-block-start: 1rem; + + .label { + margin-block-end: 0.25rem; + } + + input { + position: relative; + display: flex; + align-items: center; + height: 2.5rem; + padding-inline: 0.5rem; + font-size: 1rem; + background: hsl(0deg 0% 100%); + border-width: 1px; + border-style: solid; + border-color: hsl(0deg 0% 90%); + border-radius: 4px; + box-shadow: rgb(0 0 0 / 5%) 0 1px 2px 0; + transition: + color 150ms ease-in-out, + background 150ms ease-in-out, + border-color 150ms ease-in-out; + + &:focus { + outline: none; + border-color: #5d21ab; + } + + &:read-only { + background: hsl(0deg 0% 98%); + border-color: hsl(0deg 0% 90%); + cursor: not-allowed; + } + } + + .error { + margin-block-start: 0.25rem; + color: #e40c0c; + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/password/password.widget.ts b/apps/angular-standalone/src/app/components/widgets/password/password.widget.ts new file mode 100644 index 00000000..0869bfaf --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/password/password.widget.ts @@ -0,0 +1,53 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { PasswordWidget as PasswordWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-password-widget', + templateUrl: './password.widget.html', + styleUrls: ['./password.widget.scss'], + host: { + 'data-widget': 'password', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class PasswordWidget { + @Input() formId!: string; + @Input() config!: PasswordWidgetConfig; + + get disabled() { + return !!this.widgetService.loading$.value; + } + + get errorMessage() { + return this.widgetService.messages$.value[this.formId]?.[this.config.id]?.text; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + onChange(event: Event) { + if (this.disabled) { + return; + } + + this.widgetService.setFormValue(this.formId, this.config.id, (event.target as HTMLInputElement).value); + } + + async onKeyDown(event: KeyboardEvent) { + if (this.disabled) { + return; + } + + const input = event.target as HTMLInputElement; + + if (input.reportValidity()) { + this.widgetService.setFormValue(this.formId, this.config.id, (event.target as HTMLInputElement).value); + await this.widgetService.submitForm(this.formId); + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/phone/phone.widget.html b/apps/angular-standalone/src/app/components/widgets/phone/phone.widget.html new file mode 100644 index 00000000..7c629180 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/phone/phone.widget.html @@ -0,0 +1,19 @@ +@if (config.label) { + +} + +@if (errorMessage) { +{{ errorMessage }} +} diff --git a/apps/angular-standalone/src/app/components/widgets/phone/phone.widget.scss b/apps/angular-standalone/src/app/components/widgets/phone/phone.widget.scss new file mode 100644 index 00000000..42f79dba --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/phone/phone.widget.scss @@ -0,0 +1,44 @@ +[data-widget='phone'] { + display: flex; + flex-direction: column; + margin-block-start: 1rem; + + .label { + margin-block-end: 0.25rem; + } + + input { + position: relative; + display: flex; + align-items: center; + height: 2.5rem; + padding-inline: 0.5rem; + font-size: 1rem; + background: hsl(0deg 0% 100%); + border-width: 1px; + border-style: solid; + border-color: hsl(0deg 0% 90%); + border-radius: 4px; + box-shadow: rgb(0 0 0 / 5%) 0 1px 2px 0; + transition: + color 150ms ease-in-out, + background 150ms ease-in-out, + border-color 150ms ease-in-out; + + &:focus { + outline: none; + border-color: #5d21ab; + } + + &:read-only { + background: hsl(0deg 0% 98%); + border-color: hsl(0deg 0% 90%); + cursor: not-allowed; + } + } + + .error { + margin-block-start: 0.25rem; + color: #e40c0c; + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/phone/phone.widget.ts b/apps/angular-standalone/src/app/components/widgets/phone/phone.widget.ts new file mode 100644 index 00000000..996cdef3 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/phone/phone.widget.ts @@ -0,0 +1,69 @@ +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { PhoneWidget as PhoneWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-phone-widget', + templateUrl: './phone.widget.html', + styleUrls: ['./phone.widget.scss'], + host: { + 'data-widget': 'phone', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class PhoneWidget implements OnInit { + @Input() formId!: string; + @Input() config!: PhoneWidgetConfig; + + get disabled() { + return !!this.widgetService.loading$.value || !!this.config.readonly; + } + + get value() { + return this.widgetService.forms$.value[this.formId]?.[this.config.id] ?? this.config.value; + } + + get errorMessage() { + return this.widgetService.messages$.value[this.formId]?.[this.config.id]?.text; + } + + get validator() { + return this.config.validator; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + ngOnInit() { + // Default value handling + const value = this.value as string; + if (value && value.length > 0) { + this.widgetService.setFormValue(this.formId, this.config.id, value); + } + } + + onChange(event: Event) { + if (this.disabled) { + return; + } + + this.widgetService.setFormValue(this.formId, this.config.id, (event.target as HTMLInputElement).value); + } + + async onKeyDown(event: KeyboardEvent) { + if (this.disabled) { + return; + } + + const input = event.target as HTMLInputElement; + + if (input.reportValidity()) { + this.widgetService.setFormValue(this.formId, this.config.id, (event.target as HTMLInputElement).value); + await this.widgetService.submitForm(this.formId); + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/select/select.widget.html b/apps/angular-standalone/src/app/components/widgets/select/select.widget.html new file mode 100644 index 00000000..b6d5892a --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/select/select.widget.html @@ -0,0 +1,43 @@ +
+ @if (config.render?.type === 'radio') { @for (option of config.options; track option.value) { +
+ @if (option.type === 'group') { +

{{ option.label }}

+ @for (subOption of option.options; track subOption.value) { +
+ + +
+ } } @else { + + + } +
+ } @if (errorMessage) { + {{ errorMessage }} + } } @else { @if (config.label) { + + } + + @if (errorMessage) { + {{ errorMessage }} + } } +
diff --git a/apps/angular-standalone/src/app/components/widgets/select/select.widget.scss b/apps/angular-standalone/src/app/components/widgets/select/select.widget.scss new file mode 100644 index 00000000..8f65a1be --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/select/select.widget.scss @@ -0,0 +1,30 @@ +[data-widget='select'] { + display: flex; + flex-direction: column; + margin-block-start: 1rem; + + .label { + margin-block-end: 0.25rem; + } + + input { + transform: scale(1.4); + margin-inline-end: 0.5rem; + accent-color: #5d21ab; + outline-color: #5d21ab; + } + + .item { + display: flex; + align-items: center; + + + .item { + margin-block-start: 0.5rem; + } + } + + .error { + margin-block-start: 0.25rem; + color: #e40c0c; + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/select/select.widget.ts b/apps/angular-standalone/src/app/components/widgets/select/select.widget.ts new file mode 100644 index 00000000..a3e0f9bb --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/select/select.widget.ts @@ -0,0 +1,52 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { SelectWidget as SelectWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-select-widget', + templateUrl: './select.widget.html', + styleUrls: ['./select.widget.scss'], + host: { + 'data-widget': 'select', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class SelectWidget { + @Input() formId!: string; + @Input() config!: SelectWidgetConfig; + + get disabled() { + return !!this.widgetService.loading$.value || !!this.config.readonly; + } + + get value() { + return this.widgetService.forms$.value[this.formId]?.[this.config.id]; + } + + get errorMessage() { + return this.widgetService.messages$.value[this.formId]?.[this.config.id]?.text; + } + + get validator() { + return this.config.validator; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) { + if (this.config.value) { + this.widgetService.setFormValue(this.formId, this.config.id, this.config.value); + } + } + + onChange(event: Event) { + if (this.disabled) { + return; + } + + this.widgetService.setFormValue(this.formId, this.config.id, (event.target as HTMLInputElement).value); + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/static/static.widget.html b/apps/angular-standalone/src/app/components/widgets/static/static.widget.html new file mode 100644 index 00000000..ddcd7596 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/static/static.widget.html @@ -0,0 +1,5 @@ +@if (config.render?.type === 'html') { +
+} @if (config.render?.type !== 'html') { +
{{ config.value }}
+} diff --git a/apps/angular-standalone/src/app/components/widgets/static/static.widget.scss b/apps/angular-standalone/src/app/components/widgets/static/static.widget.scss new file mode 100644 index 00000000..d3de577b --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/static/static.widget.scss @@ -0,0 +1,18 @@ +// stylelint-disable selector-pseudo-element-no-unknown + +[data-widget='static'] { + text-align: center; + + ::ng-deep p { + margin: 0; + } + + &[data-widget-id='section-title'] { + font-size: 1.5rem; + font-weight: 600; + } + + &:not(:has(+ [data-widget='submit'])) { + margin-block-end: 0.5rem; + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/static/static.widget.ts b/apps/angular-standalone/src/app/components/widgets/static/static.widget.ts new file mode 100644 index 00000000..d4cb1235 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/static/static.widget.ts @@ -0,0 +1,21 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { StaticWidget as StaticWidgetConfig } from '@strivacity/sdk-core'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-static-widget', + templateUrl: './static.widget.html', + styleUrls: ['./static.widget.scss'], + host: { + 'data-widget': 'static', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class StaticWidget { + @Input() formId!: string; + @Input() config!: StaticWidgetConfig; +} diff --git a/apps/angular-standalone/src/app/components/widgets/submit/submit.widget.html b/apps/angular-standalone/src/app/components/widgets/submit/submit.widget.html new file mode 100644 index 00000000..3861e97f --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/submit/submit.widget.html @@ -0,0 +1,14 @@ +@if (config.render?.type === 'button') { + +} @else { +{{ config.label }} +} diff --git a/apps/angular-standalone/src/app/components/widgets/submit/submit.widget.scss b/apps/angular-standalone/src/app/components/widgets/submit/submit.widget.scss new file mode 100644 index 00000000..ba4b5bd4 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/submit/submit.widget.scss @@ -0,0 +1,54 @@ +[data-widget='submit'] { + display: block; + + button, + a { + display: block; + width: 100%; + cursor: pointer; + outline-color: #5d21ab; + } + + &[data-type='button'] { + button { + display: inline-flex; + align-items: center; + justify-content: center; + margin-block: 1rem; + min-height: 2.25rem; + padding: 0.5rem 1rem; + font-size: 1rem; + box-shadow: rgb(0 0 0 / 5%) 0 1px 2px 0; + border: 1px solid rgb(0 0 0 / 15%); + border-radius: 4px; + position: relative; + + &:disabled { + cursor: not-allowed; + background-color: #f5f5f5 !important; + color: #bdbdbd !important; + border-color: #e0e0e0; + box-shadow: none; + } + } + } + + &[data-type='link'] { + a { + display: inline-block; + text-align: center; + color: #5d21ab; + + &:hover { + color: oklch(from #5d21ab l calc(l * 0.85) c h); + } + } + } + + + [data-widget='submit'] { + button, + a { + margin-top: 1rem; + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/submit/submit.widget.ts b/apps/angular-standalone/src/app/components/widgets/submit/submit.widget.ts new file mode 100644 index 00000000..03e8e631 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/submit/submit.widget.ts @@ -0,0 +1,49 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { SubmitWidget as SubmitWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-submit-widget', + templateUrl: './submit.widget.html', + styleUrls: ['./submit.widget.scss'], + host: { + 'data-widget': 'submit', + '[attr.data-type]': 'config.render?.type', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class SubmitWidget { + @Input() formId!: string; + @Input() config!: SubmitWidgetConfig; + + get disabled() { + return !!this.widgetService.loading$.value; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + async onSubmit(event: Event) { + if (this.disabled) { + return; + } + + const form = (event.target as HTMLElement).closest('form'); + + if (form?.dataset.formId === this.formId) { + form.requestSubmit(); + } else { + await this.widgetService.submitForm(this.formId); + } + } + + async onKeyDown(event: KeyboardEvent) { + if (['Enter', 'Space'].includes(event.code)) { + await this.onSubmit(event); + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.html b/apps/angular-standalone/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.html new file mode 100644 index 00000000..99841c19 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.html @@ -0,0 +1,5 @@ +@if (config.render?.type === 'button') { + +} @else { +{{ config.label }} +} diff --git a/apps/angular-standalone/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.scss b/apps/angular-standalone/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.scss new file mode 100644 index 00000000..dceccbf5 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.scss @@ -0,0 +1,47 @@ +[data-widget='webauthnEnroll'] { + display: block; + + button, + a { + display: block; + width: 100%; + cursor: pointer; + outline-color: #5d21ab; + } + + &[data-type='button'] { + button { + display: inline-flex; + align-items: center; + justify-content: center; + margin-block: 1rem; + min-height: 2.25rem; + padding: 0.5rem 1rem; + font-size: 1rem; + box-shadow: rgb(0 0 0 / 5%) 0 1px 2px 0; + border: 1px solid rgb(0 0 0 / 15%); + border-radius: 4px; + position: relative; + + &:disabled { + cursor: not-allowed; + background-color: #f5f5f5 !important; + color: #bdbdbd !important; + border-color: #e0e0e0; + box-shadow: none; + } + } + } + + &[data-type='link'] { + a { + display: inline-block; + text-align: center; + color: #5d21ab; + + &:hover { + color: oklch(from #5d21ab l calc(l * 0.85) c h); + } + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.ts b/apps/angular-standalone/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.ts new file mode 100644 index 00000000..17b1d5d9 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.ts @@ -0,0 +1,51 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { PasskeyEnrollWidget as WebAuthnEnrollWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService, createCredential } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-webauthn-enroll-widget', + templateUrl: './webauthn-enroll.widget.html', + styleUrls: ['./webauthn-enroll.widget.scss'], + host: { + 'data-widget': 'webauthnEnroll', + '[attr.data-type]': 'config.render?.type', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class WebAuthnEnrollWidget { + @Input() formId!: string; + @Input() config!: WebAuthnEnrollWidgetConfig; + + get disabled() { + return !!this.widgetService.loading$.value; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + async onClick() { + if (this.disabled) { + return; + } + + try { + const response = await createCredential(this.config.enrollOptions); + this.widgetService.setFormValue(this.formId, this.config.id, response); + await this.widgetService.submitForm(this.formId); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + alert('Enrollment failed. Please try again.'); + } + } + + async onKeyDown(event: KeyboardEvent) { + if (['Enter', 'Space'].includes(event.code)) { + await this.onClick(); + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/webauthn-login/webauthn-login.widget.html b/apps/angular-standalone/src/app/components/widgets/webauthn-login/webauthn-login.widget.html new file mode 100644 index 00000000..99841c19 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/webauthn-login/webauthn-login.widget.html @@ -0,0 +1,5 @@ +@if (config.render?.type === 'button') { + +} @else { +{{ config.label }} +} diff --git a/apps/angular-standalone/src/app/components/widgets/webauthn-login/webauthn-login.widget.scss b/apps/angular-standalone/src/app/components/widgets/webauthn-login/webauthn-login.widget.scss new file mode 100644 index 00000000..1db137c3 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/webauthn-login/webauthn-login.widget.scss @@ -0,0 +1,47 @@ +[data-widget='webauthnLogin'] { + display: block; + + button, + a { + display: block; + width: 100%; + cursor: pointer; + outline-color: #5d21ab; + } + + &[data-type='button'] { + button { + display: inline-flex; + align-items: center; + justify-content: center; + margin-block: 1rem; + min-height: 2.25rem; + padding: 0.5rem 1rem; + font-size: 1rem; + box-shadow: rgb(0 0 0 / 5%) 0 1px 2px 0; + border: 1px solid rgb(0 0 0 / 15%); + border-radius: 4px; + position: relative; + + &:disabled { + cursor: not-allowed; + background-color: #f5f5f5 !important; + color: #bdbdbd !important; + border-color: #e0e0e0; + box-shadow: none; + } + } + } + + &[data-type='link'] { + a { + display: inline-block; + text-align: center; + color: #5d21ab; + + &:hover { + color: oklch(from #5d21ab l calc(l * 0.85) c h); + } + } + } +} diff --git a/apps/angular-standalone/src/app/components/widgets/webauthn-login/webauthn-login.widget.ts b/apps/angular-standalone/src/app/components/widgets/webauthn-login/webauthn-login.widget.ts new file mode 100644 index 00000000..92634320 --- /dev/null +++ b/apps/angular-standalone/src/app/components/widgets/webauthn-login/webauthn-login.widget.ts @@ -0,0 +1,51 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { PasskeyLoginWidget as WebAuthnLoginWidgetConfig } from '@strivacity/sdk-core'; +import { StrivacityWidgetService, getCredential } from '@strivacity/sdk-angular'; + +@Component({ + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [CommonModule], + selector: 'app-webauthn-login-widget', + templateUrl: './webauthn-login.widget.html', + styleUrls: ['./webauthn-login.widget.scss'], + host: { + 'data-widget': 'webauthnLogin', + '[attr.data-type]': 'config.render?.type', + '[attr.data-form-id]': 'formId', + '[attr.data-widget-id]': 'config.id', + }, +}) +export class WebAuthnLoginWidget { + @Input() formId!: string; + @Input() config!: WebAuthnLoginWidgetConfig; + + get disabled() { + return !!this.widgetService.loading$.value; + } + + constructor(protected readonly widgetService: StrivacityWidgetService) {} + + async onClick() { + if (this.disabled) { + return; + } + + try { + const response = await getCredential(this.config.assertionOptions); + this.widgetService.setFormValue(this.formId, this.config.id, response); + await this.widgetService.submitForm(this.formId); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + alert('Authentication failed. Please try again.'); + } + } + + async onKeyDown(event: KeyboardEvent) { + if (['Enter', 'Space'].includes(event.code)) { + await this.onClick(); + } + } +} diff --git a/apps/angular-standalone/src/app/pages/callback/callback.page.html b/apps/angular-standalone/src/app/pages/callback/callback.page.html new file mode 100644 index 00000000..fe666fa5 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/callback/callback.page.html @@ -0,0 +1,11 @@ +
+ @if (error) { +

Error in authentication

+
+

{{ error }}

+

{{ error_description }}

+
+ } @else { +

Logging in...

+ } +
diff --git a/apps/angular-standalone/src/app/pages/callback/callback.page.ts b/apps/angular-standalone/src/app/pages/callback/callback.page.ts new file mode 100644 index 00000000..6354ab4f --- /dev/null +++ b/apps/angular-standalone/src/app/pages/callback/callback.page.ts @@ -0,0 +1,48 @@ +import { Component, SkipSelf } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { type RedirectFlow, StrivacityAuthService } from '@strivacity/sdk-angular'; + +@Component({ + standalone: true, + selector: 'app-callback-page', + templateUrl: './callback.page.html', +}) +export class CallbackPage { + readonly subscription = new Subscription(); + error: string | null = null; + errorDescription: string | null = null; + + constructor( + protected route: ActivatedRoute, + protected router: Router, + @SkipSelf() protected strivacityAuthService: StrivacityAuthService, + ) {} + + ngOnInit(): void { + const url = new URL(location.href); + + if (url.searchParams.has('session_id')) { + void this.router.navigate(['/login'], { queryParams: url.searchParams }); + return; + } + + this.subscription.add( + this.strivacityAuthService.handleCallback().subscribe({ + next: () => { + void this.router.navigateByUrl('/profile'); + }, + error: (err) => { + this.error = this.route.snapshot.queryParamMap.get('error'); + this.errorDescription = this.route.snapshot.queryParamMap.get('error_description'); + // eslint-disable-next-line no-console + console.error('Error during callback handling:', err); + }, + }), + ); + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } +} diff --git a/apps/angular-standalone/src/app/pages/callback/index.ts b/apps/angular-standalone/src/app/pages/callback/index.ts new file mode 100644 index 00000000..5a13b27f --- /dev/null +++ b/apps/angular-standalone/src/app/pages/callback/index.ts @@ -0,0 +1 @@ +export { CallbackPage } from './callback.page'; diff --git a/apps/angular-standalone/src/app/pages/entry/entry.page.html b/apps/angular-standalone/src/app/pages/entry/entry.page.html new file mode 100644 index 00000000..5eb5ad42 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/entry/entry.page.html @@ -0,0 +1,3 @@ +
+

Redirecting...

+
diff --git a/apps/angular-standalone/src/app/pages/entry/entry.page.ts b/apps/angular-standalone/src/app/pages/entry/entry.page.ts new file mode 100644 index 00000000..fbfb0887 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/entry/entry.page.ts @@ -0,0 +1,31 @@ +import { Component, SkipSelf } from '@angular/core'; +import { Router } from '@angular/router'; +import { StrivacityAuthService } from '@strivacity/sdk-angular'; + +@Component({ + standalone: true, + selector: 'app-entry-page', + templateUrl: './entry.page.html', +}) +export class EntryPage { + constructor( + protected router: Router, + @SkipSelf() protected strivacityAuthService: StrivacityAuthService, + ) {} + + ngOnInit() { + this.strivacityAuthService.entry().subscribe({ + next: (data) => { + if (data && Object.keys(data).length > 0) { + void this.router.navigate([`/callback`], { queryParams: new URLSearchParams(data) }); + } else { + void this.router.navigate(['/']); + } + }, + error: (error) => { + alert(error); + void this.router.navigate(['/']); + }, + }); + } +} diff --git a/apps/angular-standalone/src/app/pages/entry/index.ts b/apps/angular-standalone/src/app/pages/entry/index.ts new file mode 100644 index 00000000..b74b6ad3 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/entry/index.ts @@ -0,0 +1 @@ +export { EntryPage } from './entry.page'; diff --git a/apps/angular-standalone/src/app/pages/home/home.page.html b/apps/angular-standalone/src/app/pages/home/home.page.html new file mode 100644 index 00000000..87035fec --- /dev/null +++ b/apps/angular-standalone/src/app/pages/home/home.page.html @@ -0,0 +1,3 @@ +
+

Framework: Angular

+
diff --git a/apps/angular-standalone/src/app/pages/home/home.page.ts b/apps/angular-standalone/src/app/pages/home/home.page.ts new file mode 100644 index 00000000..384a75d6 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/home/home.page.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'app-home-page', + templateUrl: './home.page.html', +}) +export class HomePage {} diff --git a/apps/angular-standalone/src/app/pages/home/index.ts b/apps/angular-standalone/src/app/pages/home/index.ts new file mode 100644 index 00000000..6eca12c3 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/home/index.ts @@ -0,0 +1 @@ +export { HomePage } from './home.page'; diff --git a/apps/angular-standalone/src/app/pages/login/index.ts b/apps/angular-standalone/src/app/pages/login/index.ts new file mode 100644 index 00000000..5a1d6cf9 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/login/index.ts @@ -0,0 +1 @@ +export { LoginPage } from './login.page'; diff --git a/apps/angular-standalone/src/app/pages/login/login.page.html b/apps/angular-standalone/src/app/pages/login/login.page.html new file mode 100644 index 00000000..d4803e42 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/login/login.page.html @@ -0,0 +1,33 @@ +
+ @if (options?.mode === 'redirect') { +

Redirecting...

+ } @else if (options?.mode === 'popup') { +

Loading...

+ } @else if (options?.mode === 'embedded') { + + + + } @else if (options?.mode === 'native') { + + } +
diff --git a/apps/angular-standalone/src/app/pages/login/login.page.ts b/apps/angular-standalone/src/app/pages/login/login.page.ts new file mode 100644 index 00000000..cbb3c561 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/login/login.page.ts @@ -0,0 +1,105 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Component, OnDestroy, OnInit, SkipSelf } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { SDKOptions, StrivacityAuthService, FallbackError, StyLoginRenderer, LoginFlowState, type ExtraRequestArgs } from '@strivacity/sdk-angular'; +import { type ImportMeta } from '../../app.config'; +import { widgets } from '../../components/widgets'; + +@Component({ + standalone: true, + imports: [StyLoginRenderer], + selector: 'app-login-page', + templateUrl: './login.page.html', +}) +export class LoginPage implements OnInit, OnDestroy { + readonly widgets = widgets; + readonly subscription = new Subscription(); + shortAppId: string | null = null; + sessionId: string | null = null; + language: string | null = null; + options: SDKOptions; + extraParams: ExtraRequestArgs = { + loginHint: (import.meta as unknown as ImportMeta).env.VITE_LOGIN_HINT, + acrValues: (import.meta as unknown as ImportMeta).env.VITE_ACR_VALUES ? (import.meta as unknown as ImportMeta).env.VITE_ACR_VALUES.split(' ') : undefined, + uiLocales: (import.meta as unknown as ImportMeta).env.VITE_UI_LOCALES ? (import.meta as unknown as ImportMeta).env.VITE_UI_LOCALES.split(' ') : undefined, + audiences: (import.meta as unknown as ImportMeta).env.VITE_AUDIENCES ? (import.meta as unknown as ImportMeta).env.VITE_AUDIENCES.split(' ') : undefined, + }; + + constructor( + protected router: Router, + @SkipSelf() protected strivacityAuthService: StrivacityAuthService, + ) { + this.options = this.strivacityAuthService.options; + + if (window.location.search !== '') { + const url = new URL(window.location.href); + this.shortAppId = url.searchParams.get('short_app_id'); + this.sessionId = url.searchParams.get('session_id'); + + if (url.searchParams.has('language')) { + this.language = url.searchParams.get('language'); + } + + url.search = ''; + history.replaceState({}, '', url.toString()); + } + } + + ngOnInit() { + if (this.options?.mode === 'redirect') { + this.subscription.add( + this.strivacityAuthService.login(this.extraParams).subscribe({ + next: () => {}, + error: (error: any) => this.onError(error), + }), + ); + } else if (this.options?.mode === 'popup') { + this.subscription.add( + this.strivacityAuthService.login(this.extraParams).subscribe({ + next: () => { + void this.router.navigateByUrl('/profile'); + }, + error: (error: any) => this.onError(error), + }), + ); + } + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + onLogin() { + void this.router.navigateByUrl('/profile'); + } + + onFallback(error: FallbackError) { + if (error.url) { + location.href = error.url.toString(); + } else { + alert(error); + } + } + + onClose() { + location.reload(); + } + + onError(error: any) { + alert(error); + } + + onGlobalMessage(message: string) { + alert(message); + } + + onBlockReady(_events: { previousState: LoginFlowState; state: LoginFlowState }) { + // You can handle block ready events here + } + + onLanguageChange(language: string | null) { + this.language = language; + } +} diff --git a/apps/angular-standalone/src/app/pages/logout/index.ts b/apps/angular-standalone/src/app/pages/logout/index.ts new file mode 100644 index 00000000..e50edf12 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/logout/index.ts @@ -0,0 +1 @@ +export { LogoutPage } from './logout.page'; diff --git a/apps/angular-standalone/src/app/pages/logout/logout.page.html b/apps/angular-standalone/src/app/pages/logout/logout.page.html new file mode 100644 index 00000000..85f800af --- /dev/null +++ b/apps/angular-standalone/src/app/pages/logout/logout.page.html @@ -0,0 +1,3 @@ +
+

Logging out...

+
diff --git a/apps/angular-standalone/src/app/pages/logout/logout.page.ts b/apps/angular-standalone/src/app/pages/logout/logout.page.ts new file mode 100644 index 00000000..3c92e3ea --- /dev/null +++ b/apps/angular-standalone/src/app/pages/logout/logout.page.ts @@ -0,0 +1,30 @@ +import { Component, SkipSelf } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subscription, firstValueFrom } from 'rxjs'; +import { StrivacityAuthService } from '@strivacity/sdk-angular'; + +@Component({ + standalone: true, + selector: 'app-logout-page', + templateUrl: './logout.page.html', +}) +export class LogoutPage { + readonly subscription = new Subscription(); + + constructor( + protected router: Router, + @SkipSelf() protected strivacityAuthService: StrivacityAuthService, + ) {} + + async ngOnInit(): Promise { + if (this.strivacityAuthService.isAuthenticated()) { + await firstValueFrom(this.strivacityAuthService.logout({ postLogoutRedirectUri: window.location.origin })); + } else { + void this.router.navigateByUrl('/'); + } + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } +} diff --git a/apps/angular-standalone/src/app/pages/profile/index.ts b/apps/angular-standalone/src/app/pages/profile/index.ts new file mode 100644 index 00000000..2ff27357 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/profile/index.ts @@ -0,0 +1 @@ +export { ProfilePage } from './profile.page'; diff --git a/apps/angular-standalone/src/app/pages/profile/profile.page.html b/apps/angular-standalone/src/app/pages/profile/profile.page.html new file mode 100644 index 00000000..20ade109 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/profile/profile.page.html @@ -0,0 +1,34 @@ +
+
+
+ accessToken +
+
+
{{ session.accessToken | json }}
+
+
+ refreshToken +
+
+
{{ session.refreshToken | json }}
+
+
+ accessTokenExpired +
+
+
{{ session.accessTokenExpired | json }}
+
+
+ accessTokenExpirationDate +
+
+
{{ session.accessTokenExpirationDate | date: 'medium' }}
+
+
+ claims +
+
+
{{ session.idTokenClaims | json }}
+
+
+
diff --git a/apps/angular-standalone/src/app/pages/profile/profile.page.ts b/apps/angular-standalone/src/app/pages/profile/profile.page.ts new file mode 100644 index 00000000..9867f0ad --- /dev/null +++ b/apps/angular-standalone/src/app/pages/profile/profile.page.ts @@ -0,0 +1,33 @@ +import { Component, SkipSelf } from '@angular/core'; +import { DatePipe, JsonPipe } from '@angular/common'; +import { Subscription } from 'rxjs'; +import { Session, StrivacityAuthService } from '@strivacity/sdk-angular'; + +@Component({ + standalone: true, + selector: 'app-profile-page', + templateUrl: './profile.page.html', + imports: [JsonPipe, DatePipe], +}) +export class ProfilePage { + readonly subscription = new Subscription(); + session: Session = { + loading: true, + isAuthenticated: false, + idTokenClaims: null, + accessToken: null, + refreshToken: null, + accessTokenExpired: false, + accessTokenExpirationDate: null, + }; + + constructor(@SkipSelf() protected strivacityAuthService: StrivacityAuthService) { + this.strivacityAuthService.session$.subscribe((session) => { + this.session = session; + }); + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } +} diff --git a/apps/angular-standalone/src/app/pages/register/index.ts b/apps/angular-standalone/src/app/pages/register/index.ts new file mode 100644 index 00000000..e11c7357 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/register/index.ts @@ -0,0 +1 @@ +export { RegisterPage } from './register.page'; diff --git a/apps/angular-standalone/src/app/pages/register/register.page.html b/apps/angular-standalone/src/app/pages/register/register.page.html new file mode 100644 index 00000000..d4803e42 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/register/register.page.html @@ -0,0 +1,33 @@ +
+ @if (options?.mode === 'redirect') { +

Redirecting...

+ } @else if (options?.mode === 'popup') { +

Loading...

+ } @else if (options?.mode === 'embedded') { + + + + } @else if (options?.mode === 'native') { + + } +
diff --git a/apps/angular-standalone/src/app/pages/register/register.page.ts b/apps/angular-standalone/src/app/pages/register/register.page.ts new file mode 100644 index 00000000..94c746be --- /dev/null +++ b/apps/angular-standalone/src/app/pages/register/register.page.ts @@ -0,0 +1,106 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Component, OnDestroy, OnInit, SkipSelf } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { SDKOptions, StrivacityAuthService, FallbackError, StyLoginRenderer, LoginFlowState, type ExtraRequestArgs } from '@strivacity/sdk-angular'; +import { type ImportMeta } from '../../app.config'; +import { widgets } from '../../components/widgets'; + +@Component({ + standalone: true, + imports: [StyLoginRenderer], + selector: 'app-register-page', + templateUrl: './register.page.html', +}) +export class RegisterPage implements OnInit, OnDestroy { + readonly widgets = widgets; + readonly subscription = new Subscription(); + shortAppId: string | null = null; + sessionId: string | null = null; + language: string | null = null; + options: SDKOptions; + extraParams: ExtraRequestArgs = { + prompt: 'create', + loginHint: (import.meta as unknown as ImportMeta).env.VITE_LOGIN_HINT, + acrValues: (import.meta as unknown as ImportMeta).env.VITE_ACR_VALUES ? (import.meta as unknown as ImportMeta).env.VITE_ACR_VALUES.split(' ') : undefined, + uiLocales: (import.meta as unknown as ImportMeta).env.VITE_UI_LOCALES ? (import.meta as unknown as ImportMeta).env.VITE_UI_LOCALES.split(' ') : undefined, + audiences: (import.meta as unknown as ImportMeta).env.VITE_AUDIENCES ? (import.meta as unknown as ImportMeta).env.VITE_AUDIENCES.split(' ') : undefined, + }; + + constructor( + protected router: Router, + @SkipSelf() protected strivacityAuthService: StrivacityAuthService, + ) { + this.options = this.strivacityAuthService.options; + + if (window.location.search !== '') { + const url = new URL(window.location.href); + this.shortAppId = url.searchParams.get('short_app_id'); + this.sessionId = url.searchParams.get('session_id'); + + if (url.searchParams.has('language')) { + this.language = url.searchParams.get('language'); + } + + url.search = ''; + history.replaceState({}, '', url.toString()); + } + } + + ngOnInit() { + if (this.options?.mode === 'redirect') { + this.subscription.add( + this.strivacityAuthService.register(this.extraParams).subscribe({ + next: () => {}, + error: (error: any) => this.onError(error), + }), + ); + } else if (this.options?.mode === 'popup') { + this.subscription.add( + this.strivacityAuthService.register(this.extraParams).subscribe({ + next: () => { + void this.router.navigateByUrl('/profile'); + }, + error: (error: any) => this.onError(error), + }), + ); + } + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + onLogin() { + void this.router.navigateByUrl('/profile'); + } + + onFallback(error: FallbackError) { + if (error.url) { + location.href = error.url.toString(); + } else { + alert(error); + } + } + + onClose() { + location.reload(); + } + + onError(error: any) { + alert(error); + } + + onGlobalMessage(message: string) { + alert(message); + } + + onBlockReady(_events: { previousState: LoginFlowState; state: LoginFlowState }) { + // You can handle block ready events here + } + + onLanguageChange(language: string | null) { + this.language = language; + } +} diff --git a/apps/angular-standalone/src/app/pages/revoke/index.ts b/apps/angular-standalone/src/app/pages/revoke/index.ts new file mode 100644 index 00000000..d7af5677 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/revoke/index.ts @@ -0,0 +1 @@ +export { RevokePage } from './revoke.page'; diff --git a/apps/angular-standalone/src/app/pages/revoke/revoke.page.html b/apps/angular-standalone/src/app/pages/revoke/revoke.page.html new file mode 100644 index 00000000..85f800af --- /dev/null +++ b/apps/angular-standalone/src/app/pages/revoke/revoke.page.html @@ -0,0 +1,3 @@ +
+

Logging out...

+
diff --git a/apps/angular-standalone/src/app/pages/revoke/revoke.page.ts b/apps/angular-standalone/src/app/pages/revoke/revoke.page.ts new file mode 100644 index 00000000..74fa9455 --- /dev/null +++ b/apps/angular-standalone/src/app/pages/revoke/revoke.page.ts @@ -0,0 +1,30 @@ +import { Component, SkipSelf } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { type RedirectFlow, StrivacityAuthService } from '@strivacity/sdk-angular'; + +@Component({ + standalone: true, + selector: 'app-revoke-page', + templateUrl: './revoke.page.html', +}) +export class RevokePage { + readonly subscription = new Subscription(); + + constructor( + protected router: Router, + @SkipSelf() protected strivacityAuthService: StrivacityAuthService, + ) {} + + ngOnInit(): void { + this.strivacityAuthService.revoke().subscribe({ + next: () => { + void this.router.navigateByUrl('/'); + }, + }); + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } +} diff --git a/apps/angular-standalone/src/index.html b/apps/angular-standalone/src/index.html new file mode 100644 index 00000000..fd203266 --- /dev/null +++ b/apps/angular-standalone/src/index.html @@ -0,0 +1,13 @@ + + + + + AngularAppTest + + + + + + + + diff --git a/apps/angular-standalone/src/main.ts b/apps/angular-standalone/src/main.ts new file mode 100644 index 00000000..ba1e35b3 --- /dev/null +++ b/apps/angular-standalone/src/main.ts @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig) + // eslint-disable-next-line no-console + .catch((err) => console.error(err)); diff --git a/apps/angular-standalone/tsconfig.app.json b/apps/angular-standalone/tsconfig.app.json new file mode 100644 index 00000000..bac1e022 --- /dev/null +++ b/apps/angular-standalone/tsconfig.app.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app" + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/apps/angular-standalone/tsconfig.json b/apps/angular-standalone/tsconfig.json new file mode 100644 index 00000000..7ee11ac7 --- /dev/null +++ b/apps/angular-standalone/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": false + } +} diff --git a/apps/angular-standalone/webpack.config.cjs b/apps/angular-standalone/webpack.config.cjs new file mode 100644 index 00000000..2e68d82a --- /dev/null +++ b/apps/angular-standalone/webpack.config.cjs @@ -0,0 +1,21 @@ +const { DefinePlugin } = require('webpack'); + +module.exports = { + plugins: [ + new DefinePlugin({ + 'import.meta.env.VITE_MODE': JSON.stringify(process.env.VITE_MODE), + 'import.meta.env.VITE_ISSUER': JSON.stringify(process.env.VITE_ISSUER), + 'import.meta.env.VITE_REDIRECT_URI': JSON.stringify(process.env.VITE_REDIRECT_URI), + 'import.meta.env.VITE_CLIENT_ID': JSON.stringify(process.env.VITE_CLIENT_ID), + 'import.meta.env.VITE_SCOPES': JSON.stringify(process.env.VITE_SCOPES), + 'import.meta.env.VITE_LOGIN_HINT': JSON.stringify(process.env.VITE_LOGIN_HINT), + 'import.meta.env.VITE_ACR_VALUES': JSON.stringify(process.env.VITE_ACR_VALUES), + 'import.meta.env.VITE_UI_LOCALES': JSON.stringify(process.env.VITE_UI_LOCALES), + 'import.meta.env.VITE_AUDIENCES': JSON.stringify(process.env.VITE_AUDIENCES), + }), + ], + devServer: { + port: 4200, + host: 'localhost', + }, +}; diff --git a/apps/angular/src/app/app.component.ts b/apps/angular/src/app/app.component.ts index 16ea1660..0d5e9545 100644 --- a/apps/angular/src/app/app.component.ts +++ b/apps/angular/src/app/app.component.ts @@ -1,14 +1,12 @@ import { Component } from '@angular/core'; -import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { StrivacityAuthService } from '@strivacity/sdk-angular'; @Component({ - standalone: true, + standalone: false, selector: 'app-root', templateUrl: './app.component.html', styleUrl: './app.component.scss', providers: [StrivacityAuthService], - imports: [RouterOutlet, RouterLink, RouterLinkActive], }) export class AppComponent { loading = true; diff --git a/apps/angular/src/app/app.config.ts b/apps/angular/src/app/app.config.ts index 3414535f..9746f06a 100644 --- a/apps/angular/src/app/app.config.ts +++ b/apps/angular/src/app/app.config.ts @@ -1,8 +1,3 @@ -import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; -import { provideRouter } from '@angular/router'; -import { provideStrivacity, DefaultLogging } from '@strivacity/sdk-angular'; -import { routes } from './app.routes'; - export interface ImportMeta { env: { VITE_MODE: 'redirect' | 'popup' | 'native'; @@ -16,19 +11,3 @@ export interface ImportMeta { VITE_AUDIENCES: string; }; } - -export const appConfig: ApplicationConfig = { - providers: [ - provideZoneChangeDetection({ eventCoalescing: true }), - provideRouter(routes), - provideStrivacity({ - mode: (import.meta as unknown as ImportMeta).env.VITE_MODE, - issuer: (import.meta as unknown as ImportMeta).env.VITE_ISSUER, - scopes: (import.meta as unknown as ImportMeta).env.VITE_SCOPES.split(' '), - clientId: (import.meta as unknown as ImportMeta).env.VITE_CLIENT_ID, - redirectUri: (import.meta as unknown as ImportMeta).env.VITE_REDIRECT_URI, - storageTokenName: 'sty.session.angular', - logging: DefaultLogging, - }), - ], -}; diff --git a/apps/angular/src/app/app.module.ts b/apps/angular/src/app/app.module.ts new file mode 100644 index 00000000..aa72a54a --- /dev/null +++ b/apps/angular/src/app/app.module.ts @@ -0,0 +1,83 @@ +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { RouterModule } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { StrivacityAuthModule, DefaultLogging } from '@strivacity/sdk-angular'; +import { routes } from './app.routes'; +import { type ImportMeta } from './app.config'; + +import { AppComponent } from './app.component'; +import { CallbackPage } from './pages/callback'; +import { HomePage } from './pages/home'; +import { LoginPage } from './pages/login'; +import { LogoutPage } from './pages/logout'; +import { ProfilePage } from './pages/profile'; +import { RegisterPage } from './pages/register'; +import { RevokePage } from './pages/revoke'; +import { EntryPage } from './pages/entry'; + +import { CheckboxWidget } from './components/widgets/checkbox/checkbox.widget'; +import { CloseWidget } from './components/widgets/close/close.widget'; +import { DateWidget } from './components/widgets/date/date.widget'; +import { InputWidget } from './components/widgets/input/input.widget'; +import { LayoutWidget } from './components/widgets/layout/layout.widget'; +import { LoadingWidget } from './components/widgets/loading/loading.widget'; +import { MultiSelectWidget } from './components/widgets/multiselect/multiselect.widget'; +import { PasscodeWidget } from './components/widgets/passcode/passcode.widget'; +import { PasskeyEnrollWidget } from './components/widgets/passkey-enroll/passkey-enroll.widget'; +import { PasskeyLoginWidget } from './components/widgets/passkey-login/passkey-login.widget'; +import { PasswordWidget } from './components/widgets/password/password.widget'; +import { PhoneWidget } from './components/widgets/phone/phone.widget'; +import { SelectWidget } from './components/widgets/select/select.widget'; +import { StaticWidget } from './components/widgets/static/static.widget'; +import { SubmitWidget } from './components/widgets/submit/submit.widget'; +import { WebAuthnEnrollWidget } from './components/widgets/webauthn-enroll/webauthn-enroll.widget'; +import { WebAuthnLoginWidget } from './components/widgets/webauthn-login/webauthn-login.widget'; + +@NgModule({ + declarations: [ + AppComponent, + CallbackPage, + HomePage, + LoginPage, + LogoutPage, + ProfilePage, + RegisterPage, + RevokePage, + EntryPage, + CheckboxWidget, + CloseWidget, + DateWidget, + InputWidget, + LayoutWidget, + LoadingWidget, + MultiSelectWidget, + PasscodeWidget, + PasskeyEnrollWidget, + PasskeyLoginWidget, + PasswordWidget, + PhoneWidget, + SelectWidget, + StaticWidget, + SubmitWidget, + WebAuthnEnrollWidget, + WebAuthnLoginWidget, + ], + imports: [ + BrowserModule, + CommonModule, + RouterModule.forRoot(routes), + StrivacityAuthModule.forRoot({ + mode: (import.meta as unknown as ImportMeta).env.VITE_MODE, + issuer: (import.meta as unknown as ImportMeta).env.VITE_ISSUER, + scopes: (import.meta as unknown as ImportMeta).env.VITE_SCOPES.split(' '), + clientId: (import.meta as unknown as ImportMeta).env.VITE_CLIENT_ID, + redirectUri: (import.meta as unknown as ImportMeta).env.VITE_REDIRECT_URI, + storageTokenName: 'sty.session.angular', + logging: DefaultLogging, + }), + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + bootstrap: [AppComponent], +}) +export class AppModule {} diff --git a/apps/angular/src/app/components/widgets/checkbox/checkbox.widget.ts b/apps/angular/src/app/components/widgets/checkbox/checkbox.widget.ts index a6da493e..c82e6428 100644 --- a/apps/angular/src/app/components/widgets/checkbox/checkbox.widget.ts +++ b/apps/angular/src/app/components/widgets/checkbox/checkbox.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { CheckboxWidget as CheckboxWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-checkbox-widget', templateUrl: './checkbox.widget.html', styleUrls: ['./checkbox.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/close/close.widget.ts b/apps/angular/src/app/components/widgets/close/close.widget.ts index dfc7846f..5afba810 100644 --- a/apps/angular/src/app/components/widgets/close/close.widget.ts +++ b/apps/angular/src/app/components/widgets/close/close.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { CloseWidget as CloseWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-close-widget', templateUrl: './close.widget.html', styleUrls: ['./close.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/date/date.widget.ts b/apps/angular/src/app/components/widgets/date/date.widget.ts index d5e6c495..f9ce2f74 100644 --- a/apps/angular/src/app/components/widgets/date/date.widget.ts +++ b/apps/angular/src/app/components/widgets/date/date.widget.ts @@ -1,13 +1,11 @@ import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { DateWidget as DateWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService } from '@strivacity/sdk-angular'; import { DateTime } from 'luxon'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-date-widget', templateUrl: './date.widget.html', styleUrls: ['./date.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/input/input.widget.ts b/apps/angular/src/app/components/widgets/input/input.widget.ts index 5e97a9b6..1bb27a58 100644 --- a/apps/angular/src/app/components/widgets/input/input.widget.ts +++ b/apps/angular/src/app/components/widgets/input/input.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { InputWidget as InputWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-input-widget', templateUrl: './input.widget.html', styleUrls: ['./input.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/layout/layout.widget.ts b/apps/angular/src/app/components/widgets/layout/layout.widget.ts index 7c818aba..65b2ff23 100644 --- a/apps/angular/src/app/components/widgets/layout/layout.widget.ts +++ b/apps/angular/src/app/components/widgets/layout/layout.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { LayoutWidget as LayoutWidgetType } from '@strivacity/sdk-core'; import { StrivacityWidgetService } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-layout-widget', templateUrl: './layout.widget.html', styleUrls: ['./layout.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/loading/loading.widget.ts b/apps/angular/src/app/components/widgets/loading/loading.widget.ts index f85abb94..845b39d1 100644 --- a/apps/angular/src/app/components/widgets/loading/loading.widget.ts +++ b/apps/angular/src/app/components/widgets/loading/loading.widget.ts @@ -1,10 +1,8 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-loading-widget', templateUrl: './loading.widget.html', styleUrls: ['./loading.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/multiselect/multiselect.widget.ts b/apps/angular/src/app/components/widgets/multiselect/multiselect.widget.ts index d361365d..ebec0d26 100644 --- a/apps/angular/src/app/components/widgets/multiselect/multiselect.widget.ts +++ b/apps/angular/src/app/components/widgets/multiselect/multiselect.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { MultiSelectWidget as MultiSelectWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-multiselect-widget', templateUrl: './multiselect.widget.html', styleUrls: ['./multiselect.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/passcode/passcode.widget.ts b/apps/angular/src/app/components/widgets/passcode/passcode.widget.ts index 54e03280..bcdc2630 100644 --- a/apps/angular/src/app/components/widgets/passcode/passcode.widget.ts +++ b/apps/angular/src/app/components/widgets/passcode/passcode.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { PasscodeWidget as PasscodeWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-passcode-widget', templateUrl: './passcode.widget.html', styleUrls: ['./passcode.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.ts b/apps/angular/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.ts index 1a916f5b..79db811c 100644 --- a/apps/angular/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.ts +++ b/apps/angular/src/app/components/widgets/passkey-enroll/passkey-enroll.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { PasskeyEnrollWidget as PasskeyEnrollWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService, createCredential } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-passkey-enroll-widget', templateUrl: './passkey-enroll.widget.html', styleUrls: ['./passkey-enroll.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/passkey-login/passkey-login.widget.ts b/apps/angular/src/app/components/widgets/passkey-login/passkey-login.widget.ts index d94dfd52..c7500557 100644 --- a/apps/angular/src/app/components/widgets/passkey-login/passkey-login.widget.ts +++ b/apps/angular/src/app/components/widgets/passkey-login/passkey-login.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { PasskeyLoginWidget as PasskeyLoginWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService, getCredential } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-passkey-login-widget', templateUrl: './passkey-login.widget.html', styleUrls: ['./passkey-login.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/password/password.widget.ts b/apps/angular/src/app/components/widgets/password/password.widget.ts index 0869bfaf..ddb34f58 100644 --- a/apps/angular/src/app/components/widgets/password/password.widget.ts +++ b/apps/angular/src/app/components/widgets/password/password.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { PasswordWidget as PasswordWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-password-widget', templateUrl: './password.widget.html', styleUrls: ['./password.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/phone/phone.widget.ts b/apps/angular/src/app/components/widgets/phone/phone.widget.ts index 996cdef3..9e02ee00 100644 --- a/apps/angular/src/app/components/widgets/phone/phone.widget.ts +++ b/apps/angular/src/app/components/widgets/phone/phone.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { PhoneWidget as PhoneWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-phone-widget', templateUrl: './phone.widget.html', styleUrls: ['./phone.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/select/select.widget.ts b/apps/angular/src/app/components/widgets/select/select.widget.ts index a3e0f9bb..249c9955 100644 --- a/apps/angular/src/app/components/widgets/select/select.widget.ts +++ b/apps/angular/src/app/components/widgets/select/select.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { SelectWidget as SelectWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-select-widget', templateUrl: './select.widget.html', styleUrls: ['./select.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/static/static.widget.ts b/apps/angular/src/app/components/widgets/static/static.widget.ts index d4cb1235..abe6c72e 100644 --- a/apps/angular/src/app/components/widgets/static/static.widget.ts +++ b/apps/angular/src/app/components/widgets/static/static.widget.ts @@ -1,11 +1,9 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { StaticWidget as StaticWidgetConfig } from '@strivacity/sdk-core'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-static-widget', templateUrl: './static.widget.html', styleUrls: ['./static.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/submit/submit.widget.ts b/apps/angular/src/app/components/widgets/submit/submit.widget.ts index 03e8e631..e5b9c7c3 100644 --- a/apps/angular/src/app/components/widgets/submit/submit.widget.ts +++ b/apps/angular/src/app/components/widgets/submit/submit.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { SubmitWidget as SubmitWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-submit-widget', templateUrl: './submit.widget.html', styleUrls: ['./submit.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.ts b/apps/angular/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.ts index 17b1d5d9..21381728 100644 --- a/apps/angular/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.ts +++ b/apps/angular/src/app/components/widgets/webauthn-enroll/webauthn-enroll.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { PasskeyEnrollWidget as WebAuthnEnrollWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService, createCredential } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-webauthn-enroll-widget', templateUrl: './webauthn-enroll.widget.html', styleUrls: ['./webauthn-enroll.widget.scss'], diff --git a/apps/angular/src/app/components/widgets/webauthn-login/webauthn-login.widget.ts b/apps/angular/src/app/components/widgets/webauthn-login/webauthn-login.widget.ts index 92634320..f8ceacd1 100644 --- a/apps/angular/src/app/components/widgets/webauthn-login/webauthn-login.widget.ts +++ b/apps/angular/src/app/components/widgets/webauthn-login/webauthn-login.widget.ts @@ -1,12 +1,10 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; import type { PasskeyLoginWidget as WebAuthnLoginWidgetConfig } from '@strivacity/sdk-core'; import { StrivacityWidgetService, getCredential } from '@strivacity/sdk-angular'; @Component({ + standalone: false, encapsulation: ViewEncapsulation.None, - standalone: true, - imports: [CommonModule], selector: 'app-webauthn-login-widget', templateUrl: './webauthn-login.widget.html', styleUrls: ['./webauthn-login.widget.scss'], diff --git a/apps/angular/src/app/pages/callback/callback.page.ts b/apps/angular/src/app/pages/callback/callback.page.ts index 6354ab4f..9ac2e10f 100644 --- a/apps/angular/src/app/pages/callback/callback.page.ts +++ b/apps/angular/src/app/pages/callback/callback.page.ts @@ -4,7 +4,7 @@ import { Subscription } from 'rxjs'; import { type RedirectFlow, StrivacityAuthService } from '@strivacity/sdk-angular'; @Component({ - standalone: true, + standalone: false, selector: 'app-callback-page', templateUrl: './callback.page.html', }) diff --git a/apps/angular/src/app/pages/entry/entry.page.ts b/apps/angular/src/app/pages/entry/entry.page.ts index fbfb0887..ecc7c4a0 100644 --- a/apps/angular/src/app/pages/entry/entry.page.ts +++ b/apps/angular/src/app/pages/entry/entry.page.ts @@ -3,7 +3,7 @@ import { Router } from '@angular/router'; import { StrivacityAuthService } from '@strivacity/sdk-angular'; @Component({ - standalone: true, + standalone: false, selector: 'app-entry-page', templateUrl: './entry.page.html', }) diff --git a/apps/angular/src/app/pages/home/home.page.ts b/apps/angular/src/app/pages/home/home.page.ts index 384a75d6..62e210e2 100644 --- a/apps/angular/src/app/pages/home/home.page.ts +++ b/apps/angular/src/app/pages/home/home.page.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; @Component({ - standalone: true, + standalone: false, selector: 'app-home-page', templateUrl: './home.page.html', }) diff --git a/apps/angular/src/app/pages/login/login.page.ts b/apps/angular/src/app/pages/login/login.page.ts index cbb3c561..8b33ceda 100644 --- a/apps/angular/src/app/pages/login/login.page.ts +++ b/apps/angular/src/app/pages/login/login.page.ts @@ -3,13 +3,12 @@ import { Component, OnDestroy, OnInit, SkipSelf } from '@angular/core'; import { Router } from '@angular/router'; import { Subscription } from 'rxjs'; -import { SDKOptions, StrivacityAuthService, FallbackError, StyLoginRenderer, LoginFlowState, type ExtraRequestArgs } from '@strivacity/sdk-angular'; +import { SDKOptions, StrivacityAuthService, FallbackError, LoginFlowState, type ExtraRequestArgs } from '@strivacity/sdk-angular'; import { type ImportMeta } from '../../app.config'; import { widgets } from '../../components/widgets'; @Component({ - standalone: true, - imports: [StyLoginRenderer], + standalone: false, selector: 'app-login-page', templateUrl: './login.page.html', }) diff --git a/apps/angular/src/app/pages/logout/logout.page.ts b/apps/angular/src/app/pages/logout/logout.page.ts index 3c92e3ea..630ab214 100644 --- a/apps/angular/src/app/pages/logout/logout.page.ts +++ b/apps/angular/src/app/pages/logout/logout.page.ts @@ -4,7 +4,7 @@ import { Subscription, firstValueFrom } from 'rxjs'; import { StrivacityAuthService } from '@strivacity/sdk-angular'; @Component({ - standalone: true, + standalone: false, selector: 'app-logout-page', templateUrl: './logout.page.html', }) diff --git a/apps/angular/src/app/pages/profile/profile.page.ts b/apps/angular/src/app/pages/profile/profile.page.ts index 9867f0ad..54c276e7 100644 --- a/apps/angular/src/app/pages/profile/profile.page.ts +++ b/apps/angular/src/app/pages/profile/profile.page.ts @@ -1,13 +1,11 @@ import { Component, SkipSelf } from '@angular/core'; -import { DatePipe, JsonPipe } from '@angular/common'; import { Subscription } from 'rxjs'; import { Session, StrivacityAuthService } from '@strivacity/sdk-angular'; @Component({ - standalone: true, + standalone: false, selector: 'app-profile-page', templateUrl: './profile.page.html', - imports: [JsonPipe, DatePipe], }) export class ProfilePage { readonly subscription = new Subscription(); diff --git a/apps/angular/src/app/pages/register/register.page.ts b/apps/angular/src/app/pages/register/register.page.ts index 94c746be..3eca1be9 100644 --- a/apps/angular/src/app/pages/register/register.page.ts +++ b/apps/angular/src/app/pages/register/register.page.ts @@ -3,13 +3,12 @@ import { Component, OnDestroy, OnInit, SkipSelf } from '@angular/core'; import { Router } from '@angular/router'; import { Subscription } from 'rxjs'; -import { SDKOptions, StrivacityAuthService, FallbackError, StyLoginRenderer, LoginFlowState, type ExtraRequestArgs } from '@strivacity/sdk-angular'; +import { SDKOptions, StrivacityAuthService, FallbackError, LoginFlowState, type ExtraRequestArgs } from '@strivacity/sdk-angular'; import { type ImportMeta } from '../../app.config'; import { widgets } from '../../components/widgets'; @Component({ - standalone: true, - imports: [StyLoginRenderer], + standalone: false, selector: 'app-register-page', templateUrl: './register.page.html', }) diff --git a/apps/angular/src/app/pages/revoke/revoke.page.ts b/apps/angular/src/app/pages/revoke/revoke.page.ts index 74fa9455..62162383 100644 --- a/apps/angular/src/app/pages/revoke/revoke.page.ts +++ b/apps/angular/src/app/pages/revoke/revoke.page.ts @@ -4,7 +4,7 @@ import { Subscription } from 'rxjs'; import { type RedirectFlow, StrivacityAuthService } from '@strivacity/sdk-angular'; @Component({ - standalone: true, + standalone: false, selector: 'app-revoke-page', templateUrl: './revoke.page.html', }) diff --git a/apps/angular/src/main.ts b/apps/angular/src/main.ts index ba1e35b3..1ecd750e 100644 --- a/apps/angular/src/main.ts +++ b/apps/angular/src/main.ts @@ -1,7 +1,7 @@ -import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; -import { AppComponent } from './app/app.component'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app/app.module'; -bootstrapApplication(AppComponent, appConfig) +platformBrowserDynamic() + .bootstrapModule(AppModule) // eslint-disable-next-line no-console .catch((err) => console.error(err)); diff --git a/package.json b/package.json index 5c4dc6af..08d9505c 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "sdk:core:test": "nx run sdk-core:test", "app:angular:serve": "nx run app-angular:serve", "app:angular:build": "nx run app-angular:build", + "app:angular-standalone:serve": "nx run app-angular-standalone:serve", + "app:angular-standalone:build": "nx run app-angular-standalone:build", "app:backend:serve": "nx run app-backend:serve", "app:backend:build": "nx run app-backend:build", "app:angular-bff:serve": "nx run app-angular-bff:serve",