From f2b7990737d25f3663ba8364f7b1b95c36de7218 Mon Sep 17 00:00:00 2001 From: wowkalucky Date: Thu, 13 Apr 2023 17:07:54 +0300 Subject: [PATCH] feat: [FC-0006] add Verifiable Credentials optional feature --- .env | 2 + .env.development | 2 + .env.test | 2 + README.rst | 23 ++- package-lock.json | 57 ++++++ package.json | 2 + src/assets/images/appStore.png | Bin 0 -> 9642 bytes src/assets/images/googleplay.png | Bin 0 -> 9634 bytes .../NavigationBar/NavigationBar.jsx | 50 +++++ src/components/NavigationBar/index.js | 2 + src/components/NavigationBar/messages.js | 16 ++ .../NavigationBar/test/NavigationBar.test.jsx | 48 +++++ .../ProgramCertificate/ProgramCertificate.jsx | 89 +++++++++ src/components/ProgramCertificate/index.js | 2 + src/components/ProgramCertificate/messages.js | 36 ++++ .../test/ProgramCertificate.test.jsx | 56 ++++++ .../ProgramCertificateModal.jsx | 169 ++++++++++++++++ .../ProgramCertificateModal/index.js | 2 + .../ProgramCertificateModal/messages.js | 81 ++++++++ .../test/ProgramCertificateModal.test.jsx | 33 ++++ .../ProgramCertificatesList.jsx | 184 ++++++++++++++++++ .../ProgramCertificatesList/data/service.js | 46 +++++ .../ProgramCertificatesList/index.js | 2 + .../ProgramCertificatesList/messages.js | 61 ++++++ .../test/ProgramCertificatesList.test.jsx | 79 ++++++++ .../programCertificatesList.factory.js | 33 ++++ .../ProgramRecordsList/ProgramRecordsList.jsx | 7 +- src/components/index.scss | 2 +- src/constants.js | 7 + src/index.jsx | 20 +- 30 files changed, 1104 insertions(+), 9 deletions(-) create mode 100644 src/assets/images/appStore.png create mode 100644 src/assets/images/googleplay.png create mode 100644 src/components/NavigationBar/NavigationBar.jsx create mode 100644 src/components/NavigationBar/index.js create mode 100644 src/components/NavigationBar/messages.js create mode 100644 src/components/NavigationBar/test/NavigationBar.test.jsx create mode 100644 src/components/ProgramCertificate/ProgramCertificate.jsx create mode 100644 src/components/ProgramCertificate/index.js create mode 100644 src/components/ProgramCertificate/messages.js create mode 100644 src/components/ProgramCertificate/test/ProgramCertificate.test.jsx create mode 100644 src/components/ProgramCertificateModal/ProgramCertificateModal.jsx create mode 100644 src/components/ProgramCertificateModal/index.js create mode 100644 src/components/ProgramCertificateModal/messages.js create mode 100644 src/components/ProgramCertificateModal/test/ProgramCertificateModal.test.jsx create mode 100644 src/components/ProgramCertificatesList/ProgramCertificatesList.jsx create mode 100644 src/components/ProgramCertificatesList/data/service.js create mode 100644 src/components/ProgramCertificatesList/index.js create mode 100644 src/components/ProgramCertificatesList/messages.js create mode 100644 src/components/ProgramCertificatesList/test/ProgramCertificatesList.test.jsx create mode 100644 src/components/ProgramCertificatesList/test/__factories__/programCertificatesList.factory.js create mode 100644 src/constants.js diff --git a/.env b/.env index 3f51a01c..1d032bda 100644 --- a/.env +++ b/.env @@ -21,3 +21,5 @@ USER_INFO_COOKIE_NAME='' SUPPORT_URL_LEARNER_RECORDS='' APP_ID='' MFE_CONFIG_API_URL='' +ENABLE_VERIFIABLE_CREDENTIALS='' +SUPPORT_URL_VERIFIABLE_CREDENTIALS='' diff --git a/.env.development b/.env.development index f8fce662..9f657f2c 100644 --- a/.env.development +++ b/.env.development @@ -23,3 +23,5 @@ SUPPORT_URL_LEARNER_RECORDS='https://support.edx.org/hc/en-us/sections/360001216 USE_LR_MFE='true' APP_ID='' MFE_CONFIG_API_URL='' +ENABLE_VERIFIABLE_CREDENTIALS='true' +SUPPORT_URL_VERIFIABLE_CREDENTIALS='' diff --git a/.env.test b/.env.test index 35d4bea4..ca930616 100644 --- a/.env.test +++ b/.env.test @@ -18,3 +18,5 @@ SEGMENT_KEY='' SITE_NAME=localhost USER_INFO_COOKIE_NAME='edx-user-info' SUPPORT_URL_LEARNER_RECORDS='' +ENABLE_VERIFIABLE_CREDENTIALS='true' +SUPPORT_URL_VERIFIABLE_CREDENTIALS='' diff --git a/README.rst b/README.rst index b2d023f8..f23c3c53 100644 --- a/README.rst +++ b/README.rst @@ -7,11 +7,18 @@ frontend-app-learner-record Purpose ******* -The Learner Record provides information about the enrolled programs for a user. +The Learner Record provides information about the enrolled programs for a user. It contains views for a learners current status in a program, their current grade, and the ability to share any earned credentials either publically or with institutions. +Verifiable Credentials +====================== + +Optionally, this micro-frontend allows `verifiable credentials`_ creation for already achieved Open edX credentials (currently, program certificates only). + This is the Learner Record micro-frontend, currently under development by `edX `_. +.. _verifiable credentials: https://en.wikipedia.org/wiki/Verifiable_credentials + Getting Started *************** @@ -52,7 +59,7 @@ Every time you develop something in this repo # Start the Learner Record MFE npm start - + # Using your favorite editor, edit the code to make your change. vim ... @@ -81,6 +88,18 @@ This MFE has 2 flags of its own: * ``SUPPORT_URL_LEARNER_RECORDS`` -- A link to a help/support center for learners who run into problems whilst trying to share their records * ``USE_LR_MFE`` -- A toggle that when on, uses the MFE to host shared records instead of the the old UI inside of credentials +Verifiable Credentials +...................... + +An optional feature. It is behind a feature flag. +The feature introduces a couple of enviroment variables: + +* ``ENABLE_VERIFIABLE_CREDENTIALS`` -- Toggles the Verifiable Credentials feature (used by the Credentials IDA and this micro-frontend) +* ``SUPPORT_URL_VERIFIABLE_CREDENTIALS`` -- A link to a help/support center for learners who run into problems whilst trying to create verifiable credentials + +The Verifiable Credentials UI is a functional addition to the corresponding backend app (it will use a REST API from the Credentials IDA located at `credentials/apps/verifiable_credentials/rest_api`. + + Project Structure ----------------- diff --git a/package-lock.json b/package-lock.json index b80a3b0d..0e265203 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "lodash": "4.17.21", "prop-types": "15.8.1", "react": "16.14.0", + "react-device-detect": "^2.2.3", "react-dom": "16.14.0", "react-helmet-async": "^1.3.0", "react-redux": "7.2.9", @@ -45,6 +46,7 @@ "glob": "7.2.3", "husky": "7.0.4", "jest": "27.5.1", + "resize-observer-polyfill": "^1.5.1", "rosie": "2.1.0" } }, @@ -21118,6 +21120,18 @@ "node": ">=8" } }, + "node_modules/react-device-detect": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.2.3.tgz", + "integrity": "sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==", + "dependencies": { + "ua-parser-js": "^1.0.33" + }, + "peerDependencies": { + "react": ">= 0.14.0", + "react-dom": ">= 0.14.0" + } + }, "node_modules/react-dom": { "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", @@ -21917,6 +21931,12 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -24218,6 +24238,24 @@ "node": ">=4.2.0" } }, + "node_modules/ua-parser-js": { + "version": "1.0.33", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", + "integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "engines": { + "node": "*" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -41755,6 +41793,14 @@ } } }, + "react-device-detect": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.2.3.tgz", + "integrity": "sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==", + "requires": { + "ua-parser-js": "^1.0.33" + } + }, "react-dom": { "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", @@ -42366,6 +42412,12 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "dev": true + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -44176,6 +44228,11 @@ "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", "devOptional": true }, + "ua-parser-js": { + "version": "1.0.33", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", + "integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==" + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 936ee9a5..084b7009 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "lodash": "4.17.21", "prop-types": "15.8.1", "react": "16.14.0", + "react-device-detect": "^2.2.3", "react-dom": "16.14.0", "react-helmet-async": "^1.3.0", "react-redux": "7.2.9", @@ -69,6 +70,7 @@ "glob": "7.2.3", "husky": "7.0.4", "jest": "27.5.1", + "resize-observer-polyfill": "^1.5.1", "rosie": "2.1.0" } } diff --git a/src/assets/images/appStore.png b/src/assets/images/appStore.png new file mode 100644 index 0000000000000000000000000000000000000000..4003d09927ee93f02a862c7e1548dd913e3083ed GIT binary patch literal 9642 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 literal 0 HcmV?d00001 diff --git a/src/assets/images/googleplay.png b/src/assets/images/googleplay.png new file mode 100644 index 0000000000000000000000000000000000000000..b91c113921c39b02a2ade1244b9825e9f5e69347 GIT binary patch literal 9634 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 1 ? ( + history.push(path)} + > + {NavigationTabs.map(tab => ( + + ))} + + ) : null; +} + +NavigationBar.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(NavigationBar); diff --git a/src/components/NavigationBar/index.js b/src/components/NavigationBar/index.js new file mode 100644 index 00000000..43641a0f --- /dev/null +++ b/src/components/NavigationBar/index.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './NavigationBar'; diff --git a/src/components/NavigationBar/messages.js b/src/components/NavigationBar/messages.js new file mode 100644 index 00000000..ed1a5443 --- /dev/null +++ b/src/components/NavigationBar/messages.js @@ -0,0 +1,16 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + learnerRecords: { + id: 'learnerRecords', + defaultMessage: 'My Learner Records', + description: 'A message of Learner Records navigation tab', + }, + verifiableCredentials: { + id: 'verifiableCredentials', + defaultMessage: 'Verifiable Credentials', + description: 'A message of Verifiable Credentials navigation tab', + }, +}); + +export default messages; diff --git a/src/components/NavigationBar/test/NavigationBar.test.jsx b/src/components/NavigationBar/test/NavigationBar.test.jsx new file mode 100644 index 00000000..2088834f --- /dev/null +++ b/src/components/NavigationBar/test/NavigationBar.test.jsx @@ -0,0 +1,48 @@ +/** + * @jest-environment jsdom + */ +import React from 'react'; +import { mergeConfig } from '@edx/frontend-platform'; +import { + render, screen, cleanup, initializeMockApp, fireEvent, +} from '../../../setupTest'; +import NavigationBar from '..'; + +const mockHistoryPush = jest.fn(); +global.ResizeObserver = require('resize-observer-polyfill'); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: mockHistoryPush, + }), +})); + +describe('navigation-bar', () => { + beforeAll(async () => { + await initializeMockApp(); + }); + beforeEach(() => { + mergeConfig({ ENABLE_VERIFIABLE_CREDENTIALS: 'true' }); + return jest.resetModules; + }); + afterEach(cleanup); + + it('not renders the component with disabled the Verifiable Credentials functionality', () => { + mergeConfig({ ENABLE_VERIFIABLE_CREDENTIALS: false }); + const { container } = render(); + expect(container.innerHTML).toHaveLength(0); + }); + + it('renders the component with enabled the Verifiable Credentials functionality', () => { + render(); + expect(screen.getByText('My Learner Records')).toBeTruthy(); + expect(screen.getByText('Verifiable Credentials')).toBeTruthy(); + }); + + it('redirects the appropriate route on tab click', () => { + render(); + fireEvent.click(screen.getByText('Verifiable Credentials')); + expect(mockHistoryPush).toHaveBeenCalledWith('/verifiable-credentials'); + }); +}); diff --git a/src/components/ProgramCertificate/ProgramCertificate.jsx b/src/components/ProgramCertificate/ProgramCertificate.jsx new file mode 100644 index 00000000..ea4fae86 --- /dev/null +++ b/src/components/ProgramCertificate/ProgramCertificate.jsx @@ -0,0 +1,89 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + FormattedDate, + injectIntl, + intlShape, +} from '@edx/frontend-platform/i18n'; +import { Hyperlink, DropdownButton, Dropdown } from '@edx/paragon'; +import messages from './messages'; + +function ProgramCertificate({ + intl, + program_title: programTitle, + program_org: programOrg, + modified_date: modifiedDate, + uuid, + handleCreate, + storages = [], +}) { + const showSingleAction = storages.length === 1; + + const renderCreationButtons = () => ( +
+ {showSingleAction && ( + handleCreate(uuid, storages[0].id)} + > + {intl.formatMessage(messages.certificateCardActionLabel)} + + )} + {!showSingleAction && ( + + {storages.map(({ id, name }) => ( + handleCreate(uuid, id)}> + {name} + + ))} + + )} +
+ ); + + return ( +
+
+
+
+

+ {intl.formatMessage(messages.certificateCardName)} +

+

{programTitle}

+
+

+ {intl.formatMessage(messages.certificateCardOrgLabel)} +

+

+ {programOrg + || intl.formatMessage(messages.certificateCardNoOrgText)} +

+

+ {intl.formatMessage(messages.certificateCardDateLabel, { + date: , + })} +

+ {renderCreationButtons()} +
+
+
+ ); +} + +ProgramCertificate.propTypes = { + intl: intlShape.isRequired, + program_title: PropTypes.string.isRequired, + program_org: PropTypes.string.isRequired, + modified_date: PropTypes.string.isRequired, + uuid: PropTypes.string.isRequired, + handleCreate: PropTypes.func.isRequired, + storages: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }), + ).isRequired, +}; + +export default injectIntl(ProgramCertificate); diff --git a/src/components/ProgramCertificate/index.js b/src/components/ProgramCertificate/index.js new file mode 100644 index 00000000..1245c0c5 --- /dev/null +++ b/src/components/ProgramCertificate/index.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './ProgramCertificate'; diff --git a/src/components/ProgramCertificate/messages.js b/src/components/ProgramCertificate/messages.js new file mode 100644 index 00000000..ca61de46 --- /dev/null +++ b/src/components/ProgramCertificate/messages.js @@ -0,0 +1,36 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + certificateCardName: { + id: 'certificate.card.name', + defaultMessage: 'Program Certificate', + description: 'A title text of the available program certificate item.', + }, + certificateCardOrgLabel: { + id: 'certificate.card.organization.label', + defaultMessage: 'From', + description: '', + }, + certificateCardNoOrgText: { + id: 'certificate.card.noOrg.text', + defaultMessage: 'No organization', + description: '', + }, + certificateCardDateLabel: { + id: 'certificate.card.date.label', + defaultMessage: 'Awarded on {date}', + description: '', + }, + certificateCardActionLabel: { + id: 'certificate.card.action.label', + defaultMessage: 'Create', + description: 'A text on single action button', + }, + certificateCardMultiActionLabel: { + id: 'certificate.card.multiAction.label', + defaultMessage: 'Create with', + description: 'A text on a dropdown with multiple action options', + }, +}); + +export default messages; diff --git a/src/components/ProgramCertificate/test/ProgramCertificate.test.jsx b/src/components/ProgramCertificate/test/ProgramCertificate.test.jsx new file mode 100644 index 00000000..d3284759 --- /dev/null +++ b/src/components/ProgramCertificate/test/ProgramCertificate.test.jsx @@ -0,0 +1,56 @@ +/** + * @jest-environment jsdom + */ +import React from 'react'; +import { + render, screen, cleanup, initializeMockApp, fireEvent, +} from '../../../setupTest'; +import ProgramCertificate from '..'; + +describe('program-certificate', () => { + beforeAll(async () => { + await initializeMockApp(); + }); + beforeEach(() => jest.resetModules); + afterEach(cleanup); + + const props = { + program_title: 'Program name', + program_org: 'Test org', + modified_date: '2023-02-02', + storages: [{ id: 'storageId', name: 'storageName' }], + handleCreate: jest.fn(), + }; + + it('renders the component', () => { + render(); + expect(screen.getByText('Program Certificate')).toBeTruthy(); + }); + + it('it should display a program name', () => { + render(); + expect(screen.getByText(props.program_title)).toBeTruthy(); + }); + + it('it should display a program organization', () => { + render(); + expect(screen.getByText(props.program_org)).toBeTruthy(); + }); + + it('it should display a program organization', () => { + render(); + expect(screen.getByText('Awarded on 2/2/2023')).toBeTruthy(); + }); + + it('it should display a default org name if it wasn\'t set', () => { + render(); + expect(screen.getByText('No organization')).toBeTruthy(); + }); + + it('renders modal by clicking on a create button', () => { + render(); + fireEvent.click(screen.getByText('Create')); + expect(screen.findByTitle('Verifiable credential')).toBeTruthy(); + expect(screen.findByLabelText('Close')).toBeTruthy(); + }); +}); diff --git a/src/components/ProgramCertificateModal/ProgramCertificateModal.jsx b/src/components/ProgramCertificateModal/ProgramCertificateModal.jsx new file mode 100644 index 00000000..c7836cfc --- /dev/null +++ b/src/components/ProgramCertificateModal/ProgramCertificateModal.jsx @@ -0,0 +1,169 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { BrowserView, MobileView, isBrowser } from 'react-device-detect'; +import { + ActionRow, Button, Row, StandardModal, +} from '@edx/paragon'; +import { Info } from '@edx/paragon/icons'; + +import messages from './messages'; +import appStoreImg from '../../assets/images/appStore.png'; +import googlePlayImg from '../../assets/images/googleplay.png'; + +function ProgramCertificateModal({ + intl, isOpen, close, data, +}) { + const { + deeplink, + qrcode, + app_link_android: appLinkAndroid, + app_link_ios: appLinkIos, + error, + } = data; + + if (error) { + return ( + + + {error} + + ); + } + + return ( + + + + + + + ) : null + } + > + <> + + +
+
+ {intl.formatMessage(messages.certificateModalQrCodeLabel)} +
+
+
+

+ {intl.formatMessage(messages.certificateModalInstructionTitle)} +

+
    +
  1. + {intl.formatMessage( + messages.certificateModalInstructionStep1, + )} +
  2. +
  3. + {intl.formatMessage( + messages.certificateModalInstructionStep2, + )} +
  4. +
  5. + {intl.formatMessage( + messages.certificateModalInstructionStep3, + )} +
  6. +
  7. + {intl.formatMessage( + messages.certificateModalInstructionStep4, + )} +
  8. +
+
+
+
+ +

{intl.formatMessage(messages.certificateModalMobileTitle)}

+ + +

+

    +
  1. + {intl.formatMessage(messages.certificateModalInstructionStep1)} +
  2. +
  3. + {intl.formatMessage(messages.certificateModalInstructionStep2)} +
  4. +
  5. + {intl.formatMessage(messages.certificateModalInstructionStep3)} +
  6. +
  7. + {intl.formatMessage(messages.certificateModalInstructionStep4)} +
  8. +
+ + +
+ +
+ ); +} + +ProgramCertificateModal.propTypes = { + intl: intlShape.isRequired, + isOpen: PropTypes.bool.isRequired, + close: PropTypes.func.isRequired, + data: PropTypes.shape.isRequired, +}; + +export default injectIntl(ProgramCertificateModal); diff --git a/src/components/ProgramCertificateModal/index.js b/src/components/ProgramCertificateModal/index.js new file mode 100644 index 00000000..2b394357 --- /dev/null +++ b/src/components/ProgramCertificateModal/index.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './ProgramCertificateModal'; diff --git a/src/components/ProgramCertificateModal/messages.js b/src/components/ProgramCertificateModal/messages.js new file mode 100644 index 00000000..b4efcc85 --- /dev/null +++ b/src/components/ProgramCertificateModal/messages.js @@ -0,0 +1,81 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + certificateModalTitle: { + id: 'credentials.modal.title', + defaultMessage: 'Verifiable credential', + description: 'Title of a dialog.', + }, + certificateModalCloseBtn: { + id: 'credentials.modal.close.button', + defaultMessage: 'Close modal window', + description: 'Label on button to close a dialog.', + }, + certificateModalCloseMobileBtn: { + id: 'credentials.modal.close.mobile.button', + defaultMessage: 'Cancel', + description: 'Label on button to close a dialog.', + }, + certificateModalMobileTitle: { + id: 'credentials.modal.mobile.title', + defaultMessage: 'To download a verifiable credential to your mobile wallet application, please follow the instructions below.', + description: 'Text for a mobile dialog of the program certificate.', + }, + certificateModalAppStoreBtn: { + id: 'credentials.modal.instruction.appStore.button', + defaultMessage: 'Download the mobile app from the Apple App Store', + description: 'The label for the link to download the apple version of the app.', + }, + certificateModalGooglePlayBtn: { + id: 'credentials.modal.instruction.googlePlay.button', + defaultMessage: 'Download the mobile app from the Google Play', + description: 'The label for the link to download the google version of the app.', + }, + certificateModalInstructionTitle: { + id: 'credentials.modal.instruction.title', + defaultMessage: 'Download and install the app on your smartphone.', + description: 'Title text of the instructions.', + }, + certificateModalInstructionStep1: { + id: 'credentials.modal.instruction.step1', + defaultMessage: 'Sign up for the app to identify yourself.', + description: 'Text of step of the instructions.', + }, + certificateModalInstructionStep2: { + id: 'credentials.modal.instruction.step2', + defaultMessage: 'Open the application and select the option scan the QR code. Scan the provided code.', + description: 'Text of step of the instructions.', + }, + certificateModalInstructionStep3: { + id: 'credentials.modal.instruction.step3', + defaultMessage: 'Follow this instructions below to get verifiable credential:', + description: 'Text of step of the instructions.', + }, + certificateModalInstructionStep4: { + id: 'credentials.modal.instruction.step4', + defaultMessage: 'Once you have successfully done - close modal.', + description: 'Text of step of the instructions.', + }, + certificateModalDeeplinkBtn: { + id: 'credentials.modal.deeplink', + defaultMessage: 'Download Credential', + description: 'The label for the link to download credential.', + }, + certificateModalLoading: { + id: 'credentials.modal.loading', + defaultMessage: 'Loading...', + description: 'Message when data is being loaded', + }, + certificateModalQrCodeLabel: { + id: 'credentials.modal.qrCode.label', + defaultMessage: 'QR code of the credential certificate', + description: 'The label for QR code image', + }, + credentialsModalError: { + id: 'credentials.modal.error', + defaultMessage: 'An error occurred attempting to retrieve your program certificate. Please try again later.', + description: 'An error message indicating there is a problem retrieving the user\'s program certificate data', + }, +}); + +export default messages; diff --git a/src/components/ProgramCertificateModal/test/ProgramCertificateModal.test.jsx b/src/components/ProgramCertificateModal/test/ProgramCertificateModal.test.jsx new file mode 100644 index 00000000..02b1dde2 --- /dev/null +++ b/src/components/ProgramCertificateModal/test/ProgramCertificateModal.test.jsx @@ -0,0 +1,33 @@ +/** + * @jest-environment jsdom + */ +import React from 'react'; +import ProgramCertificateModal from '..'; +import { + render, screen, cleanup, initializeMockApp, +} from '../../../setupTest'; + +const props = { + isOpen: true, + close: jest.fn(), + data: { + deeplink: 'https://example1.com', + qrcode: 'data:image/png;base64,...', + app_link_android: 'https://example2.com', + app_link_ios: 'https://example3.com', + }, +}; + +describe('program-certificate-modal', () => { + beforeAll(async () => { + await initializeMockApp(); + }); + beforeEach(() => jest.resetModules); + afterEach(cleanup); + + it('renders the component', () => { + render(); + expect(screen.getByText('Verifiable credential')).toBeTruthy(); + expect(screen.getByText('Close modal window')).toBeTruthy(); + }); +}); diff --git a/src/components/ProgramCertificatesList/ProgramCertificatesList.jsx b/src/components/ProgramCertificatesList/ProgramCertificatesList.jsx new file mode 100644 index 00000000..937cb58a --- /dev/null +++ b/src/components/ProgramCertificatesList/ProgramCertificatesList.jsx @@ -0,0 +1,184 @@ +import React, { useEffect, useState } from 'react'; + +import { ChevronLeft, Info } from '@edx/paragon/icons'; +import { + Alert, Hyperlink, Row, useToggle, +} from '@edx/paragon'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { getConfig } from '@edx/frontend-platform/config'; +import { logError } from '@edx/frontend-platform/logging'; + +import ProgramCertificate from '../ProgramCertificate'; +import NavigationBar from '../NavigationBar'; +import { + getProgramCertificates, + getAvailableStorages, + initVerifiableCredentialIssuance, +} from './data/service'; +import messages from './messages'; +import ProgramCertificateModal from '../ProgramCertificateModal'; + +function ProgramCertificatesList({ intl }) { + const [certificatesAreLoaded, setCertificatesAreLoaded] = useState(false); + const [dataLoadingIssue, setDataLoadingIssue] = useState(''); + const [certificates, setCertificates] = useState([]); + + const [storagesIsLoaded, setStoragesIsLoaded] = useState(false); + const [storages, setStorages] = useState([]); + + const [modalIsOpen, openModal, closeModal] = useToggle(false); + + const [ + verfifiableCredentialIssuanceData, + setVerifiableCredentialIssuanceData, + ] = useState({}); + + useEffect(() => { + getProgramCertificates() + .then((data) => { + setCertificates(data.program_credentials); + setCertificatesAreLoaded(true); + }) + .catch((error) => { + const errorMessage = intl.formatMessage( + messages.errorProgramCertificatesLoading, + ); + setDataLoadingIssue(errorMessage); + logError(errorMessage + error.message); + }); + }, [intl]); + + useEffect(() => { + getAvailableStorages() + .then((data) => { + setStorages(data); + setStoragesIsLoaded(true); + }) + .catch((error) => { + const errorMessage = intl.formatMessage( + messages.errorAvailableStoragesLoading, + ); + setDataLoadingIssue(errorMessage); + logError(errorMessage + error.message); + }); + }, [intl]); + + const handleCreate = (uuid, storageId) => { + initVerifiableCredentialIssuance({ uuid, storageId }) + .then((data) => { + setVerifiableCredentialIssuanceData(data); + if (data.redirect) { + window.location = data.deeplink; + } else { + openModal(); + } + }) + .catch((error) => { + const errorMessage = intl.formatMessage(messages.errorIssuanceInit); + setVerifiableCredentialIssuanceData({ error: errorMessage }); + openModal(); + logError(errorMessage + error.message); + }); + }; + + const renderProfile = () => { + const { username } = getAuthenticatedUser(); + return ( + + + {intl.formatMessage(messages.credentialsProfileLink)} + + ); + }; + + const renderCredentialsServiceIssueAlert = ({ + message = intl.formatMessage(messages.credentialsListError), + }) => ( +
+ + + {message} + +
+ ); + + const renderEmpty = () => ( +

+ {intl.formatMessage(messages.credentialsListEmpty)} +

+ ); + + const renderProgramCertificates = () => ( +
+

{intl.formatMessage(messages.credentialsDescription)}

+ + {certificates.map((certificate) => ( + + ))} + +
+ ); + + const renderData = () => { + if (dataLoadingIssue) { + return renderCredentialsServiceIssueAlert({ + message: dataLoadingIssue, + }); + } + if (!certificates.length) { + return renderEmpty(); + } + if (!certificatesAreLoaded || !storagesIsLoaded) { + return null; + } + return renderProgramCertificates(); + }; + + const renderHelp = () => ( +
+

+ {intl.formatMessage(messages.credentialsHelpHeader)} +

+ {intl.formatMessage(messages.credentialsHelpDescription)} + + {intl.formatMessage(messages.credentialsHelpLink)} + +
+ ); + + return ( +
+ {renderProfile()} + +

+ {intl.formatMessage(messages.credentialsHeader)} +

+ {renderData()} + {renderHelp()} + +
+ ); +} + +ProgramCertificatesList.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(ProgramCertificatesList); diff --git a/src/components/ProgramCertificatesList/data/service.js b/src/components/ProgramCertificatesList/data/service.js new file mode 100644 index 00000000..51b3d87d --- /dev/null +++ b/src/components/ProgramCertificatesList/data/service.js @@ -0,0 +1,46 @@ +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { getConfig } from '@edx/frontend-platform/config'; + +export async function getProgramCertificates() { + const url = `${ + getConfig().CREDENTIALS_BASE_URL + }/verifiable_credentials/api/v1/program_credentials/`; + let data = {}; + try { + ({ data } = await getAuthenticatedHttpClient().get(url, { + withCredentials: true, + })); + } catch (error) { + // We are catching and suppressing errors here on purpose. If an error occurs during the + // getProgramCertificates call we will pass back an empty `data` object. Downstream we make + // the assumption that if the ProgramCertificates object is empty that there was an issue or + // error communicating with the service/API. + } + return data; +} + +export async function getAvailableStorages() { + const url = `${ + getConfig().CREDENTIALS_BASE_URL + }/verifiable_credentials/api/v1/storages/`; + let data = []; + ({ data } = await getAuthenticatedHttpClient().get(url, { + withCredentials: true, + })); + return data; +} + +export async function initVerifiableCredentialIssuance({ uuid, storageId }) { + const url = `${ + getConfig().CREDENTIALS_BASE_URL + }/verifiable_credentials/api/v1/credentials/init/`; + const requestData = { + credential_uuid: uuid, + storage_id: storageId, + }; + let data = {}; + ({ data } = await getAuthenticatedHttpClient().post(url, requestData, { + withCredentials: true, + })); + return data; +} diff --git a/src/components/ProgramCertificatesList/index.js b/src/components/ProgramCertificatesList/index.js new file mode 100644 index 00000000..a71ed01d --- /dev/null +++ b/src/components/ProgramCertificatesList/index.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './ProgramCertificatesList'; diff --git a/src/components/ProgramCertificatesList/messages.js b/src/components/ProgramCertificatesList/messages.js new file mode 100644 index 00000000..48e130d4 --- /dev/null +++ b/src/components/ProgramCertificatesList/messages.js @@ -0,0 +1,61 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + credentialsProfileLink: { + id: 'credentials.profile.link', + defaultMessage: 'Back to My Profile', + description: 'Link text that redirects logged-in user to their profile page', + }, + credentialsListEmpty: { + id: 'credentials.list.empty', + defaultMessage: 'No certificate available. Finish you first program to get a certificate.', + description: 'A message indicating the user has no program certificates to display on the Verifiable Credentials page', + }, + credentialsListError: { + id: 'credentials.list.error', + defaultMessage: 'An error occurred attempting to retrieve your program certificates. Please try again later.', + description: 'An error message indicating there is a problem retrieving the user\'s program certificates', + }, + credentialsHeader: { + id: 'credentials.header', + defaultMessage: 'Verifiable Credentials', + description: 'Header for the Verifiable Credentials page', + }, + credentialsDescription: { + id: 'credentials.description', + defaultMessage: 'A certificate for a program will appear in the list once you have earned all course certificates in a program.', + description: 'Description of program credentials for the Verifiable Credentials page', + }, + credentialsHelpHeader: { + id: 'credentials.help.header', + defaultMessage: 'Questions about Verifiable Credentials?', + description: 'Header for the help section of Verifiable Credentials page', + }, + credentialsHelpDescription: { + id: 'credentials.help.description', + defaultMessage: 'To learn more about Verifiable Credentials you can ', + description: 'Text description for the help section of Verifiable Credentials page', + }, + credentialsHelpLink: { + id: 'credentials.help.link', + defaultMessage: 'read in our verifiable credentials help area.', + description: 'Text containing link that redirects user to support page', + }, + errorProgramCertificatesLoading: { + id: 'credentials.error.fetch.certificates', + defaultMessage: 'Could not fetch program certificates', + description: 'API data fetching error when program certificates cannot be loaded', + }, + errorAvailableStoragesLoading: { + id: 'credentials.error.fetch.storages', + defaultMessage: 'Could not fetch available storages', + description: 'API data fetching error when storages configuration cannot be loaded', + }, + errorIssuanceInit: { + id: 'credentials.error.issuance.init', + defaultMessage: 'Could not initiate issuance line', + description: 'Verifiable credential issuance init API request has failed', + }, +}); + +export default messages; diff --git a/src/components/ProgramCertificatesList/test/ProgramCertificatesList.test.jsx b/src/components/ProgramCertificatesList/test/ProgramCertificatesList.test.jsx new file mode 100644 index 00000000..6797fb49 --- /dev/null +++ b/src/components/ProgramCertificatesList/test/ProgramCertificatesList.test.jsx @@ -0,0 +1,79 @@ +/** + * @jest-environment jsdom + */ +import React from 'react'; +import { Factory } from 'rosie'; +import MockAdapter from 'axios-mock-adapter'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { getConfig } from '@edx/frontend-platform/config'; +import { + render, screen, cleanup, initializeMockApp, act, +} from '../../../setupTest'; +import ProgramCertificatesList from '..'; +import { getProgramCredentialsFactory, getAvailableStoragesFactory } from './__factories__/programCertificatesList.factory'; + +describe('program-certificates-list', () => { + beforeAll(async () => { + await initializeMockApp(); + }); + beforeEach(() => jest.resetModules); + afterEach(cleanup); + + it('renders the component', () => { + render(); + expect(screen.getByText('Verifiable Credentials')).toBeTruthy(); + }); + + it('it should display a link to the user\'s Profile', () => { + render(); + expect(screen.getByText('Back to My Profile')).toBeTruthy(); + }); + + it('it should have a help section', () => { + render(); + expect(screen.getByText('Questions about Verifiable Credentials?')).toBeTruthy(); + }); +}); + +describe('program-certificates-data', () => { + beforeAll(async () => { + await initializeMockApp(); + }); + afterEach(() => { + cleanup(); + Factory.resetAll(); + }); + + it('should display certificates when data is present', async () => { + await act(async () => { + const axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(`${getConfig().CREDENTIALS_BASE_URL}/verifiable_credentials/api/v1/program_credentials/`) + .reply(200, getProgramCredentialsFactory.build()); + axiosMock + .onGet(`${getConfig().CREDENTIALS_BASE_URL}/verifiable_credentials/api/v1/storages/`) + .reply(200, getAvailableStoragesFactory.buildList(1)); + render(); + }); + + expect(await screen.findByText('Verifiable Credentials')).toBeTruthy(); + expect(await screen.findByText('A certificate for a program will appear in the list once you ' + + 'have earned all course certificates in a program.')).toBeTruthy(); + expect(await screen.findByText('Programm title 1')).toBeTruthy(); + expect(await screen.findByText('Programm org 1')).toBeTruthy(); + }); + + it('should display no certificates when no enrolled_programs are present', async () => { + await act(async () => { + const axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(`${getConfig().CREDENTIALS_BASE_URL}/verifiable_credentials/api/v1/program_credentials/`) + .reply(200, { program_credentials: [] }); + axiosMock + .onGet(`${getConfig().CREDENTIALS_BASE_URL}/verifiable_credentials/api/v1/storages/`) + .reply(200, []); + render(); + }); + expect(await screen.findByText('No certificate available. Finish you first program to get a certificate.')).toBeTruthy(); + }); +}); diff --git a/src/components/ProgramCertificatesList/test/__factories__/programCertificatesList.factory.js b/src/components/ProgramCertificatesList/test/__factories__/programCertificatesList.factory.js new file mode 100644 index 00000000..e19b8022 --- /dev/null +++ b/src/components/ProgramCertificatesList/test/__factories__/programCertificatesList.factory.js @@ -0,0 +1,33 @@ +import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies + +export const getProgramCredentialsFactory = Factory.define('program_credentials') + .attr('program_credentials', [ + { + uuid: '12345', + status: 'awarded', + username: 'honor', + download_url: null, + credential_id: 1, + program_uuid: '54321', + program_title: 'Programm title 1', + program_org: 'Programm org 1', + modified_date: '2022-10-08', + }, + { + uuid: '67890', + status: 'awarded', + username: 'honor', + download_url: null, + credential_id: 1, + program_uuid: '09876', + program_title: 'Programm title 2', + program_org: '', + modified_date: '2023-02-02', + }, + ]); + +export const getAvailableStoragesFactory = Factory.define('storages') + .attrs({ + id: 'test_storage', + name: 'Test Storage Name', + }); diff --git a/src/components/ProgramRecordsList/ProgramRecordsList.jsx b/src/components/ProgramRecordsList/ProgramRecordsList.jsx index 5470347a..9da2ba4a 100644 --- a/src/components/ProgramRecordsList/ProgramRecordsList.jsx +++ b/src/components/ProgramRecordsList/ProgramRecordsList.jsx @@ -10,6 +10,8 @@ import { getConfig } from '@edx/frontend-platform/config'; import { logError } from '@edx/frontend-platform/logging'; import _ from 'lodash'; +import NavigationBar from '../NavigationBar/NavigationBar'; + import getProgramRecords from './data/service'; function ProgramRecordsList() { @@ -70,7 +72,7 @@ function ProgramRecordsList() { ); const renderEmpty = () => ( -

+

+

{renderProfile()} +

{ ReactDOM.render( @@ -29,19 +31,27 @@ subscribe(APP_READY, () => { + {getConfig().ENABLE_VERIFIABLE_CREDENTIALS && ( + + + + )} { mergeConfig({ SUPPORT_URL_LEARNER_RECORDS: process.env.SUPPORT_URL_LEARNER_RECORDS || '', - USE_LR_MFE: process.env.USE_LR_MFE || '', + USE_LR_MFE: process.env.USE_LR_MFE || false, + ENABLE_VERIFIABLE_CREDENTIALS: process.env.ENABLE_VERIFIABLE_CREDENTIALS || false, + SUPPORT_URL_VERIFIABLE_CREDENTIALS: process.env.SUPPORT_URL_VERIFIABLE_CREDENTIALS || '', }, 'LearnerRecordConfig'); }, },