├── .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 | [![NPM](https://img.shields.io/npm/v/@dej611/spid-react-button.svg)](https://www.npmjs.com/package/@dej611/spid-react-button) ![Gzipped size](https://badgen.net/bundlephobia/minzip/@dej611/spid-react-button) ![Dependencies count](https://badgen.net/bundlephobia/dependency-count/@dej611/spid-react-button) ![Treeshaking supported](https://badgen.net/bundlephobia/tree-shaking/@dej611/spid-react-button) [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B24373%2Fgit%40github.com%3Adej611%2Fspid-react-button.git.svg?type=shield)](https://app.fossa.com/projects/custom%2B24373%2Fgit%40github.com%3Adej611%2Fspid-react-button.git?ref=badge_shield)[![Maintainability](https://api.codeclimate.com/v1/badges/9371fd32798f8de744e5/maintainability)](https://codeclimate.com/github/dej611/spid-react-button/maintainability)[![Test Coverage](https://api.codeclimate.com/v1/badges/9371fd32798f8de744e5/test_coverage)](https://codeclimate.com/github/dej611/spid-react-button/test_coverage)[![Tests results](https://github.com/dej611/spid-react-button/workflows/Unit%20tests/badge.svg)](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 | | SPID modal version | SPID dropdown version | 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 | [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B24373%2Fgit%40github.com%3Adej611%2Fspid-react-button.git.svg?type=large)](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 | 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 | 39 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 40 | 41 | 50 | 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 | 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 | 10 | 11 | 12 | 13 | 14 | 15 | {events.map(({ type, name, arg }, i) => 16 | 17 | 18 | 19 | 20 | )} 21 | 22 | 23 |
#TypeEventArg
{events.length - i}{type}{name}{arg || ''}
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 | [![NPM](https://img.shields.io/npm/v/@dej611/spid-react-button.svg)](https://www.npmjs.com/package/@dej611/spid-react-button) ![Gzipped size](https://badgen.net/bundlephobia/minzip/@dej611/spid-react-button) ![Dependencies count](https://badgen.net/bundlephobia/dependency-count/@dej611/spid-react-button) ![Treeshaking supported](https://badgen.net/bundlephobia/tree-shaking/@dej611/spid-react-button) [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B24373%2Fgit%40github.com%3Adej611%2Fspid-react-button.git.svg?type=shield)](https://app.fossa.com/projects/custom%2B24373%2Fgit%40github.com%3Adej611%2Fspid-react-button.git?ref=badge_shield)[![Maintainability](https://api.codeclimate.com/v1/badges/9371fd32798f8de744e5/maintainability)](https://codeclimate.com/github/dej611/spid-react-button/maintainability)[![Test Coverage](https://api.codeclimate.com/v1/badges/9371fd32798f8de744e5/test_coverage)](https://codeclimate.com/github/dej611/spid-react-button/test_coverage)[![Tests results](https://github.com/dej611/spid-react-button/workflows/Unit%20tests/badge.svg)](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 | | SPID modal version | SPID dropdown version | 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 | [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B24373%2Fgit%40github.com%3Adej611%2Fspid-react-button.git.svg?type=large)](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 | 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 | 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 |
69 | 86 | 87 | {Object.entries(configuration.extraFields || {}).map( 88 | ([inputName, inputValue]) => { 89 | return ( 90 | 96 | ); 97 | } 98 | )} 99 |
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 | {idp.entityName} 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 | } --------------------------------------------------------------------------------