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();
+ });
+});