diff --git a/app/components/identity/reload.hbs b/app/components/identity/reload.hbs index 373bbab02..93fba5e16 100644 --- a/app/components/identity/reload.hbs +++ b/app/components/identity/reload.hbs @@ -2,5 +2,10 @@
Reload to complete and verify the link between Profile Service and RealDevSquad Service.
\ No newline at end of file + class='identity-box-button' + data-test-blocked-button + type="button" + {{on "click" (fn this.setState "step1")}} +> + Retry + \ No newline at end of file diff --git a/app/components/spinner.hbs b/app/components/spinner.hbs new file mode 100644 index 000000000..0859b065f --- /dev/null +++ b/app/components/spinner.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/constants/error-messages.js b/app/constants/error-messages.js index 18adcff37..c23627d8c 100644 --- a/app/constants/error-messages.js +++ b/app/constants/error-messages.js @@ -1,4 +1,5 @@ export const ERROR_MESSAGES = { somethingWentWrong: 'Something went wrong!', usernameGeneration: 'Username cannot be generated', + notLoggedIn: 'You are not logged in. Please login to continue.', }; diff --git a/app/routes/identity.js b/app/routes/identity.js index c6adb2b6b..76195e220 100644 --- a/app/routes/identity.js +++ b/app/routes/identity.js @@ -1,6 +1,9 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import { APPS } from '../constants/urls'; +import redirectAuth from '../utils/redirect-auth'; +import { TOAST_OPTIONS } from '../constants/toast-options'; +import { ERROR_MESSAGES } from '../constants/error-messages'; export default class IdentityRoute extends Route { @service router; @service login; @@ -8,8 +11,7 @@ export default class IdentityRoute extends Route { beforeModel(transition) { if (transition?.to?.queryParams?.dev !== 'true') { - this.router.transitionTo('page-not-found'); - return; + this.router.transitionTo('/page-not-found'); } } @@ -29,7 +31,8 @@ export default class IdentityRoute extends Route { if (!response.ok) { if (response.status === 401) { - this.router.transitionTo('index'); + this.toast.error(ERROR_MESSAGES.notLoggedIn, '', TOAST_OPTIONS); + setTimeout(redirectAuth, 2000); return null; } throw new Error(`HTTP error! status: ${response.status}`); diff --git a/app/styles/app.css b/app/styles/app.css index 641be47ce..da26a3da7 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -93,3 +93,33 @@ button { #toast-container > div { opacity: 1; } + +.loading { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 90vh; + width: 100%; + text-align: center; +} + +.spinner { + display: inline-block; + width: 1.875 rem; + height: 1.875 rem; + border: 4px solid rgb(0 0 0 / 30%); + border-radius: 50%; + border-top: 4px solid var(--color-black); + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/app/styles/identity.module.css b/app/styles/identity.module.css index 57e53d2ff..25300a3d7 100644 --- a/app/styles/identity.module.css +++ b/app/styles/identity.module.css @@ -231,7 +231,7 @@ } .identity-box-input { - border: 1px solid var(--color-black-50); + border: 1px solid var(--color-black-25); background: var(--color-white); width: 50%; margin-top: 16px; @@ -261,6 +261,31 @@ width: 100%; } +.identity-chaincode-copy-box { + display: flex; + width: 60%; +} + +.identity-chaincode-box { + display: flex; + position: relative; + justify-content: space-between; + align-items: center; + padding-left: 0.5rem; + padding-right: 0.5rem; + margin-top: 16px; + border: 1px solid var(--color-black-25); + background: var(--color-white); + width: 90%; + height: 2rem; + color: var(--color-black); + font-family: Inter, sans-serif; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; +} + @media (width <= 460px) { .identity-box-input { width: 90%; diff --git a/app/templates/identity.hbs b/app/templates/identity.hbs index c80bf5efa..7932dd0a9 100644 --- a/app/templates/identity.hbs +++ b/app/templates/identity.hbs @@ -28,7 +28,7 @@ {{else if (eq this.initialState 'verified')}} {{else if (eq this.initialState 'blocked')}} - + {{/if}} diff --git a/app/templates/loading.hbs b/app/templates/loading.hbs new file mode 100644 index 000000000..fc40c1c0d --- /dev/null +++ b/app/templates/loading.hbs @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/app/utils/redirect-auth.js b/app/utils/redirect-auth.js index af1afa1f3..60d863560 100644 --- a/app/utils/redirect-auth.js +++ b/app/utils/redirect-auth.js @@ -1,5 +1,12 @@ import { AUTH } from '../constants/urls'; +/** + * Redirects to the GitHub authorization URL with the current window's location + * as the redirect URL. + * @function redirectAuth + * @memberof utils + */ + export default function redirectAuth() { let authUrl = AUTH.GITHUB_SIGN_IN; if (typeof window !== 'undefined' && window.location) { diff --git a/tests/unit/routes/identity-test.js b/tests/unit/routes/identity-test.js new file mode 100644 index 000000000..4d983844f --- /dev/null +++ b/tests/unit/routes/identity-test.js @@ -0,0 +1,179 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'website-www/tests/helpers'; +import { TOAST_OPTIONS } from 'website-www/constants/toast-options'; +import { APPS } from 'website-www/constants/urls'; +import sinon from 'sinon'; + +module('Unit | Route | identity', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.route = this.owner.lookup('route:identity'); + + this.route.router = { + transitionTo: sinon.stub(), + }; + this.route.toast = { + error: sinon.stub(), + }; + this.route.fastboot = { + isFastBoot: false, + }; + }); + + test('it exists', function (assert) { + assert.ok(this.route); + }); + + test('beforeModel redirects to page-not-found when dev param is not true', function (assert) { + const transition = { + to: { + queryParams: { + dev: 'false', + }, + }, + }; + + this.route.beforeModel(transition); + assert.ok( + this.route.router.transitionTo.calledWith('/page-not-found'), + 'should redirect to page-not-found', + ); + }); + + test('beforeModel allows navigation when dev param is true', function (assert) { + const transition = { + to: { + queryParams: { + dev: 'true', + }, + }, + }; + + this.route.beforeModel(transition); + assert.notOk(this.route.router.transitionTo.called, 'should not redirect'); + }); + + test('model returns null in FastBoot', async function (assert) { + this.route.fastboot.isFastBoot = true; + const result = await this.route.model(); + assert.strictEqual(result, null, 'should return null in FastBoot'); + }); + + test('model handles 401 unauthorized response', async function (assert) { + const fetchStub = sinon.stub(window, 'fetch').resolves({ + ok: false, + status: 401, + }); + + const result = await this.route.model(); + + assert.strictEqual(result, null, 'should return null'); + assert.ok( + this.route.toast.error.calledWith( + 'You are not logged in. Please login to continue.', + '', + TOAST_OPTIONS, + ), + 'should show error toast', + ); + + fetchStub.restore(); + }); + + test('model handles successful response with invalid discord role', async function (assert) { + const fetchStub = sinon.stub(window, 'fetch').resolves({ + ok: true, + json: () => + Promise.resolve({ + roles: { + in_discord: false, + }, + }), + }); + + const result = await this.route.model(); + + assert.strictEqual(result, null, 'should return null'); + assert.ok( + this.route.router.transitionTo.calledWith('index'), + 'should redirect to index', + ); + + fetchStub.restore(); + }); + + test('model handles network error', async function (assert) { + const fetchStub = sinon + .stub(window, 'fetch') + .rejects(new Error('Network error')); + + const result = await this.route.model(); + + assert.deepEqual(result, null, 'should return null'); + assert.ok( + this.route.router.transitionTo.calledWith('index'), + 'should redirect to index', + ); + + fetchStub.restore(); + }); + + test('model handles non-401 error response', async function (assert) { + const fetchStub = sinon.stub(window, 'fetch').resolves({ + ok: false, + status: 500, + }); + + const result = await this.route.model(); + + assert.strictEqual(result, null, 'should return null'); + assert.ok( + this.route.router.transitionTo.calledWith('index'), + 'should redirect to index', + ); + + fetchStub.restore(); + }); + + test('model handles successful response with valid discord role', async function (assert) { + const mockData = { + roles: { + in_discord: true, + }, + someOtherData: 'test', + }; + + const fetchStub = sinon.stub(window, 'fetch').resolves({ + ok: true, + json: () => Promise.resolve(mockData), + }); + + const result = await this.route.model(); + + assert.deepEqual(result, mockData, 'should return the API response data'); + assert.ok(fetchStub.called, 'fetch should be called'); + + const [actualUrl, actualOptions] = fetchStub.firstCall.args; + + assert.strictEqual( + actualUrl, + `${APPS.API_BACKEND}/users?profile=true`, + 'should call correct URL', + ); + + assert.deepEqual( + actualOptions, + { + credentials: 'include', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }, + 'should pass correct options', + ); + + fetchStub.restore(); + }); +});