├── .codeclimate.yml
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .fossa.yml
├── .github
├── FUNDING.yml
└── workflows
│ ├── browserlist_update.yml
│ ├── bundlewatch.yml
│ ├── bundlewatch_pr.yml
│ ├── deploy.yml
│ ├── gatsby.template
│ ├── gatsby.yml
│ ├── license.yml
│ ├── next.config.template
│ ├── nextjs.template
│ ├── nextjs.yml
│ ├── providers_check.yml
│ ├── publish.yml
│ ├── readme.yml
│ └── test.yml
├── .gitignore
├── .prettierrc
├── LICENSE.md
├── README.md
├── example
├── .prettierrc
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── doc.md
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── BISelect.tsx
│ ├── CodeEditor.tsx
│ ├── CodeRenderer.tsx
│ ├── Configurator.tsx
│ ├── DocTable.tsx
│ ├── EventsTable.tsx
│ ├── Header.tsx
│ ├── constants.ts
│ ├── index.css
│ ├── index.tsx
│ ├── react-app-env.d.ts
│ └── setupTests.ts
└── tsconfig.json
├── package-lock.json
├── package.json
├── publiccode.yml
├── scripts
├── check_providers.js
├── doc.js
├── readme.template
├── spid-dropdown.png
├── spid-modal.png
├── styles.sh
└── svgs.sh
├── src
├── .eslintrc
├── a11y.test.tsx
├── component.test.tsx
├── component.tsx
├── dropdownVariant
│ ├── ProvidersMenu.tsx
│ ├── constants.ts
│ ├── index.module.css
│ ├── index.tsx
│ └── util.tsx
├── index.tsx
├── modalVariant
│ ├── ProvidersModal.tsx
│ ├── constants.tsx
│ ├── extra.module.css
│ ├── index.module.css
│ ├── index.tsx
│ ├── types.tsx
│ ├── utils.test.tsx
│ └── utils.tsx
├── react-app-env.d.ts
├── shared
│ ├── ProviderButton.module.css
│ ├── ProviderButton.tsx
│ ├── i18n.test.ts
│ ├── i18n.ts
│ ├── providers.tsx
│ ├── providers_meta.json
│ ├── svgs
│ │ ├── close.svg
│ │ ├── idp-logos
│ │ │ ├── spid-idp-arubaid.svg
│ │ │ ├── spid-idp-dummy.svg
│ │ │ ├── spid-idp-infocertid.svg
│ │ │ ├── spid-idp-intesaid.svg
│ │ │ ├── spid-idp-lepidaid.svg
│ │ │ ├── spid-idp-namirialid.svg
│ │ │ ├── spid-idp-posteid.svg
│ │ │ ├── spid-idp-sielteid.svg
│ │ │ ├── spid-idp-spiditalia.svg
│ │ │ ├── spid-idp-teamsystemid.svg
│ │ │ └── spid-idp-timid.svg
│ │ ├── spid-button-logo-bb-short.svg
│ │ ├── spid-button-logo-bb.svg
│ │ ├── spid-ico-circle-bb-bg.svg
│ │ ├── spid-ico-circle-bb.svg
│ │ ├── spid-ico-circle-lb.svg
│ │ ├── spid-logo-animation-black.svg
│ │ ├── spid-logo-animation.svg
│ │ └── spid-logo.svg
│ ├── types.ts
│ ├── utils.test.tsx
│ └── utils.ts
├── test
│ └── utils.ts
└── typings.d.ts
├── tsconfig.json
└── tsconfig.test.json
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | exclude_patterns:
2 | - "example"
3 | - "scripts"
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
3 | node_modules/
4 | .snapshots/
5 | *.min.js
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "standard",
5 | "standard-react",
6 | "plugin:prettier/recommended",
7 | "prettier/standard",
8 | "prettier/react",
9 | "plugin:@typescript-eslint/eslint-recommended"
10 | ],
11 | "env": {
12 | "node": true
13 | },
14 | "parserOptions": {
15 | "ecmaVersion": 2020,
16 | "ecmaFeatures": {
17 | "legacyDecorators": true,
18 | "jsx": true
19 | }
20 | },
21 | "settings": {
22 | "react": {
23 | "version": "16"
24 | }
25 | },
26 | "rules": {
27 | "space-before-function-paren": 0,
28 | "react/prop-types": 0,
29 | "react/jsx-handler-names": 0,
30 | "react/jsx-fragments": 0,
31 | "react/no-unused-prop-types": 0,
32 | "import/export": 0,
33 | "no-unused-vars": 0
34 | }
35 | }
--------------------------------------------------------------------------------
/.fossa.yml:
--------------------------------------------------------------------------------
1 | # Generated by FOSSA CLI (https://github.com/fossas/fossa-cli)
2 | # Visit https://fossa.com to learn more
3 |
4 | version: 2
5 | cli:
6 | server: https://app.fossa.com
7 | fetcher: custom
8 | project: git@github.com:dej611/spid-react-button.git
9 | analyze:
10 | modules:
11 | - name: spid-react-button
12 | type: npm
13 | target: .
14 | path: .
15 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | custom: https://www.buymeacoffee.com/dej611
4 |
--------------------------------------------------------------------------------
/.github/workflows/browserlist_update.yml:
--------------------------------------------------------------------------------
1 | name: Monthly browserlist check
2 |
3 | on:
4 | schedule:
5 | - cron: '0 13 1 * *'
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Use Node.js
15 | uses: actions/setup-node@v1
16 | with:
17 | node-version: 14.18.2
18 |
19 | - uses: actions/cache@v2
20 | with:
21 | path: ~/.npm
22 | key: ${{ runner.os }}-node-14-${{ hashFiles('**/package-lock.json') }}
23 | restore-keys: |
24 | ${{ runner.os }}-node-14-
25 |
26 | - name: Update browserlist command
27 | run: npx browserslist@latest --update-db
28 |
29 | - name: Commit to main
30 | id: commit
31 | uses: EndBug/add-and-commit@v7.0.0
32 | with:
33 | author_name: github-actions
34 | author_email: 41898282+github-actions[bot]@users.noreply.github.com
35 | message: '[skip ci] Update browserlist'
36 | add: '*.json'
37 |
38 | - name: Handle no changes
39 | if: steps.commit.outputs. committed != 'true'
40 | run: echo "No commit was required";
--------------------------------------------------------------------------------
/.github/workflows/bundlewatch.yml:
--------------------------------------------------------------------------------
1 | name: "Bundlewatch GitHub Action - on Tracked Branches Push"
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | bundlewatch:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Use Node.js
15 | uses: actions/setup-node@v1
16 | with:
17 | node-version: 16
18 |
19 | - name: Installing dependencies
20 | run: npm ci
21 |
22 | - uses: jackyef/bundlewatch-gh-action@master
23 | with:
24 | build-script: npm run build
25 | bundlewatch-github-token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
26 |
--------------------------------------------------------------------------------
/.github/workflows/bundlewatch_pr.yml:
--------------------------------------------------------------------------------
1 | name: "Bundlewatch GitHub Action"
2 |
3 | on:
4 | pull_request:
5 | types: [synchronize, opened]
6 |
7 | jobs:
8 | bundlewatch:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 |
13 | - name: Use Node.js
14 | uses: actions/setup-node@v1
15 | with:
16 | node-version: 16
17 |
18 | - name: Installing dependencies
19 | run: npm ci
20 |
21 | - uses: jackyef/bundlewatch-gh-action@master
22 | with:
23 | build-script: npm run build
24 | bundlewatch-github-token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
25 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to Github Pages
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 |
13 | - name: Use Node.js
14 | uses: actions/setup-node@v1
15 | with:
16 | node-version: 16
17 |
18 | - name: Installing dependencies
19 | run: npm ci
20 |
21 | - name: Build and deploy to Github Pages
22 | run: npm run predeploy
23 |
24 | - name: Deploy
25 | uses: peaceiris/actions-gh-pages@v3
26 | with:
27 | github_token: ${{ secrets.GITHUB_TOKEN }}
28 | publish_dir: ./example/build
29 |
--------------------------------------------------------------------------------
/.github/workflows/gatsby.template:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { SPIDReactButton } from '@dej611/spid-react-button'
4 | import '@dej611/spid-react-button/dist/index.css';
5 |
6 | import Layout from "../components/layout"
7 | import SEO from "../components/seo"
8 |
9 | const IndexPage = () => (
10 |
11 |
12 |
15 |
18 |
19 | )
20 |
21 | export default IndexPage
--------------------------------------------------------------------------------
/.github/workflows/gatsby.yml:
--------------------------------------------------------------------------------
1 | name: Gatsby.js SSR tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | pull_request:
9 | branches:
10 | - main
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | node: [14, 16]
18 | steps:
19 | - uses: actions/checkout@v2
20 |
21 | - name: Use Node.js
22 | uses: actions/setup-node@v1
23 | with:
24 | node-version: ${{ matrix.node }}
25 |
26 | - uses: actions/cache@v2
27 | with:
28 | path: ~/.npm
29 | key: ${{ runner.os }}-node-${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
30 | restore-keys: |
31 | ${{ runner.os }}-node-${{ matrix.node }}-
32 |
33 | - name: Installing dependencies
34 | run: npm ci
35 |
36 | - name: Build the library
37 | run: npm run build:ts
38 |
39 | - name: Enable the linking
40 | run: npm link
41 |
42 | - name: Start a new Gatsby project
43 | run: npx gatsby new test-gatsby
44 |
45 | - name: Link lib + add template page
46 | run: cd test-gatsby && npm link @dej611/spid-react-button && cp ../.github/workflows/gatsby.template ./src/pages/index.js
47 |
48 | - name: Run a build
49 | run: cd test-gatsby && npx gatsby build
50 |
--------------------------------------------------------------------------------
/.github/workflows/license.yml:
--------------------------------------------------------------------------------
1 | name: License check with FOSSA
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | pull_request:
9 | branches:
10 | - main
11 |
12 | jobs:
13 | check:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v2
19 |
20 | - name: Run FOSSA scan and upload build data
21 | uses: fossa-contrib/fossa-action@e015db70fcadbf6316ed25b4c0c264d78d971714
22 | with:
23 | fossa-api-key: cdee9889737a795723195b4b4f9494cf
--------------------------------------------------------------------------------
/.github/workflows/next.config.template:
--------------------------------------------------------------------------------
1 | const withTM = require('next-transpile-modules')(['@dej611/spid-react-button']);
2 |
3 | module.exports = withTM({
4 | reactStrictMode: true,
5 | webpack(config) {
6 | const fileLoaderRule = config.module.rules.find(rule => rule.test && rule.test.test('.svg'))
7 | fileLoaderRule.exclude = /\.svg$/
8 | config.module.rules.push({
9 | test: /\.svg$/,
10 | loader: require.resolve('@svgr/webpack')
11 | },
12 | {
13 | test: /\.svg$/,
14 | loader: require.resolve('url-loader')
15 | })
16 | return config
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/.github/workflows/nextjs.template:
--------------------------------------------------------------------------------
1 | import { SPIDReactButton } from '@dej611/spid-react-button'
2 | import '@dej611/spid-react-button/dist/index.css';
3 |
4 | function HomePage() {
5 | return
6 |
9 |
12 |
13 | }
14 |
15 | export default HomePage
--------------------------------------------------------------------------------
/.github/workflows/nextjs.yml:
--------------------------------------------------------------------------------
1 | name: NextJS SSR tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | pull_request:
9 | branches:
10 | - main
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | node: [14, 16]
18 | steps:
19 | - uses: actions/checkout@v2
20 |
21 | - name: Use Node.js
22 | uses: actions/setup-node@v1
23 | with:
24 | node-version: ${{ matrix.node }}
25 |
26 | - uses: actions/cache@v2
27 | with:
28 | path: ~/.npm
29 | key: ${{ runner.os }}-node-${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
30 | restore-keys: |
31 | ${{ runner.os }}-node-${{ matrix.node }}-
32 |
33 | - name: Installing dependencies
34 | run: npm ci
35 |
36 | - name: Build the library
37 | run: npm run build:ts
38 |
39 | - name: Enable the linking
40 | run: npm link
41 |
42 | - name: Start a new NextJS project
43 | run: cd .. && npx create-next-app --typescript --use-npm -e https://github.com/dej611/nextjs-spid-button test-nextjs
44 |
45 | - name: Link current library version
46 | run: cd ../test-nextjs && npm link @dej611/spid-react-button
47 |
48 | - name: Run a build
49 | run: cd ../test-nextjs && npx next build
50 |
--------------------------------------------------------------------------------
/.github/workflows/providers_check.yml:
--------------------------------------------------------------------------------
1 | name: Weekly providers check
2 |
3 | on:
4 | schedule:
5 | - cron: '0 13 * * 1'
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Use Node.js
15 | uses: actions/setup-node@v1
16 | with:
17 | node-version: 16
18 |
19 | - uses: actions/cache@v2
20 | with:
21 | path: ~/.npm
22 | key: ${{ runner.os }}-node-16-${{ hashFiles('**/package-lock.json') }}
23 | restore-keys: |
24 | ${{ runner.os }}-node-16-
25 |
26 | - name: Installing dependencies
27 | run: npm ci
28 |
29 | - name: Build the library
30 | run: npm run build
31 |
32 | - name: Check the remote providers list
33 | run: npm run providers:check
34 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to NPM
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 |
13 | - name: Use Node.js
14 | uses: actions/setup-node@v1
15 | with:
16 | node-version: 16
17 |
18 | - uses: actions/cache@v2
19 | with:
20 | path: ~/.npm
21 | key: ${{ runner.os }}-node-16-${{ hashFiles('**/package-lock.json') }}
22 | restore-keys: |
23 | ${{ runner.os }}-node-16-
24 |
25 | - name: Installing dependencies
26 | run: npm ci
27 |
28 | - name: Test
29 | run: npm test
30 | - uses: JS-DevTools/npm-publish@v1
31 | with:
32 | token: ${{ secrets.NPM_TOKEN }}
33 |
--------------------------------------------------------------------------------
/.github/workflows/readme.yml:
--------------------------------------------------------------------------------
1 | name: Build readme
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | - name: Use Node.js
16 | uses: actions/setup-node@v1
17 | with:
18 | node-version: 16
19 |
20 | - uses: actions/cache@v2
21 | with:
22 | path: ~/.npm
23 | key: ${{ runner.os }}-node-16-${{ hashFiles('**/package-lock.json') }}
24 | restore-keys: |
25 | ${{ runner.os }}-node-16-
26 |
27 | - name: Installing dependencies
28 | run: npm ci
29 |
30 | - name: Build the documentation
31 | run: npm run doc
32 |
33 | - name: Commit to main
34 | id: commit
35 | uses: EndBug/add-and-commit@v7.0.0
36 | with:
37 | author_name: github-actions
38 | author_email: 41898282+github-actions[bot]@users.noreply.github.com
39 | message: '[skip ci] Update readme'
40 | add: '*.md'
41 |
42 | - name: Handle no changes
43 | if: steps.commit.outputs. committed != 'true'
44 | run: echo "No commit was required";
45 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Unit tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | pull_request:
9 | branches:
10 | - main
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v2
17 |
18 | - name: Use Node.js
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: 16
22 |
23 | - uses: actions/cache@v2
24 | with:
25 | path: ~/.npm
26 | key: ${{ runner.os }}-node-16-${{ hashFiles('**/package-lock.json') }}
27 | restore-keys: |
28 | ${{ runner.os }}-node-16-
29 |
30 | - name: Installing dependencies
31 | run: npm ci
32 |
33 | - name: Test from committers
34 | uses: paambaati/codeclimate-action@v3.0.0
35 | if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'
36 | env:
37 | CC_TEST_REPORTER_ID: ${{ secrets.CODE_CLIMATE_TOKEN }}
38 | with:
39 | coverageCommand: npm test
40 |
41 | - name: Test from forks
42 | if: github.event.pull_request.head.repo.full_name != github.repository || github.actor == 'dependabot[bot]'
43 | run: npm test
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # See https://help.github.com/ignore-files/ for more about ignoring files.
3 |
4 | # dependencies
5 | node_modules
6 |
7 | # builds
8 | build
9 | dist
10 | .rpt2_cache
11 | coverage
12 | temp
13 |
14 | # misc
15 | .DS_Store
16 | .env
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 | doc.json
26 |
27 | *.tgz
28 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "jsxSingleQuote": true,
4 | "semi": true,
5 | "tabWidth": 2,
6 | "bracketSpacing": true,
7 | "jsxBracketSameLine": false,
8 | "arrowParens": "always",
9 | "trailingComma": "none"
10 | }
11 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | European Union Public Licence V. 1.2
2 |
3 | EUPL © the European Union 2007, 2016
4 |
5 | This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such use is covered by a right of the copyright holder of the Work).
6 |
7 | The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following notice immediately following the copyright notice for the Work: “Licensed under the EUPL”, or has expressed by any other means his willingness to license under the EUPL.
8 |
9 | Definitions
10 | In this Licence, the following terms have the following meaning: — ‘The Licence’: this Licence. — ‘The Original Work’: the work or software distributed or communicated by the ‘Licensor under this Licence, available as Source Code and also as ‘Executable Code as the case may be. — ‘Derivative Works’: the works or software that could be created by the ‘Licensee, based upon the Original Work or modifications thereof. This ‘Licence does not define the extent of modification or dependence on the ‘Original Work required in order to classify a work as a Derivative Work; ‘this extent is determined by copyright law applicable in the country ‘mentioned in Article 15. — ‘The Work’: the Original Work or its Derivative Works. — ‘The Source Code’: the human-readable form of the Work which is the most convenient for people to study and modify.
11 |
12 | — ‘The Executable Code’: any code which has generally been compiled and which is meant to be interpreted by a computer as a program. — ‘The Licensor’: the natural or legal person that distributes or communicates the Work under the Licence. — ‘Contributor(s)’: any natural or legal person who modifies the Work under the Licence, or otherwise contributes to the creation of a Derivative Work. — ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of the Work under the terms of the Licence. — ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential functionalities at the disposal of any other natural or legal person.
13 |
14 | Scope of the rights granted by the Licence
15 | The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for the duration of copyright vested in the Original Work:
16 |
17 | — use the Work in any circumstance and for all usage, — reproduce the Work, — modify the Work, and make Derivative Works based upon the Work, — communicate to the public, including the right to make available or display the Work or copies thereof to the public and perform publicly, as the case may be, the Work, — distribute the Work or copies thereof, — lend and rent the Work or copies thereof, — sublicense rights in the Work or copies thereof.
18 |
19 | Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the applicable law permits so.
20 |
21 | In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed by law in order to make effective the licence of the economic rights here above listed.
22 |
23 | The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the extent necessary to make use of the rights granted on the Work under this Licence.
24 |
25 | Communication of the Source Code
26 | The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to distribute or communicate the Work.
27 |
28 | Limitations on copyright
29 | Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations thereto.
30 |
31 | Obligations of the Licensee
32 | The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those obligations are the following:
33 |
34 | Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work to carry prominent notices stating that the Work has been modified and the date of modification.
35 |
36 | Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless the Original Work is expressly distributed only under this version of the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the Work or Derivative Work that alter or restrict the terms of the Licence.
37 |
38 | Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail.
39 |
40 | Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available for as long as the Licensee continues to distribute or communicate the Work.
41 |
42 | Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the copyright notice.
43 |
44 | Chain of Authorship
45 | The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence.
46 |
47 | Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence.
48 |
49 | Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions to the Work, under the terms of this Licence.
50 |
51 | Disclaimer of Warranty
52 | The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work and may therefore contain defects or ‘bugs’ inherent to this type of development.
53 |
54 | For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this Licence.
55 |
56 | This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work.
57 |
58 | Disclaimer of Liability
59 | Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However, the Licensor will be liable under statutory product liability laws as far such laws apply to the Work.
60 |
61 | Additional agreements
62 | While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by the fact You have accepted any warranty or additional liability.
63 |
64 | Acceptance of the Licence
65 | The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms and conditions.
66 |
67 | Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution or Communication by You of the Work or copies thereof.
68 |
69 | Information to the public
70 | In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, by offering to download the Work from a remote location) the distribution channel or media (for example, a website) must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence and the way it may be accessible, concluded, stored and reproduced by the Licensee.
71 |
72 | Termination of the Licence
73 | The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms of the Licence. Such a termination will not terminate the licences of any person who has received the Work from the Licensee under the Licence, provided such persons remain in full compliance with the Licence.
74 |
75 | Miscellaneous
76 | Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the Work.
77 |
78 | If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid and enforceable.
79 |
80 | The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. New versions of the Licence will be published with a unique version number.
81 |
82 | All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take advantage of the linguistic version of their choice.
83 |
84 | Jurisdiction
85 | Without prejudice to specific agreement between parties, — any litigation resulting from the interpretation of this License, arising between the European Union institutions, bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union, — any litigation arising between other parties and resulting from the interpretation of this License, will be subject to the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business.
86 |
87 | Applicable Law
88 | Without prejudice to specific agreement between parties, — this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, resides or has his registered office, — this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside a European Union Member State.
89 |
90 | Appendix
91 |
92 | ‘Compatible Licences’ according to Article 5 EUPL are: — GNU General Public License (GPL) v. 2, v. 3 — GNU Affero General Public License (AGPL) v. 3 — Open Software License (OSL) v. 2.1, v. 3.0 — Eclipse Public License (EPL) v. 1.0 — CeCILL v. 2.0, v. 2.1 — Mozilla Public Licence (MPL) v. 2 — GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 — Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software — European Union Public Licence (EUPL) v. 1.1, v. 1.2 — Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+)
93 |
94 | — The European Commission may update this Appendix to later versions of the above licences without producing a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the covered Source Code from exclusive appropriation. — All other changes or additions to this Appendix require the production of a new EUPL version.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # spid-react-button
2 |
3 | > Pulsante SSO per SPID in React
4 |
5 | [](https://www.npmjs.com/package/@dej611/spid-react-button)    [](https://app.fossa.com/projects/custom%2B24373%2Fgit%40github.com%3Adej611%2Fspid-react-button.git?ref=badge_shield)[](https://codeclimate.com/github/dej611/spid-react-button/maintainability)[](https://codeclimate.com/github/dej611/spid-react-button/test_coverage)[](https://github.com/dej611/spid-react-button/actions/workflows/test.yml)
6 |
7 | [Demo here](https://dej611.github.io/spid-react-button/)
8 |
9 |
10 | | Modal | Dropdown |
11 | | ------------- | ------------- |
12 | | | |
13 |
14 | ## Install
15 |
16 | ```bash
17 | npm install --save @dej611/spid-react-button typeface-titillium-web
18 | ```
19 |
20 | The package depends on the Titillium font.
21 | An alternative to installing the local package is to use it via CDN, adding this line to your css file:
22 | ```css
23 | @import url(https://fonts.googleapis.com/css?family=Titillium+Web:400,600,700,900);
24 | ```
25 |
26 | ## Usage
27 |
28 | ```jsx
29 | import React from 'react'
30 | // Import it via package or in your CSS file via the CDN @import
31 | import 'typeface-titillium-web';
32 | import {SPIDReactButton} from '@dej611/spid-react-button'
33 |
34 | import '@dej611/spid-react-button/dist/index.css'
35 |
36 |
37 | function Example(){
38 | return
39 | }
40 | ```
41 |
42 | **Note**: the providers list has no particular sorting order and is shuffled at every page load.
43 |
44 | #### Accessibility notes
45 |
46 | In general using the `POST` version of this component makes it usable for the most wide audience, as the `GET` version uses links which may have issues. To know more read below.
47 |
48 | The component tries its best to provide the best a11y practices implemented, but there are some specific browser behaviours that may still keep it hard to use with keyboard.
49 | In particular in OSX Safari and Firefox have some issues with `focus` and the behaviour of `tab`: [please read about it](https://github.com/theKashey/react-focus-lock#focusing-in-osx-safarifirefox-is-strange) when using this component.
50 |
51 |
52 | #### Contributing
53 |
54 | To get up and running with the development of the project you can follow these steps.
55 |
56 | To see the component in action, useful to test it manually after some changes, open a terminal window and type:
57 |
58 | ```sh
59 | cd example
60 | npm start
61 | ```
62 |
63 | This will start the `create-react-app` project and open a new browser tab with the example application in it.
64 |
65 | At this point it is possible to perform changes to the component/library, open a new terminal in the project folder and use this command when done with changes:
66 | ```sh
67 | npm build
68 | ```
69 |
70 | To run the test do:
71 |
72 | ```sh
73 | npm test
74 | ```
75 |
76 | ### Change Readme file
77 |
78 | The readme file is autogenerated from a template located in `script/readme.template`: the template contains everything but the API which is generated from the Typescript source.
79 | No need to touch the API part (which will be overwritten anyway).
80 |
81 | ## Next.js
82 |
83 | Next.js 11 has some issues with the imported SVGs in this package, therefore it requires some configuration tweak and some additional dependencies to make it work correctly.
84 |
85 | First step, install some additional dependencies:
86 |
87 | ```sh
88 | npm i @svgr/webpack url-loader next-transpile-modules --save-dev
89 | ```
90 |
91 | These three dependencies should be added to the Next.js configuration to enable SVG import as URLs (used in this package for the buttons.)
92 |
93 | ```js
94 | // next.config.js
95 | const withTM = require('next-transpile-modules')(['@dej611/spid-react-button']);
96 |
97 | module.exports = withTM({
98 | reactStrictMode: true,
99 | webpack(config) {
100 | const fileLoaderRule = config.module.rules.find(rule => rule.test && rule.test.test('.svg'))
101 | fileLoaderRule.exclude = /\.svg$/
102 | config.module.rules.push({
103 | test: /\.svg$/,
104 | loader: require.resolve('@svgr/webpack')
105 | },
106 | {
107 | test: /\.svg$/,
108 | loader: require.resolve('url-loader')
109 | })
110 | return config
111 | }
112 | })
113 | ```
114 |
115 | # API
116 |
117 |
118 | ## Components
119 |
120 | ### SPIDReactButton
121 |
122 | **Type**: `Component`
123 |
124 | **Props**: `SPIDButtonProps`
125 |
126 | The main component for the button.
127 | Use this component with the `type` prop to select the version you prefer.
128 |
129 |
130 | The SPIDButtonProps object contains the following properties:
131 |
132 | #### configuration
133 |
134 | **Possible values**: `{ method : "GET" } | { extraFields ?: Record, fieldName : string, method : "POST" }`
135 |
136 | **Type**: `ConfigurationGET | ConfigurationPOST`
137 |
138 | **Required**: No
139 |
140 | **Default value**: `{"method": "GET"}`
141 |
142 |
143 | Each Provider button will use this configuration for its button.
144 | The default value is `{"method": "GET"}`
145 |
146 |
147 |
148 | #### corners
149 |
150 | **Possible values**: `"rounded" | "sharp"`
151 |
152 | **Type**: `CornerType`
153 |
154 | **Required**: No
155 |
156 | **Default value**: `"rounded"`
157 |
158 |
159 | The type of corner for the button: rounded or sharp.
160 | The default value is `"rounded"`.
161 |
162 |
163 |
164 | #### extraProviders
165 |
166 |
167 | **Type**: `ProviderRecord[]`
168 |
169 | **Required**: No
170 |
171 |
172 |
173 | Used for testing. *Do not use in production*
174 |
175 |
176 |
177 | #### fluid
178 |
179 |
180 | **Type**: `boolean`
181 |
182 | **Required**: No
183 |
184 | **Default value**: `false`
185 |
186 |
187 | This controls the width of the button: when fluid it will fill all the available space.
188 | It applies only to the modal version.
189 | The default value is `false`.
190 |
191 |
192 |
193 | #### lang
194 |
195 | **Possible values**: `"it" | "en" | "de" | "es" | "fr"`
196 |
197 | **Type**: `Languages`
198 |
199 | **Required**: No
200 |
201 | **Default value**: `"it"`
202 |
203 |
204 | The language used for the UI. The default value is `"it"`.
205 |
206 |
207 |
208 | #### mapping
209 |
210 |
211 | **Type**: `Record`
212 |
213 | **Required**: No
214 |
215 |
216 |
217 | An object containing the mapping for the providers.
218 | This is useful when a Service Provider identifies the IDP with a different string than the entityID
219 |
220 |
221 |
222 | #### onProviderClicked
223 |
224 |
225 | **Type**: `(
226 | providerEntry : ProviderRecord,
227 | loginURL : string | undefined,
228 | event : React.MouseEvent | React.MouseEvent) => void`
229 |
230 | **Required**: No
231 |
232 |
233 |
234 | This is called when a user clicks on a provider button.
235 |
236 | * `providerEntry`: The full entry of the provider clicked is passed, together with the event
237 | * `loginURL`: The final URL for the specific Identity Provider. It returns undefined if the button is disabled
238 | * `event`: React original MouseEvent
239 |
240 |
241 |
242 | #### onProvidersHidden
243 |
244 |
245 | **Type**: `() => void`
246 |
247 | **Required**: No
248 |
249 |
250 |
251 | This is called when the providers are hidden on the screen (as soon as the animation starts)
252 |
253 |
254 |
255 | #### onProvidersShown
256 |
257 |
258 | **Type**: `() => void`
259 |
260 | **Required**: No
261 |
262 |
263 |
264 | This is called when the providers are shown on the screen (as soon as the animation starts)
265 |
266 |
267 |
268 | #### protocol
269 |
270 | **Possible values**: `"SAML" | "OIDC"`
271 |
272 | **Type**: `Protocols`
273 |
274 | **Required**: No
275 |
276 | **Default value**: `"SAML"`
277 |
278 |
279 | The protocol to use for the current instance.
280 | Only Providers who support the declared protocol are enabled.
281 | The default value is `"SAML"`.
282 |
283 |
284 |
285 | #### size
286 |
287 | **Possible values**: `"sm" | "md" | "l" | "xl"`
288 |
289 | **Type**: `Sizes`
290 |
291 | **Required**: No
292 |
293 | **Default value**: `"md"`
294 |
295 |
296 | The size of the button. Options are: `"sm"` (small), `"md"` (medium), `"l"` (large) and `"xl"` (extra large - dropdown only).
297 | The modal version does not support the `"xl"` size and will fallback to `"l"` if passed.
298 | The default value is `"md"`.
299 |
300 |
301 |
302 | #### sorted
303 |
304 |
305 | **Type**: `boolean`
306 |
307 | **Required**: No
308 |
309 | **Default value**: `false`
310 |
311 |
312 | It forces an ascending order (A->Z) of the providers, based on the entityName string.
313 | Note that this will sort with no distictions between official and extraProviders in the list.
314 |
315 |
316 |
317 | #### supported
318 |
319 | **Possible values**: `string[]`
320 |
321 | **Type**: `ProviderRecord["entityID"][]`
322 |
323 | **Required**: No
324 |
325 | **Default value**: All providers
326 |
327 |
328 | The list of entityID supported by the button instance.
329 | The default value is all the official providers.
330 |
331 |
332 |
333 | #### theme
334 |
335 | **Possible values**: `"positive" | "negative"`
336 |
337 | **Type**: `ColorTheme`
338 |
339 | **Required**: No
340 |
341 | **Default value**: `"positive"`
342 |
343 |
344 | The theme used for the button:
345 | * "positive" has a blue background with white text,
346 | * "negative" has a white background and blue text.
347 | The default value is `"positive"`.
348 |
349 |
350 |
351 | #### type
352 |
353 | **Possible values**: `"modal" | "dropdown"`
354 |
355 | **Type**: `Types`
356 |
357 | **Required**: No
358 |
359 | **Default value**: `"modal"`
360 |
361 |
362 | The way to present the providers once clicked. The default value is `"modal"`.
363 |
364 |
365 |
366 | #### url
367 |
368 |
369 | **Type**: `string`
370 |
371 | **Required**: Yes
372 |
373 |
374 |
375 | The URL used by the buttons.
376 | It can be either absolute or relative.
377 | It must contains the `"{{idp}}"` string in it, which will be replaced by the entityID of each provider
378 | (unless specified otherwise with the `mapping` prop - see below).
379 | This props is *mandatory*.
380 |
381 |
382 |
383 |
384 |
385 |
386 | ___
387 |
388 | ### SPIDReactButtonDropdown
389 |
390 | **Type**: `Component`
391 |
392 | **Props**: `SPIDButtonProps`
393 |
394 | The specific component button with the dropdown.
395 | Use this component when you want to minimize the footprint in your project.
396 | It accepts the same props as the main component. The `type` prop is ignored in this case.
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 | ___
405 |
406 | ### SPIDReactButtonModal
407 |
408 | **Type**: `Component`
409 |
410 | **Props**: `SPIDButtonProps`
411 |
412 | The specific component button with the modal.
413 | Use this component when you want to minimize the footprint in your project.
414 | It accepts the same props as the main component. The `type` prop is ignored in this case.
415 |
416 |
417 |
418 |
419 |
420 |
421 | ___
422 |
423 |
424 | ## Types
425 |
426 | **ColorTheme**: `"positive" | "negative"`
427 |
428 | The theme used for the button:
429 | * "positive" has a blue background with white text,
430 | * "negative" has a white background and blue text.
431 |
432 | ___
433 |
434 | **ConfigurationGET**: `{method: "GET"}`
435 |
436 | Each Provider button will use this configuration for its button.
437 | This is the specific GET type.
438 |
439 | ___
440 |
441 | **ConfigurationPOST**: `{extraFields: Record, fieldName: string, method: "POST"}`
442 |
443 | Each Provider button will use this configuration for its button.
444 | This is the specific POST type
445 |
446 | ___
447 |
448 | **CornerType**: `"rounded" | "sharp"`
449 |
450 | The type of corner for the button: rounded or sharp.
451 |
452 | ___
453 |
454 | **Languages**: `"it" | "en" | "de" | "es" | "fr"`
455 |
456 | The language used for the UI.
457 |
458 | ___
459 |
460 | **Protocols**: `"SAML" | "OIDC"`
461 |
462 | The protocol to use for the current instance.
463 | Only Providers who support the declared protocol are enabled.
464 |
465 | ___
466 |
467 | **Sizes**: `"sm" | "md" | "l" | "xl"`
468 |
469 | The size of the button. Options are: `"sm"` (small), `"md"` (medium), `"l"` (large) and `"xl"` (extra large - dropdown only).
470 | The modal version does not support the `"xl"` size and will fallback to `"l"` if passed.
471 |
472 | ___
473 |
474 | **Types**: `"modal" | "dropdown"`
475 |
476 | The way to present the providers once clicked.
477 |
478 |
479 | ___
480 |
481 |
482 | **ProviderRecord**
483 |
484 | The object format of a Identity Provider object.
485 |
486 | * entityID: `string`
487 |
488 |
489 | * entityName: `string`
490 |
491 |
492 | * logo: `string` - Optional
493 |
494 |
495 | * protocols: `Protocols[]` - Optional
496 |
497 |
498 |
499 |
500 | ___
501 |
502 |
503 | ## Utilites
504 |
505 | #### getShuffledProviders
506 |
507 |
508 | `getShuffledProviders() => RegisteredProviderRecord[]`
509 |
510 | Returns a copy of the list of the official providers, already shuffled
511 |
512 | #### getSupportedLanguages
513 |
514 |
515 | `getSupportedLanguages() => Languages[]`
516 |
517 | Returns the list of supported languages for the UI
518 |
519 |
520 |
521 | ## License
522 |
523 | EUPL 1.2 © [dej611](https://github.com/dej611)
524 |
525 | [](https://app.fossa.com/projects/custom%2B24373%2Fgit%40github.com%3Adej611%2Fspid-react-button.git?ref=badge_large)
--------------------------------------------------------------------------------
/example/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "jsxSingleQuote": true,
4 | "semi": true,
5 | "tabWidth": 2,
6 | "bracketSpacing": true,
7 | "jsxBracketSameLine": false,
8 | "arrowParens": "always",
9 | "trailingComma": "none"
10 | }
11 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | This example was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | It is linked to the spid-react-button package in the parent directory for development purposes.
4 |
5 | You can run `npm install` and then `npm start` to test your package.
6 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spid-react-button-example",
3 | "homepage": ".",
4 | "version": "0.0.0",
5 | "private": true,
6 | "scripts": {
7 | "analyze": "source-map-explorer 'build/static/js/*.js'",
8 | "start": "node ../node_modules/react-scripts/bin/react-scripts.js start",
9 | "build": "node ../node_modules/react-scripts/bin/react-scripts.js build",
10 | "test": "node ../node_modules/react-scripts/bin/react-scripts.js test",
11 | "eject": "node ../node_modules/react-scripts/bin/react-scripts.js eject"
12 | },
13 | "dependencies": {
14 | "@testing-library/jest-dom": "file:../node_modules/@testing-library/jest-dom",
15 | "@testing-library/react": "file:../node_modules/@testing-library/react",
16 | "@testing-library/user-event": "file:../node_modules/@testing-library/user-event",
17 | "@types/jest": "file:../node_modules/@types/jest",
18 | "@types/node": "file:../node_modules/@types/node",
19 | "@types/react": "file:../node_modules/@types/react",
20 | "@types/react-dom": "file:../node_modules/@types/react-dom",
21 | "@types/react-select": "^4.0.13",
22 | "bootstrap-italia": "^1.4.3",
23 | "design-react-kit": "^3.3.3",
24 | "markdown-to-jsx": "^7.1.2",
25 | "prettier": "^2.2.1",
26 | "react": "file:../node_modules/react",
27 | "react-dom": "file:../node_modules/react-dom",
28 | "react-scripts": "file:../node_modules/react-scripts",
29 | "react-select": "^4.3.0",
30 | "react-syntax-highlighter": "^15.4.3",
31 | "source-map-explorer": "^2.5.2",
32 | "@dej611/spid-react-button": "file:..",
33 | "spid-smart-button": "file:../node_modules/spid-smart-button",
34 | "svg-loaders-react": "^2.2.1",
35 | "typeface-lora": "^1.1.13",
36 | "typeface-roboto-mono": "^1.1.13",
37 | "typeface-titillium-web": "^1.1.13",
38 | "typescript": "file:../node_modules/typescript"
39 | },
40 | "devDependencies": {
41 | "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
42 | "@types/react-syntax-highlighter": "^13.5.0"
43 | },
44 | "eslintConfig": {
45 | "extends": "react-app"
46 | },
47 | "browserslist": {
48 | "production": [
49 | ">0.2%",
50 | "not dead",
51 | "not op_mini all"
52 | ],
53 | "development": [
54 | "last 1 chrome version",
55 | "last 1 firefox version",
56 | "last 1 safari version"
57 | ]
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/example/public/doc.md:
--------------------------------------------------------------------------------
1 |
2 | ## Components
3 |
4 | ### SPIDReactButton
5 |
6 | **Type**: `Component`
7 |
8 | **Props**: `SPIDButtonProps`
9 |
10 | The main component for the button.
11 | Use this component with the `type` prop to select the version you prefer.
12 |
13 |
14 | The SPIDButtonProps object contains the following properties:
15 |
16 | #### configuration
17 |
18 | **Possible values**: `{ method : "GET" } | { extraFields ?: Record, fieldName : string, method : "POST" }`
19 |
20 | **Type**: `ConfigurationGET | ConfigurationPOST`
21 |
22 | **Required**: No
23 |
24 | **Default value**: `{"method": "GET"}`
25 |
26 |
27 | Each Provider button will use this configuration for its button.
28 | The default value is `{"method": "GET"}`
29 |
30 |
31 |
32 | #### corners
33 |
34 | **Possible values**: `"rounded" | "sharp"`
35 |
36 | **Type**: `CornerType`
37 |
38 | **Required**: No
39 |
40 | **Default value**: `"rounded"`
41 |
42 |
43 | The type of corner for the button: rounded or sharp.
44 | The default value is `"rounded"`.
45 |
46 |
47 |
48 | #### extraProviders
49 |
50 |
51 | **Type**: `ProviderRecord[]`
52 |
53 | **Required**: No
54 |
55 |
56 |
57 | Used for testing. *Do not use in production*
58 |
59 |
60 |
61 | #### fluid
62 |
63 |
64 | **Type**: `boolean`
65 |
66 | **Required**: No
67 |
68 | **Default value**: `false`
69 |
70 |
71 | This controls the width of the button: when fluid it will fill all the available space.
72 | It applies only to the modal version.
73 | The default value is `false`.
74 |
75 |
76 |
77 | #### lang
78 |
79 | **Possible values**: `"it" | "en" | "de" | "es" | "fr"`
80 |
81 | **Type**: `Languages`
82 |
83 | **Required**: No
84 |
85 | **Default value**: `"it"`
86 |
87 |
88 | The language used for the UI. The default value is `"it"`.
89 |
90 |
91 |
92 | #### mapping
93 |
94 |
95 | **Type**: `Record`
96 |
97 | **Required**: No
98 |
99 |
100 |
101 | An object containing the mapping for the providers.
102 | This is useful when a Service Provider identifies the IDP with a different string than the entityID
103 |
104 |
105 |
106 | #### onProviderClicked
107 |
108 |
109 | **Type**: `(
110 | providerEntry : ProviderRecord,
111 | loginURL : string | undefined,
112 | event : React.MouseEvent | React.MouseEvent) => void`
113 |
114 | **Required**: No
115 |
116 |
117 |
118 | This is called when a user clicks on a provider button.
119 |
120 | * `providerEntry`: The full entry of the provider clicked is passed, together with the event
121 | * `loginURL`: The final URL for the specific Identity Provider. It returns undefined if the button is disabled
122 | * `event`: React original MouseEvent
123 |
124 |
125 |
126 | #### onProvidersHidden
127 |
128 |
129 | **Type**: `() => void`
130 |
131 | **Required**: No
132 |
133 |
134 |
135 | This is called when the providers are hidden on the screen (as soon as the animation starts)
136 |
137 |
138 |
139 | #### onProvidersShown
140 |
141 |
142 | **Type**: `() => void`
143 |
144 | **Required**: No
145 |
146 |
147 |
148 | This is called when the providers are shown on the screen (as soon as the animation starts)
149 |
150 |
151 |
152 | #### protocol
153 |
154 | **Possible values**: `"SAML" | "OIDC"`
155 |
156 | **Type**: `Protocols`
157 |
158 | **Required**: No
159 |
160 | **Default value**: `"SAML"`
161 |
162 |
163 | The protocol to use for the current instance.
164 | Only Providers who support the declared protocol are enabled.
165 | The default value is `"SAML"`.
166 |
167 |
168 |
169 | #### size
170 |
171 | **Possible values**: `"sm" | "md" | "l" | "xl"`
172 |
173 | **Type**: `Sizes`
174 |
175 | **Required**: No
176 |
177 | **Default value**: `"md"`
178 |
179 |
180 | The size of the button. Options are: `"sm"` (small), `"md"` (medium), `"l"` (large) and `"xl"` (extra large - dropdown only).
181 | The modal version does not support the `"xl"` size and will fallback to `"l"` if passed.
182 | The default value is `"md"`.
183 |
184 |
185 |
186 | #### sorted
187 |
188 |
189 | **Type**: `boolean`
190 |
191 | **Required**: No
192 |
193 | **Default value**: `false`
194 |
195 |
196 | It forces an ascending order (A->Z) of the providers, based on the entityName string.
197 | Note that this will sort with no distictions between official and extraProviders in the list.
198 |
199 |
200 |
201 | #### supported
202 |
203 | **Possible values**: `string[]`
204 |
205 | **Type**: `ProviderRecord["entityID"][]`
206 |
207 | **Required**: No
208 |
209 | **Default value**: All providers
210 |
211 |
212 | The list of entityID supported by the button instance.
213 | The default value is all the official providers.
214 |
215 |
216 |
217 | #### theme
218 |
219 | **Possible values**: `"positive" | "negative"`
220 |
221 | **Type**: `ColorTheme`
222 |
223 | **Required**: No
224 |
225 | **Default value**: `"positive"`
226 |
227 |
228 | The theme used for the button:
229 | * "positive" has a blue background with white text,
230 | * "negative" has a white background and blue text.
231 | The default value is `"positive"`.
232 |
233 |
234 |
235 | #### type
236 |
237 | **Possible values**: `"modal" | "dropdown"`
238 |
239 | **Type**: `Types`
240 |
241 | **Required**: No
242 |
243 | **Default value**: `"modal"`
244 |
245 |
246 | The way to present the providers once clicked. The default value is `"modal"`.
247 |
248 |
249 |
250 | #### url
251 |
252 |
253 | **Type**: `string`
254 |
255 | **Required**: Yes
256 |
257 |
258 |
259 | The URL used by the buttons.
260 | It can be either absolute or relative.
261 | It must contains the `"{{idp}}"` string in it, which will be replaced by the entityID of each provider
262 | (unless specified otherwise with the `mapping` prop - see below).
263 | This props is *mandatory*.
264 |
265 |
266 |
267 |
268 |
269 |
270 | ___
271 |
272 | ### SPIDReactButtonDropdown
273 |
274 | **Type**: `Component`
275 |
276 | **Props**: `SPIDButtonProps`
277 |
278 | The specific component button with the dropdown.
279 | Use this component when you want to minimize the footprint in your project.
280 | It accepts the same props as the main component. The `type` prop is ignored in this case.
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 | ___
289 |
290 | ### SPIDReactButtonModal
291 |
292 | **Type**: `Component`
293 |
294 | **Props**: `SPIDButtonProps`
295 |
296 | The specific component button with the modal.
297 | Use this component when you want to minimize the footprint in your project.
298 | It accepts the same props as the main component. The `type` prop is ignored in this case.
299 |
300 |
301 |
302 |
303 |
304 |
305 | ___
306 |
307 |
308 | ## Types
309 |
310 | **ColorTheme**: `"positive" | "negative"`
311 |
312 | The theme used for the button:
313 | * "positive" has a blue background with white text,
314 | * "negative" has a white background and blue text.
315 |
316 | ___
317 |
318 | **ConfigurationGET**: `{method: "GET"}`
319 |
320 | Each Provider button will use this configuration for its button.
321 | This is the specific GET type.
322 |
323 | ___
324 |
325 | **ConfigurationPOST**: `{extraFields: Record, fieldName: string, method: "POST"}`
326 |
327 | Each Provider button will use this configuration for its button.
328 | This is the specific POST type
329 |
330 | ___
331 |
332 | **CornerType**: `"rounded" | "sharp"`
333 |
334 | The type of corner for the button: rounded or sharp.
335 |
336 | ___
337 |
338 | **Languages**: `"it" | "en" | "de" | "es" | "fr"`
339 |
340 | The language used for the UI.
341 |
342 | ___
343 |
344 | **Protocols**: `"SAML" | "OIDC"`
345 |
346 | The protocol to use for the current instance.
347 | Only Providers who support the declared protocol are enabled.
348 |
349 | ___
350 |
351 | **Sizes**: `"sm" | "md" | "l" | "xl"`
352 |
353 | The size of the button. Options are: `"sm"` (small), `"md"` (medium), `"l"` (large) and `"xl"` (extra large - dropdown only).
354 | The modal version does not support the `"xl"` size and will fallback to `"l"` if passed.
355 |
356 | ___
357 |
358 | **Types**: `"modal" | "dropdown"`
359 |
360 | The way to present the providers once clicked.
361 |
362 |
363 | ___
364 |
365 |
366 | **ProviderRecord**
367 |
368 | The object format of a Identity Provider object.
369 |
370 | * entityID: `string`
371 |
372 |
373 | * entityName: `string`
374 |
375 |
376 | * logo: `string` - Optional
377 |
378 |
379 | * protocols: `Protocols[]` - Optional
380 |
381 |
382 |
383 |
384 | ___
385 |
386 |
387 | ## Utilites
388 |
389 | #### getShuffledProviders
390 |
391 |
392 | `getShuffledProviders() => RegisteredProviderRecord[]`
393 |
394 | Returns a copy of the list of the official providers, already shuffled
395 |
396 | #### getSupportedLanguages
397 |
398 |
399 | `getSupportedLanguages() => Languages[]`
400 |
401 | Returns the list of supported languages for the UI
402 |
403 |
--------------------------------------------------------------------------------
/example/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dej611/spid-react-button/15701aa32c24a11f76e831cae6d36604b5283c3d/example/public/favicon.ico
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
16 |
17 |
18 |
27 | spid-react-button
28 |
29 |
30 |
31 |
32 | You need to enable JavaScript to run this app.
33 |
34 |
35 |
36 |
37 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/example/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "spid-react-button",
3 | "name": "spid-react-button",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/example/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div')
7 | ReactDOM.render( , div)
8 | ReactDOM.unmountComponentAtNode(div)
9 | })
10 |
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from 'react'
2 |
3 | import { SPIDReactButton, SPIDButtonProps, ProviderRecord } from '@dej611/spid-react-button'
4 | import 'bootstrap-italia/dist/css/bootstrap-italia.min.css';
5 | import 'typeface-titillium-web';
6 | import 'typeface-roboto-mono';
7 | import 'typeface-lora';
8 |
9 |
10 | import '@dej611/spid-react-button/dist/index.css';
11 |
12 | // @ts-expect-error
13 | import { Col, Row, Container } from 'design-react-kit';
14 |
15 | import { AppHeader } from './Header';
16 | import { defaultURL, initState } from './constants';
17 | import { Configurator } from './Configurator';
18 | import { EventsTable } from './EventsTable';
19 | import { DocTable } from './DocTable';
20 |
21 | const App = () => {
22 | const [buttonProps, setProps] = useState(initState);
23 |
24 | const [isValidURL, setValidURL] = useState(true);
25 | const [events, setEvents] = useState<{ type: string, name: string, arg?: string }[]>([]);
26 |
27 | const updateStateProp = useCallback(
28 | (prop: T, newValue: SPIDButtonProps[T]) => {
29 | return setProps(prevState => ({ ...prevState, [prop]: newValue }))
30 | }, [setProps]);
31 |
32 | const prependEvent = useCallback((newEvent) => {
33 | setEvents((events) => [newEvent, ...events]);
34 | }, [setEvents])
35 |
36 | return <>
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | {(buttonProps.type).toUpperCase()} version
45 | prependEvent({ type: buttonProps.type, name: 'onProvidersShown' })}
49 | onProvidersHidden={() => prependEvent({ type: buttonProps.type, name: 'onProvidersHidden' })}
50 | onProviderClicked={(arg: ProviderRecord, url: string | undefined, e) => {
51 | e.preventDefault();
52 | prependEvent({ type: buttonProps.type, name: 'onProvidersClicked', arg: JSON.stringify({url, arg}, null, 2) })
53 | }}
54 | />
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | F.A.Q.
66 |
67 | X kb (gzipped) is too much for my project! Is it possible to treeshake it?
68 |
69 |
70 | Yes. If you know already you're going to use only one type of button, you can just pick it: just import SPIDReactButtonModal
or SPIDReactButtonDropdown
and experice full treeshake.
71 |
72 |
73 | Is this project official?
74 |
75 |
76 | No, this is not an official project.
77 |
78 |
79 | Is the providers list official?
80 |
81 |
82 | No, as this is not an official project, the list may not be super up-to-date (we check pretty often tho). This official list of SPID providers is avilable here
83 |
84 |
85 | Where the modal version comes from? Is that official?
86 |
87 |
88 | The modal version of this component comes from these other project spid-smart-button
89 |
90 |
91 | Does this component goes in conflict with the design-react-kit
?
92 |
93 |
94 | No. This page was in fact built using components from the design-react-kit
. If you find any conflicting issue with it, please report it to this repository.
95 |
96 |
97 | Why did you write all of this in English rather than Italian?
98 |
99 |
100 | I guess I've started it in English and just finished it. As open source project PR are very welcome, expecially for translations!
101 |
102 |
103 | Does the project have Typescript types?
104 |
105 |
106 | Yes, they are in the package. The API documentation is automatically extracted from types.
107 |
108 |
109 | Can I contribute somehow to the project?
110 |
111 |
112 | Of course you can, glad you've asked. You can report bugs or issues with the project to start with at this repository, or even enhance it with a PR!
113 |
114 |
115 | What's the license of this project?
116 |
117 |
118 | EUPL 1.2, European Union Public Licence V. 1.2
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | Note
138 | All logos of each Identity Provider is a registered trademark of their respective owners
139 | The SPID logo is a registered trademark of AGID, Agenzia per l'Italia Digitale della Presidenza del Consiglio dei Ministri
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | >
148 | }
149 | export default App
150 |
--------------------------------------------------------------------------------
/example/src/BISelect.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Select, { components } from 'react-select'
3 | import { Icon } from 'design-react-kit';
4 |
5 | const Option = (props: any) => {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
12 |
13 | const DropdownIndicator = (props: any) => {
14 | return (
15 |
16 |
17 |
18 | )
19 | }
20 |
21 | type SelectProps = {
22 | options: {value: T, label: string}[]
23 | onChange: (selectedOption: {label: string, value: T} | null) => void,
24 | label: string,
25 | selectedValue: {value: T, label: string}
26 | };
27 |
28 | let counter = 0;
29 | const generatedIds = {};
30 | const idGenerator = (label: string): string => {
31 | generatedIds[label] = generatedIds[label] || `selectExampleClassic-${counter}`;
32 | return generatedIds[label];
33 | }
34 |
35 | export function SelectComponent({options, onChange, label, selectedValue}: SelectProps) {
36 | return (
37 |
38 | {label}
39 | ({ ...provided, height: '2.5rem' }),
47 | valueContainer: provided => ({ ...provided, height: '2.5rem' }),
48 | control: provided => ({ ...provided, height: '2.5rem' })
49 | }}
50 | id={idGenerator(label)}
51 | onChange={onChange}
52 | options={options}
53 | placeholder={label}
54 | aria-label={label}
55 | classNamePrefix="react-select"
56 | value={selectedValue}
57 | />
58 |
59 | )
60 | }
--------------------------------------------------------------------------------
/example/src/CodeEditor.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Icon } from 'design-react-kit';
3 |
4 | const htmlTemplate = `
`
5 |
6 | const indexTemplate = `
7 | import React from "react";
8 | import ReactDOM from "react-dom";
9 |
10 | import App from "./App";
11 |
12 | ReactDOM.render( , document.getElementById("root"));
13 | `
14 |
15 | function makePartOfApp(code: string){
16 | return `
17 | import React from "react";
18 | ${code.replace('function', 'export default function')}
19 | `
20 | }
21 |
22 | export const CodeEditorLink = ({ code }: { code: string }) => {
23 | return (
24 |
51 | )
52 | }
--------------------------------------------------------------------------------
/example/src/CodeRenderer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
3 | import jsx from 'react-syntax-highlighter/dist/esm/languages/prism/jsx';
4 | import css from 'react-syntax-highlighter/dist/esm/languages/prism/css';
5 | import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash';
6 | import {vs} from 'react-syntax-highlighter/dist/esm/styles/prism';
7 | import { CodeEditorLink } from './CodeEditor';
8 |
9 | import { initState, NoFunctionProps } from './constants';
10 |
11 | SyntaxHighlighter.registerLanguage('jsx', jsx);
12 | SyntaxHighlighter.registerLanguage('css', css);
13 | SyntaxHighlighter.registerLanguage('bash', bash);
14 |
15 | function isDefaultProp(prop: string, value: unknown) {
16 | if (prop === 'url') {
17 | return false;
18 | }
19 | if (Array.isArray(value)) {
20 | return value.length === initState[prop].length &&
21 | initState[prop].every((v: unknown, i: number) => v === value[i]);
22 | }
23 | return initState[prop] === value;
24 | }
25 |
26 | export const CodeRenderer = (buttonProps: NoFunctionProps) => {
27 | const entries = Object.entries(buttonProps);
28 | const code = `
29 | import { SPIDReactButton } from '@dej611/spid-react-button';
30 | import 'typeface-titillium-web';
31 | import '@dej611/spid-react-button/dist/index.css';
32 |
33 | function mySPIDButton(props){
34 | return (
35 | !isDefaultProp(prop, value))
38 | .map(([prop, value]) => `${prop}={${JSON.stringify(value, null, 2)}}`)
39 | .join('\n ')}
40 | />
41 | );
42 | }`;
43 |
44 | return
47 | {code}
48 |
49 |
50 |
51 | }
52 |
53 | export const GenericCodeRenderer = ({code, lang} : {code: string, lang: 'css' | 'bash'}) => {
54 | return
57 | {code}
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/example/src/Configurator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { SPIDButtonProps } from '@dej611/spid-react-button'
4 | // @ts-expect-error
5 | import { Input, Col, Row, Toggle, FormGroup, Label } from 'design-react-kit';
6 | import { SelectComponent } from './BISelect';
7 | import { colorThemes, configurations, cornerTypes, languages, NoFunctionProps, protocols, providersList, sizes, types } from './constants';
8 |
9 | function getOptionsAndCurrentSelection(labels: string[], options: T[], currentState: SPIDButtonProps, prop: keyof SPIDButtonProps) {
10 | const outputOptions = labels.map(
11 | (label, i) => ({ label, value: options[i] })
12 | );
13 | // @ts-expect-error
14 | const currentSelection = outputOptions.find(({ value }) => value === currentState[prop])
15 | return { options: outputOptions, selection: currentSelection! }
16 | }
17 |
18 | type ConfiguratorProps = {
19 | buttonProps: NoFunctionProps,
20 | updateProp: (prop: T, newValue: NoFunctionProps[T]) => void,
21 | setValidURL: (newValue: boolean) => void,
22 | isValidURL: boolean
23 | }
24 |
25 | export const Configurator = ({ buttonProps, updateProp, setValidURL, isValidURL }: ConfiguratorProps) => {
26 |
27 |
28 | const { options: langOptions, selection: langSelection } = getOptionsAndCurrentSelection(['Italiano', 'English', 'Deutsche', 'Spagnolo', 'Francese'], languages, buttonProps, 'lang')
29 | const { options: sizeOptions, selection: sizeSelection } = getOptionsAndCurrentSelection(['Small', 'Medium', 'Large'], sizes, buttonProps, 'size')
30 | const { options: colorSchemeOptions, selection: colorThemeSelection } = getOptionsAndCurrentSelection(['Positive', 'Negative'], colorThemes, buttonProps, 'theme')
31 | const { options: protocolOptions, selection: protocolSelection } = getOptionsAndCurrentSelection(['SAML', 'OIDC'], protocols, buttonProps, 'protocol')
32 | const { options: cornerTypeOptions, selection: cornerTypeSelection } = getOptionsAndCurrentSelection(['Rounded', 'Sharp'], cornerTypes, buttonProps, 'corners')
33 | const { options: methodOptions, selection: methodSelection } = getOptionsAndCurrentSelection(['GET', 'POST'], configurations, buttonProps, 'configuration')
34 | const { options: typeOptions, selection: typeSelection } = getOptionsAndCurrentSelection(['Modal', 'Dropown'], types, buttonProps, 'type')
35 |
36 | const validProps = isValidURL ? { valid: true } : { invalid: true }
37 | return <>
38 |
39 |
40 | {
47 | // @ts-expect-error
48 | const newURL = event.target.value;
49 | setValidURL(newURL.indexOf('{{idp}}') > -1);
50 | updateProp('url', newURL);
51 | }}
52 | />
53 |
54 |
55 |
56 | {
61 | if (selectedOption != null && configurations.includes(selectedOption.value)) {
62 | updateProp('configuration', selectedOption.value)
63 | }
64 | }}
65 | />
66 |
67 |
68 |
69 |
70 |
71 |
72 | {
77 | if (selectedOption != null && languages.includes(selectedOption.value)) {
78 | updateProp('lang', selectedOption.value)
79 | }
80 | }}
81 | />
82 |
83 |
84 |
85 |
86 | {
91 | if (selectedOption != null && sizes.includes(selectedOption.value)) {
92 | updateProp('size', selectedOption.value)
93 | }
94 | }}
95 | />
96 |
97 |
98 |
99 |
100 |
101 |
102 | {
107 | if (selectedOption != null && colorThemes.includes(selectedOption.value)) {
108 | updateProp('theme', selectedOption.value)
109 | }
110 | }}
111 | />
112 |
113 |
114 |
115 |
116 | {
121 | if (selectedOption != null && cornerTypes.includes(selectedOption.value)) {
122 | updateProp('corners', selectedOption.value)
123 | }
124 | }}
125 | />
126 |
127 |
128 |
129 |
130 |
131 |
132 | {
136 | // @ts-expect-error
137 | updateProp('fluid', target.checked)
138 | }}
139 | />
140 |
141 |
142 |
143 |
144 | {
149 | if (selectedOption != null && types.includes(selectedOption.value)) {
150 | updateProp('type', selectedOption.value)
151 | }
152 | }}
153 | />
154 |
155 |
156 |
157 |
158 | {
163 | if (selectedOption != null && protocols.includes(selectedOption.value)) {
164 | updateProp('protocol', selectedOption.value)
165 | }
166 | }}
167 | />
168 |
169 |
170 |
171 |
172 |
173 |
174 | Provider supported:
175 |
176 | {providersList.map(({ entityID, entityName, logo }) =>
177 |
{
178 | // @ts-expect-error
179 | const isChecked = event.target.checked;
180 | if (isChecked) {
181 | updateProp('supported', [...buttonProps.supported, entityID])
182 | } else {
183 | updateProp('supported', buttonProps.supported.filter((id) => entityID !== id))
184 | }
185 | }} />
186 |
187 |
188 |
189 |
190 | )}
191 |
192 |
193 |
194 |
195 |
196 | {
200 | // @ts-expect-error
201 | updateProp('sorted', target.checked)
202 | }}
203 | />
204 |
205 |
206 |
207 | >
208 | }
--------------------------------------------------------------------------------
/example/src/DocTable.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect , useState} from 'react';
2 | import Markdown from 'markdown-to-jsx';
3 | // @ts-expect-error
4 | import {Puff} from 'svg-loaders-react';
5 | import { CodeRenderer, GenericCodeRenderer } from './CodeRenderer';
6 | import { NoFunctionProps } from './constants';
7 |
8 |
9 | const docURL = process.env.PUBLIC_URL + "/doc.md";
10 |
11 | const possibleStates = {
12 | 'init': {state: 'init'},
13 | 'loaded': {state: 'loaded', payload: '' as string},
14 | 'error': {state: 'error'}
15 | } as const;
16 |
17 | type LoadingStates = keyof typeof possibleStates;
18 | type StatesValues = (typeof possibleStates)[LoadingStates]
19 |
20 | const wait = (ms: number) => new Promise(r => setTimeout(r, ms))
21 |
22 | export const DocTable = (buttonProps: NoFunctionProps) => {
23 | const [doc, setDoc] = useState(possibleStates.init);
24 |
25 | useEffect(() => {
26 | Promise.all([
27 | fetch(docURL)
28 | .then((response) => response.text()),
29 | wait(1500)
30 | ])
31 | .then(([markdown]) => setDoc({
32 | ...possibleStates.loaded, payload: markdown
33 | }))
34 | .catch(() => setDoc(possibleStates.error))
35 | }, [setDoc]);
36 |
37 | const npmInstallLine = 'npm install --save @dej611/spid-react-button typeface-titillium-web';
38 | const cssImportLine = '@import url(https://fonts.googleapis.com/css?family=Titillium+Web:400,600,700,900);';
39 | return
40 |
Getting started
41 |
42 |
43 | The package depends on the Titillium font.
44 |
45 | An alternative to installing the local package is to use it via CDN, adding this line to your css file:
46 |
47 |
48 |
49 |
Github repository
50 |
Reference API
51 |
52 | {doc.state === 'init' &&
}
53 | {
54 | doc.state === 'loaded'
55 | ?
{doc.payload}
56 | : null
57 | }
58 | {doc.state === 'error' &&
59 | An error occurred when loading the documentation from the server
60 |
}
61 |
62 | }
--------------------------------------------------------------------------------
/example/src/EventsTable.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const EventsTable = ({ events }: { events: { type: string, name: string, arg?: string }[] }) => {
4 | return
5 |
6 |
7 |
8 | #
9 | Type
10 | Event
11 | Arg
12 |
13 |
14 |
15 | {events.map(({ type, name, arg }, i) =>
16 | {events.length - i}
17 | {type}
18 | {name}
19 | {arg || ''}
20 | )}
21 |
22 |
23 |
24 |
25 | }
--------------------------------------------------------------------------------
/example/src/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Header, HeaderContent, HeaderBrand, HeaderRightZone, HeaderSocialsZone, Icon } from 'design-react-kit';
4 |
5 | export const AppHeader = () => (
8 |
9 |
13 |
14 | SPID React button
15 |
16 |
17 | The React component for the SPID smart button
18 |
19 |
20 |
21 |
22 |
38 |
39 |
40 |
41 | )
--------------------------------------------------------------------------------
/example/src/constants.ts:
--------------------------------------------------------------------------------
1 | import { getShuffledProviders } from "@dej611/spid-react-button";
2 |
3 | import {Protocols, Languages, Sizes, CornerType, ColorTheme, ConfigurationGET, ConfigurationPOST, Types, SPIDButtonProps, getSupportedLanguages} from '@dej611/spid-react-button'
4 |
5 | export const defaultURL = "/myLogin/idp={{idp}}";
6 | export const providersList = [...getShuffledProviders()].sort((idpA, idpB) => idpA.entityName.localeCompare(idpB.entityName));
7 | export const languages: Languages[] = getSupportedLanguages()
8 | export const configurations: [ConfigurationGET, ConfigurationPOST] = [{ method: 'GET' }, { method: 'POST', fieldName: 'prova' }]
9 | export const protocols: Protocols[] = ['SAML', 'OIDC']
10 | export const sizes: Sizes[] = ['sm', 'md', 'l']
11 | export const colorThemes: ColorTheme[] = ['positive', 'negative']
12 | export const cornerTypes: CornerType[] = ['rounded', 'sharp']
13 | export const types: Types[] = ['modal', 'dropdown']
14 |
15 |
16 | export type NoFunctionProps = Required>
17 |
18 | export const initState: NoFunctionProps = {
19 | lang: languages[0],
20 | url: defaultURL,
21 | mapping: {},
22 | supported: providersList.slice(0, 4).map(({entityID}) => entityID),
23 | protocol: protocols[0],
24 | size: sizes[1],
25 | theme: colorThemes[0],
26 | fluid: false,
27 | corners: cornerTypes[0],
28 | configuration: configurations[0],
29 | extraProviders: [],
30 | type: types[0],
31 | sorted: false
32 | }
--------------------------------------------------------------------------------
/example/src/index.css:
--------------------------------------------------------------------------------
1 | #main {
2 | padding-top: 25px;
3 | }
4 |
5 | .tableFixHead {
6 | overflow-y: auto;
7 | height: 500px;
8 | }
9 |
10 | .tableFixHead thead th {
11 | position: sticky;
12 | top: 0;
13 | background-color: #FFF;
14 | }
--------------------------------------------------------------------------------
/example/src/index.tsx:
--------------------------------------------------------------------------------
1 | import 'react-app-polyfill/ie11';
2 | import 'react-app-polyfill/stable';
3 | import './index.css'
4 |
5 | import React from 'react'
6 | import ReactDOM from 'react-dom'
7 | import App from './App'
8 |
9 | ReactDOM.render( , document.getElementById('root'))
10 |
--------------------------------------------------------------------------------
/example/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/example/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "module": "esnext",
5 | "lib": [
6 | "dom",
7 | "esnext"
8 | ],
9 | "moduleResolution": "node",
10 | "jsx": "react",
11 | "sourceMap": true,
12 | "declaration": true,
13 | "esModuleInterop": true,
14 | "noImplicitReturns": true,
15 | "noImplicitThis": true,
16 | "noImplicitAny": true,
17 | "strictNullChecks": true,
18 | "suppressImplicitAnyIndexErrors": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "allowSyntheticDefaultImports": true,
22 | "target": "es5",
23 | "allowJs": false,
24 | "skipLibCheck": true,
25 | "strict": true,
26 | "forceConsistentCasingInFileNames": true,
27 | "noFallthroughCasesInSwitch": true,
28 | "resolveJsonModule": true,
29 | "isolatedModules": true,
30 | "noEmit": true
31 | },
32 | "include": [
33 | "src"
34 | ],
35 | "exclude": [
36 | "node_modules",
37 | "build"
38 | ]
39 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@dej611/spid-react-button",
3 | "version": "0.4.0",
4 | "description": "Pulsante SSO per SPID in React",
5 | "author": "dej611",
6 | "license": "EUPL-1.2",
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/dej611/spid-react-button.git"
10 | },
11 | "main": "dist/index.js",
12 | "module": "dist/index.modern.js",
13 | "sideEffects": false,
14 | "source": "src/index.tsx",
15 | "engines": {
16 | "node": ">=10"
17 | },
18 | "scripts": {
19 | "build:ts": "microbundle-crl --no-compress --format modern,cjs",
20 | "build:svg": "./scripts/svgs.sh",
21 | "build:css": "./scripts/styles.sh",
22 | "build": "npm run build:css && npm run build:svg && npm run build:ts && npm run doc",
23 | "start": "microbundle-crl watch --no-compress --format modern,cjs",
24 | "prepare": "run-s build",
25 | "test": "run-s test:unit test:lint test:build",
26 | "test:build": "run-s build",
27 | "test:lint": "eslint .",
28 | "test:unit": "cross-env CI=1 react-scripts test --env=jsdom --coverage",
29 | "test:watch": "react-scripts test --env=jsdom",
30 | "predeploy": "cd example && npm install && npm run build",
31 | "deploy": "gh-pages -d example/build",
32 | "doc": "typedoc src/index.tsx --excludePrivate --json ./doc.json && node scripts/doc.js",
33 | "providers:check": "node ./scripts/check_providers.js"
34 | },
35 | "peerDependencies": {
36 | "react": ">=16.8.0",
37 | "@types/react": ">=16.8.0",
38 | "typeface-titillium-web": "latest"
39 | },
40 | "peerDependenciesMeta": {
41 | "@types/react": {
42 | "optional": true
43 | },
44 | "typeface-titillium-web": {
45 | "optional": true
46 | }
47 | },
48 | "devDependencies": {
49 | "@testing-library/jest-dom": "5.11.9",
50 | "@testing-library/react": "11.2.5",
51 | "@testing-library/user-event": "13.0.6",
52 | "@types/jest": "^25.1.4",
53 | "@types/jest-axe": "^3.5.1",
54 | "@types/node": "^12.12.38",
55 | "@types/react": "^16.9.27",
56 | "@types/react-dom": "^16.9.7",
57 | "@types/react-transition-group": "^4.4.1",
58 | "@typescript-eslint/eslint-plugin": "^2.26.0",
59 | "@typescript-eslint/parser": "^2.26.0",
60 | "babel-eslint": "^10.0.3",
61 | "cross-env": "^7.0.2",
62 | "eslint": "^6.8.0",
63 | "eslint-config-prettier": "^6.7.0",
64 | "eslint-config-standard": "^14.1.0",
65 | "eslint-config-standard-react": "^9.2.0",
66 | "eslint-plugin-import": "^2.18.2",
67 | "eslint-plugin-node": "^11.0.0",
68 | "eslint-plugin-prettier": "^3.1.1",
69 | "eslint-plugin-promise": "^4.2.1",
70 | "eslint-plugin-react": "^7.17.0",
71 | "eslint-plugin-standard": "^4.0.1",
72 | "fast-xml-parser": "^4.0.11",
73 | "gh-pages": "^2.2.0",
74 | "jest-axe": "^4.1.0",
75 | "microbundle-crl": "^0.13.10",
76 | "node-fetch": "^2.0.0",
77 | "npm-run-all": "^4.1.5",
78 | "prettier": "^2.0.4",
79 | "react": "^16.13.1",
80 | "react-dom": "^16.13.1",
81 | "react-scripts": "^3.4.1",
82 | "spid-smart-button": "1.1.5",
83 | "svgo": "^2.2.2",
84 | "typedoc": "^0.20.34",
85 | "typeface-titillium-web": "latest",
86 | "typescript": "3.9.9"
87 | },
88 | "files": [
89 | "dist"
90 | ],
91 | "bugs": {
92 | "url": "https://github.com/dej611/spid-react-button/issues"
93 | },
94 | "homepage": "https://github.com/dej611/spid-react-button",
95 | "dependencies": {
96 | "react-focus-on": "^3.5.2"
97 | },
98 | "keywords": [
99 | "spid",
100 | "login",
101 | "sso",
102 | "italia.it"
103 | ],
104 | "jest": {
105 | "moduleNameMapper": {
106 | "/../(.*).svg$": "/src/$1.svg",
107 | "/./(.*).svg$": "/src/shared/$1.svg"
108 | }
109 | },
110 | "bundlewatch": {
111 | "trackBranches": [
112 | "main"
113 | ],
114 | "files": [
115 | {
116 | "path": "./dist/index.js",
117 | "maxSize": "8.0 KB"
118 | },
119 | {
120 | "path": "./dist/index.modern.js",
121 | "maxSize": "8.0 KB"
122 | }
123 | ]
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/publiccode.yml:
--------------------------------------------------------------------------------
1 | # This repository adheres to the publiccode.yml standard by including this
2 | # metadata file that makes public software easily discoverable.
3 | # More info at https://github.com/italia/publiccode.yml
4 |
5 | publiccodeYmlVersion: '0.2'
6 | categories:
7 | - it-development
8 | dependsOn:
9 | open:
10 | - name: react
11 | optional: false
12 | version: ''
13 | versionMax: ''
14 | versionMin: 16.8.0
15 | description:
16 | it:
17 | features:
18 | - Internationalization
19 | - Accessibility
20 | genericName: SPID React component
21 | longDescription: |
22 | A React component for the SPID button batteries included: all you need to
23 | configure is how to pass the information to the server, the rest is
24 | generated by the component.
25 |
26 |
27 | The library has been designed to provide the maximum level of
28 | configuration for the button, from styling to the communication layer,
29 | while keeping the footprint as small as possible with full treeshake. The
30 | key drivers of the library are accessibility and quality, providing the
31 | best accessibility faetures with zero configuration from the developer and
32 | a full test coverage of the code base with extensive documentation and
33 | Typescript types.
34 | screenshots:
35 | - |-
36 | https://raw.githubusercontent.com/dej611/spid-react-button/master/scripts/spid-modal.png
37 | - |-
38 | https://raw.githubusercontent.com/dej611/spid-react-button/master/scripts/spid-dropdown.png
39 | shortDescription: A React component for the SPID button
40 | developmentStatus: stable
41 | isBasedOn:
42 | - 'spid-sp-access-button, spid-smart-button'
43 | it:
44 | conforme:
45 | gdpr: false
46 | lineeGuidaDesign: true
47 | misureMinimeSicurezza: false
48 | modelloInteroperabilita: false
49 | countryExtensionVersion: '0.2'
50 | piattaforme:
51 | anpr: false
52 | cie: false
53 | pagopa: false
54 | spid: true
55 | landingURL: 'https://dej611.github.io/spid-react-button/'
56 | legal:
57 | license: EUPL-1.2
58 | localisation:
59 | availableLanguages:
60 | - it
61 | - en
62 | - de
63 | localisationReady: true
64 | maintenance:
65 | contacts:
66 | - name: Marco Liberati
67 | type: community
68 | name: SPID React button
69 | platforms:
70 | - web
71 | releaseDate: '2021-04-21'
72 | softwareType: library
73 | softwareVersion: 0.2.1
74 | url: 'https://github.com/dej611/spid-react-button'
75 |
--------------------------------------------------------------------------------
/scripts/check_providers.js:
--------------------------------------------------------------------------------
1 | const { XMLParser } = require('fast-xml-parser');
2 | const fetch = require('node-fetch');
3 | const providersJSON = require('../src/shared/providers_meta.json');
4 |
5 | const URL = 'https://registry.spid.gov.it/metadata/idp/spid-entities-idps.xml';
6 |
7 | fetch(URL)
8 | .then((res) =>
9 | Promise.all([res.text(), new Set(Object.values(providersJSON))])
10 | )
11 | .then(([xml, currentLookup]) => {
12 | const parser = new XMLParser({ ignoreAttributes: false });
13 | const json = parser.parse(xml);
14 | const entities = json['md:EntitiesDescriptor']['md:EntityDescriptor'].map(
15 | (meta) => meta['@_entityID']
16 | );
17 | // are all remote entities in this repo?
18 | const missingEntities = entities.filter((id) => !currentLookup.has(id));
19 | if (missingEntities.length) {
20 | throw Error(
21 | `There are missing providers in the repo: ${missingEntities.join(', ')}`
22 | );
23 | }
24 | // has some provider been pulled out?
25 | if (entities.length !== currentLookup.size) {
26 | const remoteLookup = new Set(entities);
27 | const pulledEntities = [...currentLookup].filter(
28 | (id) => !remoteLookup.has(id)
29 | );
30 | throw Error(
31 | `The following providers should be removed from the repo: ${pulledEntities.join(
32 | ', '
33 | )}`
34 | );
35 | }
36 | console.log('Providers list up to date!');
37 | });
38 |
--------------------------------------------------------------------------------
/scripts/doc.js:
--------------------------------------------------------------------------------
1 | const { readFile, writeFile } = require('fs').promises;
2 | const path = require('path');
3 | const rootPath = path.resolve(__dirname);
4 |
5 | let json;
6 | try {
7 | json = require('../doc.json');
8 | } catch (e) {
9 | throw Error(
10 | "No doc.json file found. Run 'npm run doc' to make sure the file is in place."
11 | );
12 | }
13 |
14 | function isFunction({ kindString }) {
15 | return kindString === 'Function';
16 | }
17 |
18 | function isInterface({ kindString, type }) {
19 | return (
20 | kindString === 'Interface' ||
21 | (kindString === 'Type alias' &&
22 | type.type === 'reflection' &&
23 | type.declaration.children)
24 | );
25 | }
26 |
27 | function isProperty({ kindString }) {
28 | return kindString === 'Property';
29 | }
30 |
31 | const isUtility = isFunction;
32 |
33 | function isReactComponent({ kindString, signatures }) {
34 | return isFunction({ kindString }) && signatures[0].type.name === 'Element';
35 | }
36 |
37 | function getCommentSection(typeRecord) {
38 | return { shortText: '', tags: [], ...typeRecord.comment };
39 | }
40 |
41 | function getChildren({ kindString, children, type }) {
42 | if (kindString === 'Interface') {
43 | return children;
44 | }
45 | if (
46 | kindString === 'Type alias' &&
47 | type.type === 'reflection' &&
48 | type.declaration.children
49 | ) {
50 | return type.declaration.children;
51 | }
52 | throw Error('No children found');
53 | }
54 |
55 | function typeDescription({ type, ...props }, inline = false) {
56 | if (type === 'literal') {
57 | return '"' + props.value + '"';
58 | }
59 | if (props.kindString === 'Property' || props.kindString === 'Parameter') {
60 | return `${props.name} ${
61 | props.flags.isOptional ? '?' : ''
62 | }: ${typeDescription(type)}`;
63 | }
64 | if (type === 'reference' || type === 'intrinsic') {
65 | if (props.typeArguments) {
66 | return `${props.name}<${props.typeArguments
67 | .map(typeDescription)
68 | .join(' , ')}>`;
69 | }
70 | return props.name;
71 | }
72 | if (type === 'union') {
73 | return props.types.map(typeDescription).join(' | ');
74 | }
75 |
76 | if (type === 'array') {
77 | return `${typeDescription(props.elementType)}[]`;
78 | }
79 |
80 | if (type === 'indexedAccess') {
81 | return `${typeDescription(props.objectType)}[${typeDescription(
82 | props.indexType
83 | )}]`;
84 | }
85 |
86 | if (type === 'reflection') {
87 | if (props.declaration.children) {
88 | // object shaped
89 | return `{
90 | ${props.declaration.children
91 | .map(typeDescription)
92 | .join(inline ? ', ' : '\n')}
93 | }`;
94 | }
95 | if (props.declaration.signatures) {
96 | const [signature] = props.declaration.signatures;
97 | if (!signature.parameters) {
98 | return `() => ${typeDescription(signature.type)}`;
99 | }
100 | // function literal
101 | return `(
102 | ${signature.parameters
103 | .map(typeDescription)
104 | .join(',\n')}) => ${typeDescription(signature.type)}`;
105 | }
106 | }
107 | }
108 |
109 | function unrollType(record, array) {
110 | if (record.type === 'reference') {
111 | const referenced = array.find(({ name }) => name === record.name);
112 | if (referenced) {
113 | const value = typeDescription(
114 | referenced.type ? referenced.type : referenced,
115 | true
116 | );
117 | return value && value.replace(/\n/g, '');
118 | }
119 | }
120 | if (record.type === 'union') {
121 | return record.types
122 | .map((nestedRecord) => unrollType(nestedRecord, array))
123 | .join(' | ');
124 | }
125 | if (record.type === 'array') {
126 | const finalType = unrollType(record.elementType, array);
127 | return finalType ? finalType + '[]' : undefined;
128 | }
129 | if (record.type === 'indexedAccess') {
130 | const referenced = array.find(
131 | ({ name }) => name === record.objectType.name
132 | );
133 | if (referenced) {
134 | const prop = referenced.children.find(
135 | ({ name }) => name === record.indexType.value
136 | );
137 | if (prop) {
138 | return typeDescription(prop).replace(/.* : /, '');
139 | }
140 | }
141 | }
142 | }
143 |
144 | const rewriteRecord = (typeRecord, i, array) => {
145 | if (isReactComponent(typeRecord)) {
146 | return {
147 | type: 'Component',
148 | name: typeRecord.name,
149 | description: getCommentSection(typeRecord.signatures[0]).shortText,
150 | returnType: typeDescription(typeRecord.signatures[0].type),
151 | props: typeRecord.signatures.flatMap(({ parameters }) =>
152 | parameters.map(typeDescription)
153 | )
154 | };
155 | }
156 | if (isUtility(typeRecord)) {
157 | return {
158 | type: typeRecord.kindString,
159 | name: typeRecord.name,
160 | description: typeRecord.signatures
161 | .flatMap(({ comment }) => comment.shortText)
162 | .join('\n'),
163 | returnType: typeDescription(typeRecord.signatures[0].type),
164 | props: typeRecord.signatures
165 | .flatMap(
166 | ({ parameters }) => parameters && parameters.map(typeDescription)
167 | )
168 | .filter(Boolean)
169 | };
170 | }
171 | if (isInterface(typeRecord)) {
172 | return {
173 | type: typeRecord.kindString,
174 | name: typeRecord.name,
175 | description: getCommentSection(typeRecord).shortText,
176 | children: getChildren(typeRecord).map((record) =>
177 | rewriteRecord(record, null, array)
178 | )
179 | };
180 | }
181 | if (isProperty(typeRecord)) {
182 | return {
183 | type: typeRecord.kindString,
184 | name: typeRecord.name,
185 | description: getCommentSection(typeRecord).shortText,
186 | isOptional: typeRecord.flags.isOptional,
187 | unrolledTypes: unrollType(typeRecord.type, array),
188 | valueType: typeDescription(typeRecord.type),
189 | defaultValue: getCommentSection(typeRecord)
190 | .tags.filter(({ tag }) => tag === 'defaultvalue')
191 | .map(({ text }) => text),
192 | paramsDescription: getCommentSection(typeRecord)
193 | .tags.filter(({ tag }) => tag === 'param')
194 | .map(({ param, text }) => `\`${param}\`: ${text}`)
195 | };
196 | }
197 | return {
198 | type: typeRecord.kindString,
199 | name: typeRecord.name,
200 | description: getCommentSection(typeRecord).shortText,
201 | valueType: typeDescription(typeRecord.type)
202 | };
203 | };
204 |
205 | const types = json.children.flatMap(rewriteRecord);
206 |
207 | readFile(path.normalize(rootPath + '/readme.template'), {
208 | encoding: 'UTF8'
209 | })
210 | .then((readme) => {
211 | const SEPARATOR = '\n\n___\n\n';
212 | const markdown = `
213 | ## Components
214 |
215 | ${types
216 | .filter(({ type }) => type === 'Component')
217 | .map(({ name, ...props }, i) => {
218 | const param = props.props[0]
219 | .replace('__namedParameters : ', '')
220 | .replace('props : ', '');
221 | const { children } = types.find(({ name }) => name === param);
222 | return `### ${name}
223 |
224 | **Type**: \`${props.type}\` \n
225 | **Props**: \`${param}\` \n
226 | ${props.description.replace(/\n/g, ' \n')}
227 |
228 |
229 | ${
230 | i > 0
231 | ? ''
232 | : `The ${param} object contains the following properties:
233 |
234 | ${children
235 | .map(
236 | ({
237 | name,
238 | valueType,
239 | defaultValue,
240 | description,
241 | isOptional,
242 | unrolledTypes,
243 | paramsDescription
244 | }) => {
245 | return `#### ${name}
246 |
247 | ${
248 | unrolledTypes
249 | ? `**Possible values**: \`${unrolledTypes}\` \n`
250 | : ''
251 | }
252 | **Type**: \`${valueType}\` \n
253 | **Required**: ${isOptional ? 'No' : 'Yes'} \n
254 | ${
255 | defaultValue && defaultValue.length
256 | ? `**Default value**: ${defaultValue[0]}`
257 | : ''
258 | } \n
259 | ${description.replace(/\n/g, ' \n')}
260 | ${
261 | paramsDescription.length
262 | ? '\n' +
263 | paramsDescription
264 | .map((line) => `* ${line}`)
265 | .join(' \n')
266 | : ''
267 | }
268 | `;
269 | }
270 | )
271 | .join('\n\n')}`
272 | }
273 | `;
274 | })
275 | .join(`\n\n${SEPARATOR}`)}
276 | ${SEPARATOR}
277 | ## Types
278 |
279 | ${types
280 | .filter(({ name, type }) => !/Props$/.test(name) && /Type/.test(type))
281 | .map(({ name, valueType, description, children }) => {
282 | if (children) {
283 | return `**${name}**: \`{${children
284 | .map(({ name, valueType }) => `${name}: ${valueType}`)
285 | .join(', ')}}\` \n
286 | ${description.replace(/\n/g, ' \n')}`;
287 | }
288 | return `**${name}**: \`${valueType}\` \n
289 | ${description.replace(/\n/g, ' \n')}`;
290 | })
291 | .join(SEPARATOR)}
292 | ${SEPARATOR}
293 | ${types
294 | .filter(({ type }) => type === 'Interface')
295 | .map(({ name, description, children }) => {
296 | return `**${name}**\n
297 | ${description.replace(/\n/g, ' \n')}
298 |
299 | ${children
300 | .map(({ name, description, isOptional, valueType }) => {
301 | return `* ${name}: \`${valueType}\`${
302 | isOptional ? ' - Optional' : ''
303 | } \n
304 | ${description.replace(/\n/g, ' \n')}`;
305 | })
306 | .join('\n')}`;
307 | })
308 | .join(SEPARATOR)}
309 | ${SEPARATOR}
310 | ## Utilites
311 |
312 | ${types
313 | .filter(({ type }) => type === 'Function')
314 | .map(({ name, description, returnType, props }) => {
315 | return `#### ${name}\n
316 |
317 | \`${name}(${props.length ? props.join('`, `') : ''}) => ${returnType}\`
318 |
319 | ${description.replace(/\n/g, ' \n')}
320 | `;
321 | })
322 | .join('\n')}
323 | `
324 | .split('\n')
325 | .map((line) => line.trimStart())
326 | .join('\n');
327 | const result = readme.replace('{{api}}', markdown);
328 |
329 | return Promise.all([
330 | writeFile(path.normalize(rootPath + '/../README.md'), result, {
331 | encoding: 'UTF8'
332 | }),
333 | writeFile(
334 | path.normalize(rootPath + '/../example/public/doc.md'),
335 | markdown,
336 | {
337 | encoding: 'UTF8'
338 | }
339 | )
340 | ]);
341 | })
342 | .then(() => console.log('All done!'));
343 |
--------------------------------------------------------------------------------
/scripts/readme.template:
--------------------------------------------------------------------------------
1 | # spid-react-button
2 |
3 | > Pulsante SSO per SPID in React
4 |
5 | [](https://www.npmjs.com/package/@dej611/spid-react-button)    [](https://app.fossa.com/projects/custom%2B24373%2Fgit%40github.com%3Adej611%2Fspid-react-button.git?ref=badge_shield)[](https://codeclimate.com/github/dej611/spid-react-button/maintainability)[](https://codeclimate.com/github/dej611/spid-react-button/test_coverage)[](https://github.com/dej611/spid-react-button/actions/workflows/test.yml)
6 |
7 | [Demo here](https://dej611.github.io/spid-react-button/)
8 |
9 |
10 | | Modal | Dropdown |
11 | | ------------- | ------------- |
12 | | | |
13 |
14 | ## Install
15 |
16 | ```bash
17 | npm install --save @dej611/spid-react-button typeface-titillium-web
18 | ```
19 |
20 | The package depends on the Titillium font.
21 | An alternative to installing the local package is to use it via CDN, adding this line to your css file:
22 | ```css
23 | @import url(https://fonts.googleapis.com/css?family=Titillium+Web:400,600,700,900);
24 | ```
25 |
26 | ## Usage
27 |
28 | ```jsx
29 | import React from 'react'
30 | // Import it via package or in your CSS file via the CDN @import
31 | import 'typeface-titillium-web';
32 | import {SPIDReactButton} from '@dej611/spid-react-button'
33 |
34 | import '@dej611/spid-react-button/dist/index.css'
35 |
36 |
37 | function Example(){
38 | return
39 | }
40 | ```
41 |
42 | **Note**: the providers list has no particular sorting order and is shuffled at every page load.
43 |
44 | #### Accessibility notes
45 |
46 | In general using the `POST` version of this component makes it usable for the most wide audience, as the `GET` version uses links which may have issues. To know more read below.
47 |
48 | The component tries its best to provide the best a11y practices implemented, but there are some specific browser behaviours that may still keep it hard to use with keyboard.
49 | In particular in OSX Safari and Firefox have some issues with `focus` and the behaviour of `tab`: [please read about it](https://github.com/theKashey/react-focus-lock#focusing-in-osx-safarifirefox-is-strange) when using this component.
50 |
51 |
52 | #### Contributing
53 |
54 | To get up and running with the development of the project you can follow these steps.
55 |
56 | To see the component in action, useful to test it manually after some changes, open a terminal window and type:
57 |
58 | ```sh
59 | cd example
60 | npm start
61 | ```
62 |
63 | This will start the `create-react-app` project and open a new browser tab with the example application in it.
64 |
65 | At this point it is possible to perform changes to the component/library, open a new terminal in the project folder and use this command when done with changes:
66 | ```sh
67 | npm build
68 | ```
69 |
70 | To run the test do:
71 |
72 | ```sh
73 | npm test
74 | ```
75 |
76 | ### Change Readme file
77 |
78 | The readme file is autogenerated from a template located in `script/readme.template`: the template contains everything but the API which is generated from the Typescript source.
79 | No need to touch the API part (which will be overwritten anyway).
80 |
81 | ## Next.js
82 |
83 | Next.js 11 has some issues with the imported SVGs in this package, therefore it requires some configuration tweak and some additional dependencies to make it work correctly.
84 |
85 | First step, install some additional dependencies:
86 |
87 | ```sh
88 | npm i @svgr/webpack url-loader next-transpile-modules --save-dev
89 | ```
90 |
91 | These three dependencies should be added to the Next.js configuration to enable SVG import as URLs (used in this package for the buttons.)
92 |
93 | ```js
94 | // next.config.js
95 | const withTM = require('next-transpile-modules')(['@dej611/spid-react-button']);
96 |
97 | module.exports = withTM({
98 | reactStrictMode: true,
99 | webpack(config) {
100 | const fileLoaderRule = config.module.rules.find(rule => rule.test && rule.test.test('.svg'))
101 | fileLoaderRule.exclude = /\.svg$/
102 | config.module.rules.push({
103 | test: /\.svg$/,
104 | loader: require.resolve('@svgr/webpack')
105 | },
106 | {
107 | test: /\.svg$/,
108 | loader: require.resolve('url-loader')
109 | })
110 | return config
111 | }
112 | })
113 | ```
114 |
115 | # API
116 |
117 | {{api}}
118 |
119 | ## License
120 |
121 | EUPL 1.2 © [dej611](https://github.com/dej611)
122 |
123 | [](https://app.fossa.com/projects/custom%2B24373%2Fgit%40github.com%3Adej611%2Fspid-react-button.git?ref=badge_large)
--------------------------------------------------------------------------------
/scripts/spid-dropdown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dej611/spid-react-button/15701aa32c24a11f76e831cae6d36604b5283c3d/scripts/spid-dropdown.png
--------------------------------------------------------------------------------
/scripts/spid-modal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dej611/spid-react-button/15701aa32c24a11f76e831cae6d36604b5283c3d/scripts/spid-modal.png
--------------------------------------------------------------------------------
/scripts/styles.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | cp ./node_modules/spid-smart-button/dist/spid-button.min.css ./src/modalVariant/index.module.css
4 | if [ $(uname) = 'Linux' ]; then
5 | sed -i '/@import url/d' ./src/modalVariant/index.module.css
6 | else
7 | sed -i '' '/@import url/d' ./src/modalVariant/index.module.css
8 | fi
--------------------------------------------------------------------------------
/scripts/svgs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | mkdir /tmp/idp-svgs
3 | curl -L "https://github.com/italia/spid-sp-access-button/archive/master.tar.gz" | tar -C /tmp/idp-svgs --strip-components=4 -zxf - spid-sp-access-button-master/src/production/img/spid-idp*.svg
4 |
5 | npx svgo -f node_modules/spid-smart-button/dist/img/ -o src/shared/svgs
6 | npx svgo -f /tmp/idp-svgs -o src/shared/svgs/idp-logos
7 |
8 | rm -r /tmp/idp-svgs
9 |
--------------------------------------------------------------------------------
/src/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/a11y.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import { axe, toHaveNoViolations } from 'jest-axe';
4 | import '@testing-library/jest-dom/extend-expect';
5 | import { SPIDReactButton } from './component';
6 | import {
7 | types,
8 | defaultURL,
9 | TestHelper,
10 | configurations,
11 | createHelper
12 | } from './test/utils';
13 |
14 | expect.extend(toHaveNoViolations);
15 |
16 | describe('A11y tests', () => {
17 | for (const type of types) {
18 | describe(`[${type.toUpperCase()}] Full component`, () => {
19 | describe(`[${type.toUpperCase()}] rendering button`, () => {
20 | it('should pass basic a11y checks for the button', async () => {
21 | const { container } = render(
22 |
23 | );
24 |
25 | const result = await axe(container);
26 | expect(result).toHaveNoViolations();
27 | });
28 | });
29 | });
30 |
31 | for (const configuration of configurations) {
32 | describe(`[${type.toUpperCase()}] [${
33 | configuration.method
34 | }] button list`, () => {
35 | let helper: TestHelper;
36 |
37 | beforeAll(() => {
38 | helper = createHelper(type, configuration.method);
39 | });
40 |
41 | it(`should render all providers`, async () => {
42 | const { container } = render(
43 |
48 | );
49 |
50 | helper.openList();
51 |
52 | const result = await axe(container);
53 | expect(result).toHaveNoViolations();
54 | });
55 | });
56 | }
57 | }
58 | });
59 |
--------------------------------------------------------------------------------
/src/component.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, fireEvent, screen } from '@testing-library/react';
3 | import '@testing-library/jest-dom/extend-expect';
4 | import { providers } from './shared/providers';
5 | import { SPIDReactButton } from './component';
6 | import {
7 | types,
8 | configurations,
9 | createHelper,
10 | defaultURL,
11 | TestHelper,
12 | muteConsoleWithCheck
13 | } from './test/utils';
14 |
15 | for (const type of types) {
16 | describe(`[${type.toUpperCase()}] Full component`, () => {
17 | describe(`[${type.toUpperCase()}] rendering button`, () => {
18 | const buttonRole = type === 'dropdown' ? 'link' : 'button';
19 | it('should render the button and no modal', () => {
20 | render( );
21 |
22 | expect(screen.getByRole(buttonRole)).toHaveTextContent(
23 | 'Entra con SPID'
24 | );
25 | expect(screen.queryByRole('header')).toBeNull();
26 | });
27 |
28 | it('should render with the correct language', () => {
29 | render( );
30 |
31 | expect(screen.getByRole(buttonRole)).toHaveTextContent(
32 | 'Sign in with SPID'
33 | );
34 | });
35 |
36 | it('should throw if a wrong URL is passed', () => {
37 | muteConsoleWithCheck(() => {
38 | expect(() =>
39 | render( )
40 | ).toThrow();
41 | });
42 | });
43 |
44 | if (type === 'modal') {
45 | it('should render the modal on button click', () => {
46 | render( );
47 | fireEvent.click(screen.getByRole(buttonRole));
48 |
49 | expect(screen.getByRole('heading')).toHaveTextContent(
50 | 'Scegli il tuo provider SPID'
51 | );
52 | });
53 | }
54 | });
55 |
56 | for (const configuration of configurations) {
57 | describe(`[${type.toUpperCase()}] [${
58 | configuration.method
59 | }] button`, () => {
60 | let helper: TestHelper;
61 |
62 | beforeAll(() => {
63 | helper = createHelper(type, configuration.method);
64 | });
65 |
66 | it(`should render all providers`, () => {
67 | render(
68 |
73 | );
74 |
75 | helper.openList();
76 |
77 | expect(helper.getProviders()).toHaveLength(providers.length);
78 | });
79 |
80 | it(`should enable only supported providers`, () => {
81 | render(
82 |
88 | );
89 | helper.openList();
90 |
91 | expect(helper.getEnabledProviders()).toHaveLength(1);
92 | });
93 |
94 | it(`should disable all official providers if any extra is passed`, () => {
95 | render(
96 |
104 | );
105 | helper.openList();
106 |
107 | expect(helper.getEnabledProviders()).toHaveLength(1);
108 |
109 | expect(helper.getDisabledProviders()).toHaveLength(providers.length);
110 | });
111 |
112 | it(`should call all the handlers passed`, () => {
113 | const onShowCallback = jest.fn();
114 | const onHideCallback = jest.fn();
115 | const onClickCallback = jest.fn();
116 | render(
117 |
125 | );
126 | helper.openList();
127 |
128 | const [firstProviderAvailable] = helper.getEnabledProviders();
129 |
130 | helper.clickProvider(firstProviderAvailable);
131 |
132 | helper.closeList();
133 | const providerID = firstProviderAvailable.id?.trim();
134 | const provider = providers.find(
135 | ({ entityID }) => providerID === entityID
136 | );
137 |
138 | expect(onShowCallback).toHaveBeenCalledTimes(1);
139 | expect(onHideCallback).toHaveBeenCalledTimes(1);
140 | expect(onClickCallback).toHaveBeenCalledTimes(1);
141 | expect(onClickCallback).toHaveBeenCalledWith(
142 | provider,
143 | expect.stringContaining('/myLogin/idp'),
144 | // React mouse event
145 | expect.anything()
146 | );
147 | });
148 |
149 | it('should have the custom id for each clicked IDP', () => {
150 | const onClickCallback = jest.fn();
151 | const customMapping = providers.reduce((memo, { entityID }, i) => {
152 | memo[entityID] = i;
153 | return memo;
154 | }, {});
155 | render(
156 |
163 | );
164 | helper.openList();
165 |
166 | const [firstProviderAvailable] = helper.getEnabledProviders();
167 |
168 | helper.clickProvider(firstProviderAvailable);
169 |
170 | helper.closeList();
171 | const indexID = firstProviderAvailable.id?.trim();
172 | const provider = providers[indexID]!;
173 |
174 | expect(onClickCallback).toHaveBeenCalledWith(
175 | provider,
176 | '/myLogin/idp=' +
177 | encodeURIComponent(customMapping[provider.entityID]),
178 | // React mouse event
179 | expect.anything()
180 | );
181 | });
182 | });
183 | }
184 | });
185 | }
186 |
--------------------------------------------------------------------------------
/src/component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { SPIDReactButton as SPIDReactButtonModal } from './modalVariant';
4 | import { SPIDReactButton as SPIDReactButtonDropdown } from './dropdownVariant';
5 | import type { SPIDButtonProps } from './shared/types';
6 | /**
7 | * The main component for the button.
8 | * Use this component with the `type` prop to select the version you prefer.
9 | * @param props
10 | */
11 | export const SPIDReactButton = (props: SPIDButtonProps) => {
12 | if (!props.type || props.type === 'modal') {
13 | return ;
14 | }
15 | return ;
16 | };
17 |
--------------------------------------------------------------------------------
/src/dropdownVariant/ProvidersMenu.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { isProviderActive, SPID_URL } from '../shared/utils';
4 | import { getIdpButtonSizeClass } from './util';
5 |
6 | import type { TranslateFn } from '../shared/i18n';
7 | import type {
8 | RegisteredProviderRecord,
9 | SPIDButtonProps
10 | } from '../shared/types';
11 |
12 | import styles from './index.module.css';
13 | import { SharedProviderButton } from '../shared/ProviderButton';
14 |
15 | type ProvidersDropdownProps = Required<
16 | Pick<
17 | SPIDButtonProps,
18 | | 'url'
19 | | 'supported'
20 | | 'mapping'
21 | | 'size'
22 | | 'configuration'
23 | | 'protocol'
24 | | 'extraProviders'
25 | >
26 | > &
27 | Pick & {
28 | i18n: TranslateFn;
29 | providers: RegisteredProviderRecord[];
30 | };
31 | export const ProvidersDropdown = ({
32 | configuration,
33 | supported,
34 | url,
35 | mapping,
36 | i18n,
37 | size,
38 | protocol,
39 | providers,
40 | extraProviders,
41 | onProviderClicked
42 | }: ProvidersDropdownProps) => {
43 | return (
44 |
50 |
54 | {providers.map((idp, i) => {
55 | const isActive = isProviderActive(
56 | idp,
57 | supported,
58 | protocol,
59 | extraProviders
60 | );
61 |
62 | const buttonClasses = `${styles.idpLogo} ${
63 | isActive ? '' : styles.disabled
64 | }`;
65 | return (
66 |
67 |
79 |
80 | );
81 | })}
82 |
83 |
84 | {i18n('maggiori_info')}
85 |
86 |
87 |
88 |
89 | {i18n('non_hai_SPID')}
90 |
91 |
92 |
93 |
94 | );
95 | };
96 |
--------------------------------------------------------------------------------
/src/dropdownVariant/constants.ts:
--------------------------------------------------------------------------------
1 | export const sizeMapping = {
2 | sm: 'small',
3 | md: 'medium',
4 | l: 'large',
5 | xl: 'extraLarge'
6 | } as const;
7 |
--------------------------------------------------------------------------------
/src/dropdownVariant/index.module.css:
--------------------------------------------------------------------------------
1 | /**
2 | * The content of this file is derived from: https://github.com/italia/spid-sp-access-button/blob/master/src/production/css/spid-sp-access-button.min.css
3 | * All original copyrights, credits and license apply here as: https://github.com/italia/spid-sp-access-button/blob/master/LICENSE.md
4 | */
5 |
6 | @import url(https://fonts.googleapis.com/css?family=Titillium+Web:400,600);
7 | :root {
8 | --text-color-positive: #FFF;
9 | --text-color-negative: #06C;
10 | --background-color-positive: #06C;
11 | --background-color-negative: #FFF;
12 | --color-darker: #036;
13 | --color-lighter: #83BEED;
14 | }
15 |
16 | .container {
17 | position: relative;
18 | display: inline-block;
19 | }
20 |
21 | .button {
22 | display: inline-block;
23 | position: relative;
24 | padding: 0;
25 | color: var(--text-color-positive);
26 | font-family: "Titillium Web", HelveticaNeue, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif;
27 | font-weight: 600;
28 | line-height: 1em;
29 | text-decoration: none;
30 | border: 0;
31 | text-align: center;
32 | cursor: pointer;
33 | overflow: hidden
34 | }
35 |
36 | .buttonIcon, .buttonText {
37 | display: block;
38 | float: left;
39 | }
40 |
41 | .buttonIcon {
42 | margin: 0 -.4em 0 0;
43 | padding: 0.6em .8em .5em;
44 | border-right: rgba(255, 255, 255, 0.1) 0.1em solid;
45 | }
46 |
47 | .buttonIcon>img {
48 | margin-bottom: 0rem !important;
49 | }
50 |
51 | .buttonText {
52 | padding: .95em 1em .85em 1em;
53 | font-size: 1.15em;
54 | text-align: center;
55 | }
56 |
57 | .theme .buttonText {
58 | color: var(--text-color-positive);
59 | }
60 |
61 | .themeNegative.buttonText {
62 | color: var(--text-color-negative);
63 | }
64 |
65 | .button svg {
66 | width: 1.8em;
67 | height: 1.8em;
68 | }
69 |
70 | .block {
71 | display: block
72 | }
73 |
74 | /**
75 | * Width of this class has been relaxed to fix #61
76 | * It differs from the original style
77 | */
78 | .small {
79 | font-size: 10px;
80 | min-width: 150px
81 | }
82 |
83 | .small>span img {
84 | width: 19px;
85 | height: 19px;
86 | border: 0;
87 | }
88 | /**
89 | * Width of this class has been relaxed to fix #61
90 | * It differs from the original style
91 | */
92 | .medium {
93 | font-size: 15px;
94 | min-width: 220px
95 | }
96 |
97 | .medium>span img {
98 | width: 29px;
99 | height: 29px;
100 | border: 0
101 | }
102 | /**
103 | * Width of this class has been relaxed to fix #61
104 | * It differs from the original style
105 | */
106 | .large {
107 | font-size: 20px;
108 | min-width: 280px
109 | }
110 |
111 | .large>span img {
112 | width: 38px;
113 | height: 38px;
114 | border: 0;
115 | }
116 |
117 | .extraLarge {
118 | font-size: 25px;
119 | width: 340px
120 | }
121 |
122 | .extraLarge>span img {
123 | width: 47px;
124 | height: 47px;
125 | border: 0;
126 | }
127 |
128 | .theme {
129 | background-color: var(--background-color-positive);
130 | color: var(--text-color-positive);
131 | }
132 |
133 | .theme:hover {
134 | background-color: var(--color-darker);
135 | color: var(--text-color-positive);
136 | }
137 |
138 | .theme:active {
139 | background-color: var(--color-lighter);
140 | color: var(--color-darker)
141 | }
142 |
143 | .themeNegative {
144 | color: var(--text-color-negative);
145 | background-color: var(--background-color-negative);
146 | }
147 |
148 | .themeNegative:hover {
149 | color: var(--color-darker);
150 | background-color: var(--background-color-negative);
151 | }
152 |
153 | .themeNegative:active {
154 | color: var(--color-lighter);
155 | background-color: var(--color-darker)
156 | }
157 |
158 | .idpButton {
159 | position: absolute;
160 | z-index: 1039;
161 | top: 100%;
162 | left: 0
163 | }
164 |
165 | .idpButton .idpButtonMenu, .idpButton .idpButtonPanel {
166 | list-style: none;
167 | background: white;
168 | border: solid 1px #ddd;
169 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
170 | overflow: visible;
171 | padding: 0;
172 | margin: 0
173 | }
174 |
175 | .idpButtonSmall, .idpButtonMedium {
176 | width: 230px;
177 | }
178 |
179 | .idpButtonLarge {
180 | width: 270px;
181 | }
182 |
183 | .idpButtonExtraLarge {
184 | width: 330px;
185 | }
186 |
187 | .idpButton .idpButtonPanel {
188 | padding: 10px;
189 | }
190 |
191 | .idpButton.idpButtonTip {
192 | margin-top: 8px;
193 | }
194 |
195 | .idpButton.idpButtonTip:before {
196 | position: absolute;
197 | content: "";
198 | border-left: 7px solid transparent;
199 | border-right: 7px solid transparent;
200 | border-bottom: 7px solid #ddd;
201 | display: inline-block;
202 | top: -6px;
203 | left: 9px;
204 | }
205 |
206 | .idpButton.idpButtonTip:after {
207 | position: absolute;
208 | content: "";
209 | border-left: 6px solid transparent;
210 | border-right: 6px solid transparent;
211 | border-bottom: 6px solid #fff;
212 | display: inline-block;
213 | top: -5px;
214 | left: 10px;
215 | }
216 |
217 | .idpButton.idpButtonTip.idpButtonTipAnchorRight:before {
218 | left: auto;
219 | right: 9px
220 | }
221 |
222 | .idpButton.idpButtonTip.idpButtonTipAnchorRight:after {
223 | left: auto;
224 | right: 10px
225 | }
226 |
227 | .idpButton.idpButtonScroll .idpButtonMenu, .idpButton.idpButtonScroll .idpButtonPanel {
228 | max-height: 180px;
229 | overflow: auto
230 | }
231 |
232 | .idpButton .idpButtonMenu li {
233 | list-style: none;
234 | padding: 0 0;
235 | margin: 0;
236 | line-height: 18px
237 | }
238 |
239 | .idpButton .idpButtonMenu li>a, .idpButton .idpButtonMenu label {
240 | display: block;
241 | font-family: "Titillium Web", HelveticaNeue, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif;
242 | font-weight: 600;
243 | font-size: .9em;
244 | color: #06C;
245 | text-decoration: underline;
246 | line-height: 18px;
247 | padding-top: 5px;
248 | white-space: nowrap;
249 | border-bottom: 1px solid #ddd
250 | }
251 |
252 | .idpButton .idpButtonMenu li>a:hover, .idpButton .idpButtonMenu label:hover {
253 | color: var(--color-darker);
254 | cursor: pointer;
255 | background-color: #F0F0F0
256 | }
257 |
258 | .idpLogo {
259 | font-size: 100%;
260 | height: 10%;
261 | width: 100%;
262 | border: 0;
263 | border-bottom: 1px solid #CCC;
264 | background-color: #FFF;
265 | padding: 15px !important;
266 | text-align: left;
267 | cursor: pointer
268 | }
269 |
270 | .idpLogo:hover {
271 | background-color: #F0F0F0
272 | }
273 |
274 | .idpLogo img {
275 | height: 25px;
276 | vertical-align: middle;
277 | cursor: pointer
278 | }
279 |
280 | .topMenuSpace10>a img {
281 | margin-top: 10px;
282 | }
283 |
284 | .topMenuSpace20>a img {
285 | margin-top: 20px;
286 | }
287 |
288 | .topMenuLine {
289 | border-top: 5px solid #000
290 | }
291 |
292 | .supportLink>a {
293 | padding: 5px 0 10px 10px
294 | }
295 |
296 | .disabled {
297 | color: #aaa;
298 | opacity: 0.2;
299 | cursor: not-allowed;
300 | pointer-events: none;
301 | }
302 |
--------------------------------------------------------------------------------
/src/dropdownVariant/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { FocusOn } from 'react-focus-on';
3 |
4 | import SpidIcoCircleBbUrl from '/../shared/svgs/spid-ico-circle-bb.svg';
5 | import SpidIcoCircleLbUrl from '/../shared/svgs/spid-ico-circle-lb.svg';
6 | import { getTranslationFn } from '../shared/i18n';
7 | import { SPIDButtonProps } from '../shared/types';
8 | import {
9 | validateURL,
10 | getShuffledProviders,
11 | mergeProviders
12 | } from '../shared/utils';
13 |
14 | import styles from './index.module.css';
15 | import { ProvidersDropdown } from './ProvidersMenu';
16 | import { getButtonSizeClass } from './util';
17 |
18 | const shuffledProviders = getShuffledProviders();
19 | /**
20 | * The specific component button with the dropdown.
21 | * Use this component when you want to minimize the footprint in your project.
22 | * It accepts the same props as the main component. The `type` prop is ignored in this case.
23 | *
24 | * @param props
25 | */
26 | export const SPIDReactButton = ({
27 | url,
28 | lang = 'it',
29 | supported = shuffledProviders.map(({ entityID }) => entityID),
30 | mapping = {},
31 | size = 'md',
32 | configuration = { method: 'GET' },
33 | theme = 'positive',
34 | protocol = 'SAML',
35 | sorted = false,
36 | extraProviders = [],
37 | onProviderClicked,
38 | onProvidersHidden,
39 | onProvidersShown
40 | }: SPIDButtonProps) => {
41 | const [openDropdown, toggleDropdown] = useState(
42 | undefined
43 | );
44 |
45 | const i18n = getTranslationFn(lang);
46 |
47 | useEffect(() => {
48 | if (openDropdown && onProvidersShown) {
49 | onProvidersShown();
50 | }
51 | if (openDropdown === false && onProvidersHidden) {
52 | onProvidersHidden();
53 | }
54 | }, [openDropdown]);
55 |
56 | validateURL(url);
57 |
58 | const mergedProviders = mergeProviders(shuffledProviders, extraProviders, {
59 | sorted
60 | });
61 |
62 | const buttonImageUrl =
63 | theme === 'negative' ? SpidIcoCircleLbUrl : SpidIcoCircleBbUrl;
64 |
65 | return (
66 | toggleDropdown(false)}
68 | onEscapeKey={() => toggleDropdown(false)}
69 | scrollLock={false}
70 | enabled={openDropdown}
71 | >
72 |
102 |
103 | );
104 | };
105 |
--------------------------------------------------------------------------------
/src/dropdownVariant/util.tsx:
--------------------------------------------------------------------------------
1 | import { sizeMapping } from './constants';
2 |
3 | import type { Sizes } from '../shared/types';
4 |
5 | export const getButtonSizeClass = (size: Sizes | 'xl') => {
6 | return sizeMapping[size];
7 | };
8 |
9 | export const getIdpButtonSizeClass = (size: Sizes | 'xl') => {
10 | const currentSize = sizeMapping[size];
11 | return 'idpButton' + currentSize[0].toUpperCase() + currentSize.slice(1);
12 | };
13 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | // export the list of official providers
2 | export {
3 | getShuffledProviders,
4 | providersCopy as providers
5 | } from './shared/utils';
6 |
7 | // export a helper to get the list of supported languages
8 | export { getSupportedLanguages } from './shared/i18n';
9 |
10 | // export the types
11 | export type {
12 | SPIDButtonProps,
13 | Protocols,
14 | ProviderRecord,
15 | Languages,
16 | Sizes,
17 | ColorTheme,
18 | CornerType,
19 | ConfigurationGET,
20 | ConfigurationPOST,
21 | Types
22 | } from './shared/types';
23 |
24 | export { SPIDReactButton } from './component';
25 | export { SPIDReactButton as SPIDReactButtonModal } from './modalVariant';
26 | export { SPIDReactButton as SPIDReactButtonDropdown } from './dropdownVariant';
27 |
--------------------------------------------------------------------------------
/src/modalVariant/ProvidersModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FocusOn } from 'react-focus-on';
3 |
4 | import SpidLogoAnimationBlackUrl from '/../shared/svgs/spid-logo-animation-black.svg';
5 | import SpidLogoUrl from '/../shared/svgs/spid-logo.svg';
6 | import CloseSvgUrl from '/../shared/svgs/close.svg';
7 |
8 | import type {
9 | RegisteredProviderRecord,
10 | SPIDButtonProps
11 | } from '../shared/types';
12 |
13 | import {
14 | BUTTON_DELAY_TIME,
15 | DELAY_STEP,
16 | emptyClass,
17 | logoAnimationOutClass,
18 | panelAnimClass,
19 | possibleStates
20 | } from './constants';
21 | import type { ModalState } from './types';
22 | import { isProviderActive, SPID_URL } from '../shared/utils';
23 | import { isVisible, getDefinedClasses } from './utils';
24 | import { TranslateFn } from '../shared/i18n';
25 |
26 | import { SharedProviderButton } from '../shared/ProviderButton';
27 |
28 | const ButtonImage = ({ url, altText }: { url: string; altText: string }) => (
29 |
30 | );
31 |
32 | function getModalClasses({ type }: ModalState) {
33 | const fadeInLeftClass = 'spid-button-fade-in-left';
34 | switch (type) {
35 | case possibleStates.ENTERING.type:
36 | return {
37 | panel: panelAnimClass,
38 | buttonLogo: fadeInLeftClass,
39 | buttonClose: fadeInLeftClass
40 | };
41 | case possibleStates.EXITING.type:
42 | return {
43 | panel: panelAnimClass,
44 | buttonManIcon: logoAnimationOutClass
45 | };
46 | case possibleStates.ENTERED.type:
47 | case possibleStates.EXITED.type:
48 | case possibleStates.INIT.type: {
49 | return {};
50 | }
51 | }
52 | }
53 |
54 | type ProvidersModalProps = Required<
55 | Pick<
56 | SPIDButtonProps,
57 | | 'supported'
58 | | 'protocol'
59 | | 'url'
60 | | 'mapping'
61 | | 'configuration'
62 | | 'extraProviders'
63 | >
64 | > &
65 | Pick & {
66 | i18n: TranslateFn;
67 | visibility: ModalState;
68 | providers: RegisteredProviderRecord[];
69 | closeModal: () => void;
70 | };
71 | export const ProvidersModal = ({
72 | i18n,
73 | mapping,
74 | visibility,
75 | extraProviders = [],
76 | supported,
77 | providers,
78 | protocol,
79 | url,
80 | closeModal,
81 | configuration,
82 | onProviderClicked
83 | }: ProvidersModalProps) => {
84 | const {
85 | panel: panelClasses,
86 | buttonClose: buttonCloseClasses,
87 | buttonLogo: buttonLogoClasses,
88 | buttonManIcon: buttonManIconClasses
89 | } = getModalClasses(visibility);
90 |
91 | return (
92 |
97 |
102 |
103 |
112 |
113 |
114 |
120 |
124 |
125 |
132 |
141 |
145 |
146 |
147 |
148 |
149 |
150 |
158 |
163 |
170 | {i18n('scegli_provider_SPID')}
171 |
172 |
173 | {providers.map((idp, i) => {
174 | const isActive = isProviderActive(
175 | idp,
176 | supported,
177 | protocol,
178 | extraProviders
179 | );
180 | const {
181 | classNames,
182 | style
183 | }: {
184 | classNames: string;
185 | style?: Record;
186 | } =
187 | visibility.type === possibleStates.ENTERING.type
188 | ? {
189 | classNames: 'spid-button-idp-fade-in',
190 | style: {
191 | animationDelay: `${
192 | BUTTON_DELAY_TIME + DELAY_STEP * (i + 1)
193 | }s`
194 | }
195 | }
196 | : { classNames: emptyClass };
197 |
198 | return (
199 |
207 |
221 |
222 | );
223 | })}
224 |
225 |
234 |
235 |
236 |
240 | {i18n('annulla_accesso')}
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 | );
249 | };
250 |
--------------------------------------------------------------------------------
/src/modalVariant/constants.tsx:
--------------------------------------------------------------------------------
1 | export const DEFAULT_TRANSITION_TIME = 2000;
2 | export const BUTTON_DELAY_TIME = 1.1;
3 | export const DELAY_STEP = 0.1;
4 |
5 | export const possibleStates = {
6 | INIT: { type: 'init' },
7 | ENTERING: { type: 'entering' },
8 | ENTERED: { type: 'entered' },
9 | EXITING: { type: 'exiting' },
10 | EXITED: { type: 'exited' }
11 | } as const;
12 |
13 | export const panelAnimClass = 'spid-button-panel-anim';
14 | export const logoAnimationOutClass = 'spid-button-logo-animation-out';
15 | export const emptyClass = '';
16 | export const buttonIconAnimationClass = 'spid-button-icon-animation';
17 |
18 | export const sizeMapping = {
19 | sm: 'small',
20 | md: 'medium',
21 | l: 'large'
22 | };
23 |
--------------------------------------------------------------------------------
/src/modalVariant/extra.module.css:
--------------------------------------------------------------------------------
1 | img.spid-button-image-fix {
2 | vertical-align: baseline;
3 | }
--------------------------------------------------------------------------------
/src/modalVariant/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import SpidIcoCircleLbUrl from '/../shared/svgs/spid-ico-circle-lb.svg';
3 | import SpidIcoCircleBbUrl from '/../shared/svgs/spid-ico-circle-bb.svg';
4 |
5 | import { getTranslationFn } from '../shared/i18n';
6 | import {
7 | computeButtonClasses,
8 | computeButtonTransitionClasses,
9 | isVisible,
10 | getDefinedClasses
11 | } from './utils';
12 |
13 | import { DEFAULT_TRANSITION_TIME, possibleStates } from './constants';
14 | import {
15 | mergeProviders,
16 | validateURL,
17 | getShuffledProviders
18 | } from '../shared/utils';
19 | import { ProvidersModal } from './ProvidersModal';
20 |
21 | import type { TranslateFn } from '../shared/i18n';
22 | import type { SPIDButtonProps } from '../shared/types';
23 | import type { ModalState } from './types';
24 |
25 | const providersList = getShuffledProviders();
26 |
27 | const LoginButton = ({
28 | i18n,
29 | theme,
30 | corners,
31 | fluid,
32 | size,
33 | toggleModal,
34 | modalVisibility
35 | }: Pick & {
36 | i18n: TranslateFn;
37 | modalVisibility: ModalState;
38 | toggleModal: (prevState: boolean) => void;
39 | }) => {
40 | const customStylingClasses = computeButtonClasses({
41 | theme,
42 | corners,
43 | size,
44 | fluid
45 | });
46 | const {
47 | wrapper: wrapperTransitionClasses,
48 | icon: iconButtonClasses
49 | } = computeButtonTransitionClasses(modalVisibility);
50 | const buttonImageUrl =
51 | theme === 'negative' ? SpidIcoCircleLbUrl : SpidIcoCircleBbUrl;
52 | return (
53 | toggleModal(true)}
60 | >
61 |
68 |
73 |
74 |
75 | {i18n('entra_con_SPID')}
76 |
77 |
78 | );
79 | };
80 |
81 | /**
82 | * The specific component button with the modal.
83 | * Use this component when you want to minimize the footprint in your project.
84 | * It accepts the same props as the main component. The `type` prop is ignored in this case.
85 | *
86 | * @param props
87 | */
88 | export const SPIDReactButton = ({
89 | lang = 'it',
90 | extraProviders = [],
91 | corners = 'rounded',
92 | fluid = true,
93 | size = 'md',
94 | theme = 'positive',
95 | configuration = { method: 'GET' },
96 | mapping = {},
97 | protocol = 'SAML',
98 | url,
99 | sorted = false,
100 | supported = providersList.map(({ entityID }) => entityID),
101 | onProvidersShown,
102 | onProvidersHidden,
103 | onProviderClicked
104 | }: SPIDButtonProps) => {
105 | const [state, setState] = useState(possibleStates.INIT);
106 |
107 | useEffect(() => {
108 | if (state.type === possibleStates.ENTERING.type) {
109 | if (onProvidersShown) {
110 | onProvidersShown();
111 | }
112 | }
113 | if (state.type === possibleStates.EXITING.type) {
114 | if (onProvidersHidden) {
115 | onProvidersHidden();
116 | }
117 | }
118 | }, [state]);
119 |
120 | useEffect(() => {
121 | let timer: NodeJS.Timeout;
122 | if (state.type === possibleStates.ENTERING.type) {
123 | timer = setTimeout(
124 | () => setState(possibleStates.ENTERED),
125 | DEFAULT_TRANSITION_TIME
126 | );
127 | }
128 | if (state.type === possibleStates.EXITING.type) {
129 | timer = setTimeout(
130 | () => setState(possibleStates.EXITED),
131 | DEFAULT_TRANSITION_TIME
132 | );
133 | }
134 | return () => {
135 | if (timer != null) {
136 | clearTimeout(timer);
137 | }
138 | };
139 | }, [state]);
140 |
141 | validateURL(url);
142 |
143 | const translateFn = getTranslationFn(lang);
144 |
145 | const moreLoginProps = {
146 | theme,
147 | corners,
148 | fluid,
149 | size
150 | };
151 |
152 | const moreModalProps = {
153 | extraProviders,
154 | configuration,
155 | url,
156 | mapping,
157 | protocol,
158 | supported,
159 | onProviderClicked
160 | };
161 |
162 | const mergedProviders = mergeProviders(providersList, extraProviders, {
163 | sorted
164 | });
165 |
166 | return (
167 |
168 |
setState(possibleStates.EXITING)}
173 | {...moreModalProps}
174 | />
175 |
179 | setState(open ? possibleStates.ENTERING : possibleStates.EXITING)
180 | }
181 | {...moreLoginProps}
182 | />
183 |
184 | );
185 | };
186 |
--------------------------------------------------------------------------------
/src/modalVariant/types.tsx:
--------------------------------------------------------------------------------
1 | import { possibleStates } from '../modalVariant/constants';
2 |
3 | export type ModalState = typeof possibleStates[keyof typeof possibleStates];
4 |
--------------------------------------------------------------------------------
/src/modalVariant/utils.test.tsx:
--------------------------------------------------------------------------------
1 | import { possibleStates } from './constants';
2 | import { computeButtonClasses, isVisible } from './utils';
3 |
4 | describe('Modal Utils', () => {
5 | describe('computeButtonClasses', () => {
6 | it('should fallback to large if `xl` is passed', () => {
7 | expect(computeButtonClasses({ size: 'xl' })).toEqual([
8 | 'spid-button-size-large'
9 | ]);
10 | });
11 |
12 | it('should return no class for fluid falsy', () => {
13 | expect(computeButtonClasses({ fluid: false })).toEqual([]);
14 | });
15 | });
16 |
17 | describe('isVisible', () => {
18 | it('should return false for all non entering states', () => {
19 | for (const state of [
20 | possibleStates.INIT,
21 | possibleStates.EXITED,
22 | possibleStates.EXITING
23 | ]) {
24 | expect(isVisible(state)).toEqual(false);
25 | }
26 | });
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/modalVariant/utils.tsx:
--------------------------------------------------------------------------------
1 | import { SPIDButtonProps } from '../shared/types';
2 | import {
3 | buttonIconAnimationClass,
4 | possibleStates,
5 | sizeMapping,
6 | emptyClass
7 | } from './constants';
8 | import { ModalState } from './types';
9 |
10 | import styles from './index.module.css';
11 | import extraStyles from './extra.module.css';
12 |
13 | // the css module is fake for the modal, so we can just centralize the renaming logic here
14 | export function getDefinedClasses(klasses: (string | undefined)[]) {
15 | return klasses
16 | .map(
17 | (klass) => (klass && (styles[klass] || extraStyles[klass])) || emptyClass
18 | )
19 | .join(' ');
20 | }
21 |
22 | // do not wrap these results with the getDefinedClasses yet as they will be wrapped later on
23 | // on the components
24 | type classesProps = Pick<
25 | SPIDButtonProps,
26 | 'theme' | 'corners' | 'size' | 'fluid'
27 | >;
28 | export function computeButtonClasses({
29 | theme,
30 | corners,
31 | size,
32 | fluid
33 | }: classesProps): string[] {
34 | if (process.env.NODE_ENV === 'production') {
35 | if (size === 'xl') {
36 | console.log(
37 | 'Size "xl" is not supported by SPID React Button with Modal variant'
38 | );
39 | }
40 | }
41 | return [
42 | theme,
43 | corners,
44 | size ? `size-${sizeMapping[size] || 'large'}` : null,
45 | fluid ? 'fluid' : null
46 | ]
47 | .map((type) => (type != null ? `spid-button-${type}` : ''))
48 | .filter(Boolean);
49 | }
50 |
51 | const emptyClasses: string[] = [];
52 | export function computeButtonTransitionClasses({
53 | type
54 | }: ModalState): { wrapper: string[]; icon: string[] } {
55 | const inClass = 'in';
56 | switch (type) {
57 | case possibleStates.ENTERING.type:
58 | return {
59 | wrapper: ['spid-button-transition'],
60 | icon: [buttonIconAnimationClass, inClass]
61 | };
62 | case possibleStates.ENTERED.type:
63 | return {
64 | wrapper: emptyClasses,
65 | icon: [buttonIconAnimationClass, inClass]
66 | };
67 | case possibleStates.EXITING.type:
68 | return {
69 | wrapper: ['spid-button-reverse-enter-transition'],
70 | icon: [buttonIconAnimationClass]
71 | };
72 | case possibleStates.EXITED.type:
73 | return { wrapper: emptyClasses, icon: [buttonIconAnimationClass] };
74 | case possibleStates.INIT.type:
75 | return { wrapper: emptyClasses, icon: emptyClasses };
76 | }
77 | }
78 |
79 | export function isVisible(modalState: ModalState) {
80 | return modalState.type.includes('enter');
81 | }
82 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/shared/ProviderButton.module.css:
--------------------------------------------------------------------------------
1 | .srOnly {
2 | position: absolute;
3 | width: 1px;
4 | height: 1px;
5 | padding: 0;
6 | margin: -1px;
7 | overflow: hidden;
8 | clip: rect(0, 0, 0, 0);
9 | border: 0
10 | }
--------------------------------------------------------------------------------
/src/shared/ProviderButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect } from 'react';
2 |
3 | import { isGetMethod } from './utils';
4 |
5 | import type { TranslateFn } from './i18n';
6 | import type { RegisteredProviderRecord, SPIDButtonProps, Types } from './types';
7 |
8 | import styles from './ProviderButton.module.css';
9 |
10 | type ProviderButtonProps = Required<
11 | Pick
12 | > &
13 | Pick & {
14 | idp: RegisteredProviderRecord;
15 | isActive: boolean;
16 | i18n: TranslateFn;
17 | className: string;
18 | firstFocus: boolean;
19 | };
20 | export const SharedProviderButton = ({
21 | idp,
22 | configuration,
23 | url,
24 | isActive,
25 | mapping,
26 | i18n,
27 | onProviderClicked,
28 | className,
29 | type,
30 | firstFocus
31 | }: ProviderButtonProps) => {
32 | const idpRef = useRef(null);
33 | const entityID =
34 | idp.entityID in mapping ? mapping[idp.entityID] : idp.entityID;
35 | const actionURL = url.replace('{{idp}}', encodeURIComponent(entityID));
36 | const linkTitle = isActive
37 | ? i18n('accedi_con_idp', idp.entityName)
38 | : i18n('idp_disabled');
39 |
40 | const loginURL = isActive ? actionURL : undefined;
41 |
42 | useEffect(() => {
43 | if (firstFocus && idpRef.current) {
44 | idpRef.current.focus();
45 | }
46 | }, [idpRef]);
47 |
48 | if (isGetMethod(configuration)) {
49 | return (
50 | (idpRef.current = el)}
52 | title={linkTitle}
53 | href={loginURL}
54 | // @ts-expect-error
55 | disabled={!isActive}
56 | className={type === 'modal' ? '' : className}
57 | onClick={(e) =>
58 | onProviderClicked && onProviderClicked(idp, loginURL, e)
59 | }
60 | role='link'
61 | id={entityID}
62 | >
63 |
64 |
65 | );
66 | }
67 | return (
68 |
100 | );
101 | };
102 |
103 | const ProviderButtonContent = ({
104 | idp,
105 | title,
106 | type
107 | }: {
108 | idp: RegisteredProviderRecord;
109 | title: string;
110 | type: Types;
111 | }) => {
112 | if (idp.logo == null) {
113 | return {idp.entityName} ;
114 | }
115 | return (
116 |
117 | {title}
118 |
124 |
125 | );
126 | };
127 |
--------------------------------------------------------------------------------
/src/shared/i18n.test.ts:
--------------------------------------------------------------------------------
1 | import { getTranslationFn } from './i18n';
2 |
3 | describe('i18n', () => {
4 | it('should throw if an unknown language or key is passed', () => {
5 | // Wrong language
6 | // @ts-expect-error
7 | expect(() => getTranslationFn('kk')('naviga_indietro')).toThrowError();
8 | // Wrong key
9 | // @ts-expect-error
10 | expect(() => getTranslationFn('it')('blah_blah')).toThrowError();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/src/shared/i18n.ts:
--------------------------------------------------------------------------------
1 | import type { Languages } from './types';
2 |
3 | const translations = {
4 | naviga_indietro: {
5 | it: 'Torna indietro',
6 | en: 'Go back',
7 | de: 'Gehen Sie zurück',
8 | es: 'Regresar',
9 | fr: 'Revenir'
10 | },
11 | alt_logo_SPID: {
12 | it: 'Logo SPID',
13 | en: 'Logo SPID',
14 | de: 'Logo SPID',
15 | es: 'Logo SPID',
16 | fr: 'Logo SPID'
17 | },
18 | scegli_provider_SPID: {
19 | it: 'Scegli il tuo provider SPID',
20 | en: 'Choose your SPID provider',
21 | de: 'Wähle Ihren SPIDProvider',
22 | es: 'Escoge tu proveedor SPID',
23 | fr: 'Choisissez votre fournisseur SPID'
24 | },
25 | annulla_accesso: {
26 | it: 'Annulla',
27 | en: 'Cancel',
28 | de: 'Zurücknehmen',
29 | es: 'Cancelar',
30 | fr: 'Annuler'
31 | },
32 | non_hai_SPID: {
33 | it: 'Non hai SPID?',
34 | en: "Don't have SPID?",
35 | de: 'Haben Sie nicht SPID?',
36 | es: 'No tiene SPID?',
37 | fr: "Vous ñ'avez pas de SPID?"
38 | },
39 | cosa_SPID: {
40 | it: "Cos'è SPID?",
41 | en: 'What is SPID?',
42 | de: 'Was ist SPID?',
43 | es: 'Qué es SPID?',
44 | fr: 'Qu’est-ce que SPID?'
45 | },
46 | entra_con_SPID: {
47 | it: 'Entra con SPID',
48 | en: 'Sign in with SPID',
49 | de: 'Loggen Sie mit SPID',
50 | es: 'Ingresa con SPID',
51 | fr: 'Connectez-vous avec SPID'
52 | },
53 | scopri_di_piu: {
54 | it: 'Scopri di più.',
55 | en: 'Learn more.',
56 | de: 'Finde mehr heraus.',
57 | es: 'Saber más',
58 | fr: 'En savoir plus.'
59 | },
60 | accedi_con_idp: {
61 | it: 'Accedi a SPID con {0}',
62 | en: 'Access to SPID with {0}',
63 | de: 'Zugriff auf SPID mit {0}',
64 | es: 'Accede a SPID con {0}',
65 | fr: 'Accès à SPID avec {0}'
66 | },
67 | idp_disabled: {
68 | it: 'Provider non attivo',
69 | en: 'Provider not enabled',
70 | de: 'Provider nicht aktiviert',
71 | es: 'Proveedor no disponible',
72 | fr: 'Fournisseur non activé'
73 | },
74 | maggiori_info: {
75 | it: 'Maggiori info',
76 | en: 'More info',
77 | de: 'Mehr info',
78 | es: 'Más información',
79 | fr: 'Plus d’info'
80 | }
81 | } as const;
82 | const placeholderRegex = /\{\d}/;
83 |
84 | export type labelKeys = keyof typeof translations;
85 |
86 | export type TranslateFn = (
87 | labelKey: labelKeys,
88 | placeholderValue?: string
89 | ) => string;
90 |
91 | let currentLang = 'it';
92 | export const getTranslationFn = (language: Languages): TranslateFn => {
93 | currentLang = language;
94 | return (labelKey: labelKeys, placeholderValue?: string) => {
95 | const text = translations[labelKey] && translations[labelKey][currentLang];
96 | if (!text) {
97 | throw Error(
98 | `labelKey ${labelKey} non presente per la lingua selezionata ${currentLang}`
99 | );
100 | }
101 | if (placeholderValue != null) {
102 | return text.replace(placeholderRegex, placeholderValue);
103 | }
104 | return text;
105 | };
106 | };
107 | /**
108 | * Returns the list of supported languages for the UI
109 | */
110 | export const getSupportedLanguages = (): Languages[] =>
111 | Object.keys(Object.values(translations)[0]) as Languages[];
112 |
--------------------------------------------------------------------------------
/src/shared/providers.tsx:
--------------------------------------------------------------------------------
1 | import ArubaSVGUrl from '/./svgs/idp-logos/spid-idp-arubaid.svg';
2 | import InfocertSVGUrl from '/./svgs/idp-logos/spid-idp-infocertid.svg';
3 | import PosteSVGUrl from '/./svgs/idp-logos/spid-idp-posteid.svg';
4 | import SielteSVGUrl from '/./svgs/idp-logos/spid-idp-sielteid.svg';
5 | import TimSVGUrl from '/./svgs/idp-logos/spid-idp-timid.svg';
6 | import NamirialSVGUrl from '/./svgs/idp-logos/spid-idp-namirialid.svg';
7 | import RegisterItSVGUrl from '/./svgs/idp-logos/spid-idp-spiditalia.svg';
8 | import IntesaSVGUrl from '/./svgs/idp-logos/spid-idp-intesaid.svg';
9 | import LepidaSVGUrl from '/./svgs/idp-logos/spid-idp-lepidaid.svg';
10 | import TeamSystemSVGUrl from '/./svgs/idp-logos/spid-idp-teamsystemid.svg';
11 |
12 | import providersList from './providers_meta.json';
13 |
14 | import type { RegisteredProviderRecord } from './types';
15 |
16 | /**
17 | * This array of images has been carefully ordered alphabetically
18 | * to do the matching with the metadata from json, until
19 | * https://github.com/italia/spid-sp-access-button/issues/7 gets resolved
20 | */
21 | const images = [
22 | ArubaSVGUrl,
23 | InfocertSVGUrl,
24 | IntesaSVGUrl,
25 | LepidaSVGUrl,
26 | NamirialSVGUrl,
27 | PosteSVGUrl,
28 | RegisterItSVGUrl,
29 | SielteSVGUrl,
30 | TeamSystemSVGUrl,
31 | TimSVGUrl
32 | ];
33 |
34 | export const providers: Readonly[] = Object.entries(
35 | providersList
36 | )
37 | .sort(([idA], [idB]) => idA.localeCompare(idB))
38 | .map(([entityName, entityID], i) => ({
39 | protocols: ['SAML'],
40 | entityName,
41 | entityID,
42 | active: true,
43 | logo: images[i]
44 | }));
45 |
--------------------------------------------------------------------------------
/src/shared/providers_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "Aruba ID": "https://loginspid.aruba.it",
3 | "Infocert": "https://identity.infocert.it",
4 | "Poste ID": "https://posteid.poste.it",
5 | "Sielte": "https://identity.sieltecloud.it",
6 | "Tim ID": "https://login.id.tim.it/affwebservices/public/saml2sso",
7 | "Namirial ID": "https://idp.namirialtsp.com/idp",
8 | "SPIDItalia Register.it": "https://spid.register.it",
9 | "Intesa ID": "https://spid.intesa.it",
10 | "Lepida ID": "https://id.lepida.it/idp/shibboleth",
11 | "Team System ID": "https://spid.teamsystem.com/idp"
12 | }
--------------------------------------------------------------------------------
/src/shared/svgs/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/svgs/idp-logos/spid-idp-infocertid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/svgs/idp-logos/spid-idp-intesaid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/svgs/idp-logos/spid-idp-lepidaid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/svgs/idp-logos/spid-idp-namirialid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/svgs/idp-logos/spid-idp-posteid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/svgs/idp-logos/spid-idp-spiditalia.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/svgs/idp-logos/spid-idp-timid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/svgs/spid-ico-circle-bb-bg.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/svgs/spid-ico-circle-bb.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/svgs/spid-ico-circle-lb.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/svgs/spid-logo-animation-black.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/svgs/spid-logo-animation.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/svgs/spid-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The protocol to use for the current instance.
3 | * Only Providers who support the declared protocol are enabled.
4 | */
5 | export type Protocols = 'SAML' | 'OIDC';
6 | /**
7 | * The language used for the UI.
8 | */
9 | export type Languages = 'it' | 'en' | 'de' | 'es' | 'fr';
10 | /**
11 | * The size of the button. Options are: `"sm"` (small), `"md"` (medium), `"l"` (large) and `"xl"` (extra large - dropdown only).
12 | * The modal version does not support the `"xl"` size and will fallback to `"l"` if passed.
13 | */
14 | export type Sizes = 'sm' | 'md' | 'l' | 'xl';
15 | /**
16 | * The theme used for the button:
17 | * * "positive" has a blue background with white text,
18 | * * "negative" has a white background and blue text.
19 | */
20 | export type ColorTheme = 'positive' | 'negative';
21 |
22 | /**
23 | * The type of corner for the button: rounded or sharp.
24 | */
25 | export type CornerType = 'rounded' | 'sharp';
26 | /**
27 | * Each Provider button will use this configuration for its button.
28 | * This is the specific GET type.
29 | */
30 | export type ConfigurationGET = { method: 'GET' };
31 | /**
32 | * Each Provider button will use this configuration for its button.
33 | * This is the specific POST type
34 | */
35 | export type ConfigurationPOST = {
36 | method: 'POST';
37 | fieldName: string;
38 | extraFields?: Record;
39 | };
40 |
41 | /**
42 | * The way to present the providers once clicked.
43 | */
44 | export type Types = 'modal' | 'dropdown';
45 | /**
46 | * The object format of a Identity Provider object.
47 | */
48 | export interface ProviderRecord {
49 | entityID: string;
50 | entityName: string;
51 | protocols?: Protocols[];
52 | logo?: string;
53 | }
54 | /**
55 | * Format used internally for Providers, more strict compare to the one above
56 | * @private
57 | */
58 | export type RegisteredProviderRecord = Omit & {
59 | protocols: Protocols[];
60 | active: boolean;
61 | };
62 |
63 | // public types: do not use utilities here,
64 | // but just base all the internal types from this extended version
65 | export type SPIDButtonProps = {
66 | /**
67 | * The way to present the providers once clicked. The default value is `"modal"`.
68 | * @defaultValue `"modal"`
69 | */
70 | type?: Types;
71 | /**
72 | * The language used for the UI. The default value is `"it"`.
73 | * @defaultValue `"it"`
74 | */
75 | lang?: Languages;
76 | /**
77 | * The URL used by the buttons.
78 | * It can be either absolute or relative.
79 | * It must contains the `"{{idp}}"` string in it, which will be replaced by the entityID of each provider
80 | * (unless specified otherwise with the `mapping` prop - see below).
81 | * This props is *mandatory*.
82 | */
83 | url: string;
84 | /**
85 | * An object containing the mapping for the providers.
86 | * This is useful when a Service Provider identifies the IDP with a different string than the entityID
87 | */
88 | mapping?: Record;
89 | /**
90 | * The list of entityID supported by the button instance.
91 | * The default value is all the official providers.
92 | * @defaultValue All providers
93 | */
94 | supported?: ProviderRecord['entityID'][];
95 | /**
96 | * It forces an ascending order (A->Z) of the providers, based on the entityName string.
97 | * Note that this will sort with no distictions between official and extraProviders in the list.
98 | * @defaultValue `false`
99 | */
100 | sorted?: boolean;
101 | /**
102 | * The protocol to use for the current instance.
103 | * Only Providers who support the declared protocol are enabled.
104 | * The default value is `"SAML"`.
105 | * @defaultValue `"SAML"`
106 | */
107 | protocol?: Protocols;
108 | /**
109 | * Each Provider button will use this configuration for its button.
110 | * The default value is `{"method": "GET"}`
111 | * @defaultValue `{"method": "GET"}`
112 | */
113 | configuration?: ConfigurationGET | ConfigurationPOST;
114 | /**
115 | * The size of the button. Options are: `"sm"` (small), `"md"` (medium), `"l"` (large) and `"xl"` (extra large - dropdown only).
116 | * The modal version does not support the `"xl"` size and will fallback to `"l"` if passed.
117 | * The default value is `"md"`.
118 | * @defaultValue `"md"`
119 | */
120 | size?: Sizes;
121 | /**
122 | * The theme used for the button:
123 | * * "positive" has a blue background with white text,
124 | * * "negative" has a white background and blue text.
125 | * The default value is `"positive"`.
126 | * @defaultValue `"positive"`
127 | */
128 | theme?: ColorTheme;
129 | /**
130 | * This controls the width of the button: when fluid it will fill all the available space.
131 | * It applies only to the modal version.
132 | * The default value is `false`.
133 | * @defaultValue `false`
134 | */
135 | fluid?: boolean;
136 | /**
137 | * The type of corner for the button: rounded or sharp.
138 | * The default value is `"rounded"`.
139 | * @defaultValue `"rounded"`
140 | */
141 | corners?: CornerType;
142 | /**
143 | * This is called when the providers are shown on the screen (as soon as the animation starts)
144 | */
145 | onProvidersShown?: () => void;
146 | /**
147 | * This is called when the providers are hidden on the screen (as soon as the animation starts)
148 | */
149 | onProvidersHidden?: () => void;
150 | /**
151 | * This is called when a user clicks on a provider button.
152 | * @param providerEntry The full entry of the provider clicked is passed, together with the event
153 | * @param loginURL The final URL for the specific Identity Provider. It returns undefined if the button is disabled
154 | * @param event React original MouseEvent
155 | */
156 | onProviderClicked?: (
157 | providerEntry: ProviderRecord,
158 | loginURL: string | undefined,
159 | event:
160 | | React.MouseEvent
161 | | React.MouseEvent
162 | ) => void;
163 | /**
164 | * Used for testing. *Do not use in production*
165 | */
166 | extraProviders?: ProviderRecord[];
167 | };
168 |
--------------------------------------------------------------------------------
/src/shared/utils.test.tsx:
--------------------------------------------------------------------------------
1 | import { ProviderRecord, RegisteredProviderRecord } from './types';
2 | import { isProviderActive, mergeProviders, validateURL } from './utils';
3 |
4 | describe('Shared utils', () => {
5 | describe('validateURL', () => {
6 | it('should pass if the {{idp}} string is within the URL', () => {
7 | expect(() => validateURL('{{idp}}')).not.toThrowError();
8 | expect(() => validateURL('/myLogin/{{idp}}')).not.toThrowError();
9 | expect(() => validateURL('www.myidp.it/{{idp}}')).not.toThrowError();
10 | });
11 |
12 | it('should throw if the URL does not contain the {{idp}} string', () => {
13 | expect(() => validateURL('')).toThrowError();
14 | expect(() => validateURL('idp')).toThrowError();
15 | expect(() => validateURL('{{idp')).toThrowError();
16 | });
17 | });
18 |
19 | describe('isProviderActive', () => {
20 | it('should return true if all checks are right', () => {
21 | expect(
22 | isProviderActive(
23 | { entityID: 'a', entityName: 'A', protocols: ['SAML'], active: true },
24 | ['a'],
25 | 'SAML',
26 | []
27 | )
28 | ).toEqual(true);
29 | });
30 | it('should return false if not supported', () => {
31 | expect(
32 | isProviderActive(
33 | { entityID: 'a', entityName: 'A', protocols: ['SAML'], active: true },
34 | [],
35 | 'SAML',
36 | []
37 | )
38 | ).toEqual(false);
39 | });
40 | it('should return false if has an unsupported protocol', () => {
41 | expect(
42 | isProviderActive(
43 | { entityID: 'a', entityName: 'A', protocols: ['OIDC'], active: true },
44 | ['a'],
45 | 'SAML',
46 | []
47 | )
48 | ).toEqual(false);
49 | });
50 | it('should return false if there are any extraProviders', () => {
51 | expect(
52 | isProviderActive(
53 | { entityID: 'a', entityName: 'A', protocols: ['OIDC'], active: true },
54 | ['a'],
55 | 'SAML',
56 | [{ entityID: 'b', entityName: 'B' }]
57 | )
58 | ).toEqual(false);
59 | });
60 | it('should return false if the record is not active for some reason', () => {
61 | expect(
62 | isProviderActive(
63 | {
64 | entityID: 'a',
65 | entityName: 'A',
66 | protocols: ['OIDC'],
67 | active: false
68 | },
69 | ['a'],
70 | 'SAML',
71 | []
72 | )
73 | ).toEqual(false);
74 | });
75 | });
76 |
77 | describe('mergeProviders', () => {
78 | it('should always prepend the official providers', () => {
79 | const officialIdps: RegisteredProviderRecord[] = [
80 | {
81 | entityID: 'a',
82 | entityName: 'A',
83 | protocols: ['OIDC'],
84 | active: true
85 | }
86 | ];
87 | const extraIdps: ProviderRecord[] = [{ entityID: 'b', entityName: 'B' }];
88 | const merged = mergeProviders(officialIdps, extraIdps);
89 | merged.slice(0, officialIdps.length).forEach(({ entityID }, i) => {
90 | expect(entityID).toEqual(officialIdps[i].entityID);
91 | });
92 | });
93 |
94 | it('should always disable the official providers if any extra one is passed', () => {
95 | const officialIdps: RegisteredProviderRecord[] = [
96 | {
97 | entityID: 'a',
98 | entityName: 'A',
99 | protocols: ['OIDC'],
100 | active: true
101 | }
102 | ];
103 | const extraIdps: ProviderRecord[] = [{ entityID: 'b', entityName: 'B' }];
104 | const merged = mergeProviders(officialIdps, extraIdps);
105 | for (const { active } of merged.slice(0, officialIdps.length)) {
106 | expect(active).toEqual(false);
107 | }
108 | });
109 |
110 | it('should sort providers by name when requested', () => {
111 | const officialIdps: RegisteredProviderRecord[] = [
112 | {
113 | entityID: 'a',
114 | entityName: 'A',
115 | protocols: ['OIDC'],
116 | active: true
117 | },
118 | {
119 | entityID: 'c',
120 | entityName: 'C',
121 | protocols: ['OIDC'],
122 | active: true
123 | }
124 | ];
125 | const extraIdps: ProviderRecord[] = [{ entityID: 'b', entityName: 'B' }];
126 | const merged = mergeProviders(officialIdps, extraIdps, { sorted: true });
127 | expect(merged.map(({ entityName }) => entityName)).toEqual([
128 | 'A',
129 | 'B',
130 | 'C'
131 | ]);
132 | });
133 | });
134 | });
135 |
--------------------------------------------------------------------------------
/src/shared/utils.ts:
--------------------------------------------------------------------------------
1 | import { ProviderRecord } from '..';
2 | import { providers } from './providers';
3 | import {
4 | ConfigurationGET,
5 | ConfigurationPOST,
6 | Protocols,
7 | RegisteredProviderRecord
8 | } from './types';
9 |
10 | // avoid http/https confusion and centralize this URL
11 | export const SPID_URL = 'https://www.spid.gov.it';
12 |
13 | export function mergeProviders(
14 | providers: Readonly[],
15 | extraProviders: ProviderRecord[],
16 | { sorted }: { sorted?: boolean } = {}
17 | ): RegisteredProviderRecord[] {
18 | const mergedList = [
19 | ...providers.map((idp) => ({
20 | ...idp,
21 | active: !extraProviders.length
22 | })),
23 | ...extraProviders.map((idp) => ({
24 | ...idp,
25 | protocols: idp.protocols ?? ['SAML'],
26 | active: true
27 | }))
28 | ];
29 | if (!sorted) {
30 | return mergedList;
31 | }
32 | return mergedList.sort((idpA, idpB) =>
33 | idpA.entityName.localeCompare(idpB.entityName)
34 | );
35 | }
36 |
37 | export function validateURL(url: string | undefined) {
38 | if (url == null || url.indexOf('{{idp}}') < 0) {
39 | throw Error('URL parameter must contain the "{{idp}} string');
40 | }
41 | }
42 |
43 | export function isGetMethod(
44 | configuration: ConfigurationGET | ConfigurationPOST
45 | ): configuration is ConfigurationGET {
46 | return configuration.method.toUpperCase() === 'GET';
47 | }
48 |
49 | function dirtyCopy(obj: T): T {
50 | return JSON.parse(JSON.stringify(obj));
51 | }
52 | // Used for exporting
53 | /**
54 | * Returns a copy of the list of the official providers.
55 | * @private
56 | */
57 | export const providersCopy = dirtyCopy(providers) as RegisteredProviderRecord[];
58 |
59 | /**
60 | * Returns a copy of the list of the official providers, already shuffled
61 | */
62 | export function getShuffledProviders() {
63 | return providersCopy.sort(() => Math.random() - 0.5);
64 | }
65 |
66 | export function isProviderActive(
67 | idp: RegisteredProviderRecord,
68 | supported: string[],
69 | protocol: Protocols,
70 | extraProviders: ProviderRecord[]
71 | ) {
72 | const isExtraProviders = extraProviders.some(
73 | ({ entityID }) => entityID === idp.entityID
74 | );
75 | return (
76 | supported.indexOf(idp.entityID) > -1 &&
77 | idp.protocols.indexOf(protocol) > -1 &&
78 | (extraProviders.length === 0 || isExtraProviders) &&
79 | idp.active
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/src/test/utils.ts:
--------------------------------------------------------------------------------
1 | import { fireEvent, screen, act } from '@testing-library/react';
2 | import { ConfigurationGET, ConfigurationPOST, Types } from '../shared/types';
3 |
4 | export const TIMEOUT_VALUE = 2500;
5 | export const defaultURL = '/myLogin/idp={{idp}}';
6 |
7 | export const configurations: [ConfigurationGET, ConfigurationPOST] = [
8 | { method: 'GET' },
9 | { method: 'POST', fieldName: 'a' }
10 | ];
11 |
12 | export const types: Types[] = ['modal', 'dropdown'];
13 |
14 | function getAllProvidersButtons(
15 | method: (ConfigurationGET | ConfigurationPOST)['method'],
16 | criteria = /(Accedi a SPID con|Provider non)/
17 | ) {
18 | return screen.queryAllByRole(method === 'GET' ? 'link' : 'button', {
19 | name: criteria
20 | });
21 | }
22 |
23 | function getOnlyEnabledProviders(
24 | method: (ConfigurationGET | ConfigurationPOST)['method']
25 | ) {
26 | return getAllProvidersButtons(method, /Accedi a SPID con/);
27 | }
28 |
29 | function getOnlyDisabledProviders(
30 | method: (ConfigurationGET | ConfigurationPOST)['method']
31 | ) {
32 | return getAllProvidersButtons(method, /Provider non/);
33 | }
34 | // Sometimes it is useful to mute the console for internal warnings/error
35 | // just be sure to check that we aren't hiding other errors with that
36 | export function muteConsoleWithCheck(
37 | fn: Function,
38 | messageToFilter: string | RegExp = ''
39 | ) {
40 | const originalError = console.error;
41 | console.error = jest.fn();
42 |
43 | const result = fn();
44 |
45 | if (messageToFilter) {
46 | expect(console.error).toHaveBeenCalledWith(
47 | expect.stringMatching(messageToFilter),
48 | undefined
49 | );
50 | } else {
51 | expect(console.error).toHaveBeenCalled();
52 | }
53 | console.error = originalError;
54 | return result;
55 | }
56 |
57 | export type TestHelper = {
58 | getMainButton: () => HTMLElement;
59 | openList: () => void;
60 | closeList: () => void;
61 | clickProvider: (provider: HTMLElement) => boolean;
62 | getEnabledProviders: () => HTMLElement[];
63 | getDisabledProviders: () => HTMLElement[];
64 | getProviders: () => HTMLElement[];
65 | };
66 | export function createHelper(
67 | type: Types,
68 | method: (ConfigurationGET | ConfigurationPOST)['method']
69 | ): TestHelper {
70 | const buttonRole = type === 'dropdown' ? 'link' : 'button';
71 | const getMainButton = () =>
72 | screen.getByRole(buttonRole, { name: /Entra con SPID/ });
73 | const openList = () => {
74 | act(() => {
75 | jest.useFakeTimers();
76 | fireEvent.click(getMainButton());
77 | jest.advanceTimersByTime(TIMEOUT_VALUE);
78 | jest.useRealTimers();
79 | });
80 | };
81 | const closeList = () => {
82 | jest.useFakeTimers();
83 | act(() => {
84 | if (type === 'modal') {
85 | fireEvent.click(screen.getByRole('button', { name: /Annulla/ }));
86 | } else {
87 | fireEvent.click(
88 | screen.getByRole(buttonRole, { name: /Entra con SPID/ })
89 | );
90 | }
91 | jest.advanceTimersByTime(TIMEOUT_VALUE);
92 | jest.useRealTimers();
93 | });
94 | };
95 | return {
96 | getMainButton,
97 | openList,
98 | closeList,
99 | clickProvider: (provider: HTMLElement) => {
100 | if (method === 'POST') {
101 | return muteConsoleWithCheck(() => {
102 | // window.HTMLFormElement.prototype.submit = () => {};
103 | return fireEvent.click(provider);
104 | }, 'Error: Not implemented: HTMLFormElement.prototype.submit');
105 | }
106 | return fireEvent.click(provider);
107 | },
108 | getEnabledProviders: () => getOnlyEnabledProviders(method),
109 | getDisabledProviders: () => getOnlyDisabledProviders(method),
110 | getProviders: () => getAllProvidersButtons(method)
111 | };
112 | }
113 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Default CSS definition for typescript,
3 | * will be overridden with file-specific definitions by rollup
4 | */
5 | declare module '*.css' {
6 | const content: { [className: string]: string };
7 | export default content;
8 | }
9 |
10 | interface SvgrComponent
11 | extends React.FunctionComponent> {}
12 |
13 | declare module '*.svg' {
14 | const svgUrl: string;
15 | const svgComponent: SvgrComponent;
16 | export default svgUrl;
17 | export { svgComponent as ReactComponent };
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "module": "esnext",
5 | "lib": [
6 | "dom",
7 | "esnext"
8 | ],
9 | "moduleResolution": "node",
10 | "jsx": "react",
11 | "sourceMap": true,
12 | "declaration": true,
13 | "esModuleInterop": true,
14 | "noImplicitReturns": true,
15 | "noImplicitThis": true,
16 | "noImplicitAny": true,
17 | "strictNullChecks": true,
18 | "suppressImplicitAnyIndexErrors": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "allowSyntheticDefaultImports": true,
22 | "target": "es5",
23 | "allowJs": false,
24 | "skipLibCheck": true,
25 | "strict": true,
26 | "forceConsistentCasingInFileNames": true,
27 | "noFallthroughCasesInSwitch": true,
28 | "resolveJsonModule": true,
29 | "isolatedModules": true,
30 | "noEmit": true
31 | },
32 | "include": [
33 | "src"
34 | ],
35 | "exclude": [
36 | "node_modules",
37 | "dist",
38 | "example"
39 | ]
40 | }
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
--------------------------------------------------------------------------------