diff --git a/scripts/console.invitation.mjs b/scripts/console.invitation.mjs old mode 100644 new mode 100755 index 7cd773d99..f2c3a5686 --- a/scripts/console.invitation.mjs +++ b/scripts/console.invitation.mjs @@ -1,14 +1,13 @@ #!/usr/bin/env node import { nanoid } from 'nanoid'; -import { consoleActorIC } from './actor.mjs'; +import { consoleActorLocal } from './actor.mjs'; -(async () => { - const actor = await consoleActorIC(); +const actor = await consoleActorLocal(); - const invitationCode = nanoid(); +const invitationCode = nanoid(); - await actor.add_invitation_code(invitationCode); +await actor.add_invitation_code(invitationCode); - console.log('Invitation code:', invitationCode); -})(); +console.log('🏷️ Invitation code:', invitationCode); +console.log('🔗 Redeem URL:', `https://console.juno.build/join?invite=${invitationCode}`); diff --git a/src/console/console.did b/src/console/console.did index f5ddaa496..7dfa6ee20 100644 --- a/src/console/console.did +++ b/src/console/console.did @@ -213,7 +213,7 @@ service : () -> { ) query; init_asset_upload : (InitAssetKey, nat) -> (InitUploadResult); init_proposal : (ProposalType) -> (nat, Proposal); - init_user_mission_control_center : () -> (MissionControl); + init_user_mission_control_center : (opt text) -> (MissionControl); list_assets : (text, ListParams) -> (ListResults) query; list_custom_domains : () -> (vec record { text; CustomDomain }) query; list_payments : () -> (vec record { nat64; Payment }) query; diff --git a/src/console/src/factory/mission_control.rs b/src/console/src/factory/mission_control.rs index a995d84c7..bd332bac2 100644 --- a/src/console/src/factory/mission_control.rs +++ b/src/console/src/factory/mission_control.rs @@ -1,5 +1,5 @@ use crate::controllers::update_mission_control_controllers; -use crate::store::heap::increment_mission_controls_rate; +use crate::store::heap::{increment_mission_controls_rate, redeem_invitation_code}; use crate::store::stable::{ add_mission_control, delete_mission_control, get_mission_control, init_empty_mission_control, }; @@ -13,17 +13,23 @@ use junobuild_shared::types::state::UserId; pub async fn init_user_mission_control( console: &Principal, caller: &Principal, + invitation_code: &Option, ) -> Result { let result = get_mission_control(caller); match result { Err(error) => Err(error.to_string()), Ok(mission_control) => match mission_control { Some(mission_control) => Ok(mission_control), - None => { - // Guard too many requests - increment_mission_controls_rate()?; + None => match invitation_code { + None => Err("No invitation code provided.".to_string()), + Some(invitation_code) => { + // Guard too many requests + increment_mission_controls_rate()?; - create_mission_control(caller, console).await + redeem_invitation_code(caller, invitation_code)?; + + create_mission_control(caller, console).await + } } }, } diff --git a/src/console/src/lib.rs b/src/console/src/lib.rs index fcfc2e584..481cff493 100644 --- a/src/console/src/lib.rs +++ b/src/console/src/lib.rs @@ -155,11 +155,11 @@ fn list_user_mission_control_centers() -> MissionControls { } #[update] -async fn init_user_mission_control_center() -> MissionControl { +async fn init_user_mission_control_center(invitation_code: Option) -> MissionControl { let caller = caller(); let console = id(); - init_user_mission_control(&console, &caller) + init_user_mission_control(&console, &caller, &invitation_code) .await .unwrap_or_else(|e| trap(&e)) } diff --git a/src/declarations/console/console.did.d.ts b/src/declarations/console/console.did.d.ts index 562a3c664..fa40fea24 100644 --- a/src/declarations/console/console.did.d.ts +++ b/src/declarations/console/console.did.d.ts @@ -249,7 +249,7 @@ export interface _SERVICE { >; init_asset_upload: ActorMethod<[InitAssetKey, bigint], InitUploadResult>; init_proposal: ActorMethod<[ProposalType], [bigint, Proposal]>; - init_user_mission_control_center: ActorMethod<[], MissionControl>; + init_user_mission_control_center: ActorMethod<[[] | [string]], MissionControl>; list_assets: ActorMethod<[string, ListParams], ListResults>; list_custom_domains: ActorMethod<[], Array<[string, CustomDomain]>>; list_payments: ActorMethod<[], Array<[bigint, Payment]>>; diff --git a/src/declarations/console/console.factory.did.js b/src/declarations/console/console.factory.did.js index bbf797cb6..b0fbf9e5f 100644 --- a/src/declarations/console/console.factory.did.js +++ b/src/declarations/console/console.factory.did.js @@ -260,7 +260,7 @@ export const idlFactory = ({ IDL }) => { ), init_asset_upload: IDL.Func([InitAssetKey, IDL.Nat], [InitUploadResult], []), init_proposal: IDL.Func([ProposalType], [IDL.Nat, Proposal], []), - init_user_mission_control_center: IDL.Func([], [MissionControl], []), + init_user_mission_control_center: IDL.Func([IDL.Opt(IDL.Text)], [MissionControl], []), list_assets: IDL.Func([IDL.Text, ListParams], [ListResults], ['query']), list_custom_domains: IDL.Func([], [IDL.Vec(IDL.Tuple(IDL.Text, CustomDomain))], ['query']), list_payments: IDL.Func([], [IDL.Vec(IDL.Tuple(IDL.Nat64, Payment))], ['query']), diff --git a/src/declarations/console/console.factory.did.mjs b/src/declarations/console/console.factory.did.mjs index bbf797cb6..b0fbf9e5f 100644 --- a/src/declarations/console/console.factory.did.mjs +++ b/src/declarations/console/console.factory.did.mjs @@ -260,7 +260,7 @@ export const idlFactory = ({ IDL }) => { ), init_asset_upload: IDL.Func([InitAssetKey, IDL.Nat], [InitUploadResult], []), init_proposal: IDL.Func([ProposalType], [IDL.Nat, Proposal], []), - init_user_mission_control_center: IDL.Func([], [MissionControl], []), + init_user_mission_control_center: IDL.Func([IDL.Opt(IDL.Text)], [MissionControl], []), list_assets: IDL.Func([IDL.Text, ListParams], [ListResults], ['query']), list_custom_domains: IDL.Func([], [IDL.Vec(IDL.Tuple(IDL.Text, CustomDomain))], ['query']), list_payments: IDL.Func([], [IDL.Vec(IDL.Tuple(IDL.Nat64, Payment))], ['query']), diff --git a/src/frontend/src/lib/api/console.api.ts b/src/frontend/src/lib/api/console.api.ts index d84ff5589..d20c5d579 100644 --- a/src/frontend/src/lib/api/console.api.ts +++ b/src/frontend/src/lib/api/console.api.ts @@ -1,10 +1,17 @@ import type { MissionControl } from '$declarations/console/console.did'; +import type { OptionInvitationCode } from '$lib/types/console'; import type { OptionIdentity } from '$lib/types/itentity'; import { getConsoleActor } from '$lib/utils/actor.juno.utils'; import type { Principal } from '@dfinity/principal'; -import { fromNullable, isNullish } from '@dfinity/utils'; +import { fromNullable, isNullish, toNullable } from '@dfinity/utils'; -export const initMissionControl = async (identity: OptionIdentity): Promise => { +export const initMissionControl = async ({ + identity, + invitationCode +}: { + identity: OptionIdentity; + invitationCode: OptionInvitationCode; +}): Promise => { const actor = await getConsoleActor(identity); const existingMissionControl: MissionControl | undefined = fromNullable( @@ -12,7 +19,7 @@ export const initMissionControl = async (identity: OptionIdentity): Promise - import { signIn } from '$lib/services/auth.services'; import { isBusy } from '$lib/stores/busy.store'; import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); - - const doSignIn = async (domain: 'internetcomputer.org' | 'ic0.app') => { - dispatch('junoSignIn'); - await signIn({ domain }); - };

Alternatively, use the legacy method at - .

diff --git a/src/frontend/src/lib/components/core/SignIn.svelte b/src/frontend/src/lib/components/core/SignIn.svelte index 5abf69075..e89f1bea6 100644 --- a/src/frontend/src/lib/components/core/SignIn.svelte +++ b/src/frontend/src/lib/components/core/SignIn.svelte @@ -4,6 +4,7 @@ import DeprecatedSignIn from '$lib/components/core/DeprecatedSignIn.svelte'; import IconICMonochrome from '$lib/components/icons/IconICMonochrome.svelte'; import { signIn } from '$lib/services/auth.services'; + import Container from '$lib/components/ui/Container.svelte'; let quotes: string[]; $: quotes = [ @@ -21,59 +22,24 @@ let title: string; $: title = quotes[Math.floor(Math.random() * quotes.length)]; - - -
-
-

{title}

-

{$i18n.sign_in.future}

-
+ const doSignIn = async (domain?: 'internetcomputer.org' | 'ic0.app') => { + await signIn({ domain }); + }; + - -
- - + await doSignIn('internetcomputer.org')} + on:junoSignInDeprecated={async () => await doSignIn('ic0.app')} + /> + + diff --git a/src/frontend/src/lib/components/core/SignInRedeem.svelte b/src/frontend/src/lib/components/core/SignInRedeem.svelte new file mode 100644 index 000000000..36e8d49fc --- /dev/null +++ b/src/frontend/src/lib/components/core/SignInRedeem.svelte @@ -0,0 +1,84 @@ + + + + Enter your code to join Juno and start building. + Unlock Your Invitation + + + + + + +
+

Redeem code

+ +

Enter your code below, and sign in to join Juno.

+ + + + + + await redeemSignIn('internetcomputer.org')} + on:junoSignInDeprecated={async () => await redeemSignIn('ic0.app')} + /> +
+
+ + diff --git a/src/frontend/src/lib/components/ui/Container.svelte b/src/frontend/src/lib/components/ui/Container.svelte new file mode 100644 index 000000000..aec5bae4e --- /dev/null +++ b/src/frontend/src/lib/components/ui/Container.svelte @@ -0,0 +1,58 @@ +
+
+

+ +

+
+ + +
+ + diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index 9f8c5512a..4b245dbfd 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -155,7 +155,7 @@ "quote_8": "Galaxies within grasp forever.", "quote_9": "Navigate stars, chase dreams.", "quote_10": "Universe: endless exploration awaits.", - "future": "Sign in to build your Web3 future.", + "deprecated": "Juno is reserved for existing developers. Invitation upon request.", "internet_identity": "Continue with Internet Identity" }, "satellites": { diff --git a/src/frontend/src/lib/i18n/zh-cn.json b/src/frontend/src/lib/i18n/zh-cn.json index fc6dd2b9d..cbc2a6ec1 100644 --- a/src/frontend/src/lib/i18n/zh-cn.json +++ b/src/frontend/src/lib/i18n/zh-cn.json @@ -155,7 +155,7 @@ "quote_8": "Galaxies within grasp forever.", "quote_9": "Navigate stars, chase dreams.", "quote_10": "Universe: endless exploration awaits.", - "future": "Sign in to build your Web3 future.", + "deprecated": "Juno is reserved for existing developers. Invitation upon request.", "internet_identity": "继续使用Internet Identity" }, "satellites": { diff --git a/src/frontend/src/lib/services/auth.services.ts b/src/frontend/src/lib/services/auth.services.ts index c0e0651fc..db25e187d 100644 --- a/src/frontend/src/lib/services/auth.services.ts +++ b/src/frontend/src/lib/services/auth.services.ts @@ -43,6 +43,14 @@ export const warnSignOut = (text: string): Promise => } }); +export const errorSignOut = (text: string): Promise => + logout({ + msg: { + text, + level: 'error' + } + }); + export const idleSignOut = async () => warnSignOut(get(i18n).authentication.session_expired); const logout = async ({ msg = undefined }: { msg?: ToastMsg }) => { diff --git a/src/frontend/src/lib/services/console.services.ts b/src/frontend/src/lib/services/console.services.ts index 0490e6858..7b06a855d 100644 --- a/src/frontend/src/lib/services/console.services.ts +++ b/src/frontend/src/lib/services/console.services.ts @@ -7,6 +7,7 @@ import { getNewestReleasesMetadata } from '$lib/rest/cdn.rest'; import { authStore } from '$lib/stores/auth.store'; import { toasts } from '$lib/stores/toasts.store'; import { versionStore, type ReleaseVersionSatellite } from '$lib/stores/version.store'; +import type { OptionInvitationCode } from '$lib/types/console'; import { container } from '$lib/utils/juno.utils'; import type { Identity } from '@dfinity/agent'; import type { Principal } from '@dfinity/principal'; @@ -16,22 +17,25 @@ import { get } from 'svelte/store'; export const initMissionControl = async ({ identity, + invitationCode, onInitMissionControlSuccess }: { identity: Identity; + invitationCode: OptionInvitationCode; onInitMissionControlSuccess: (missionControlId: Principal) => Promise; }) => // eslint-disable-next-line no-async-promise-executor new Promise(async (resolve, reject) => { try { const { missionControlId } = await getMissionControl({ - identity + identity, + invitationCode }); if (isNullish(missionControlId)) { setTimeout(async () => { try { - await initMissionControl({ identity, onInitMissionControlSuccess }); + await initMissionControl({ identity, invitationCode, onInitMissionControlSuccess }); resolve(); } catch (err: unknown) { reject(err); @@ -49,9 +53,11 @@ export const initMissionControl = async ({ }); const getMissionControl = async ({ - identity + identity, + invitationCode }: { identity: Identity | undefined; + invitationCode: OptionInvitationCode; }): Promise<{ missionControlId: Principal | undefined; }> => { @@ -59,7 +65,7 @@ const getMissionControl = async ({ throw new Error('Invalid identity.'); } - const mission_control = await initMissionControlApi(identity); + const mission_control = await initMissionControlApi({ identity, invitationCode }); const missionControlId: Principal | undefined = fromNullable( mission_control.mission_control_id diff --git a/src/frontend/src/lib/stores/auth.store.ts b/src/frontend/src/lib/stores/auth.store.ts index 779c4e14a..99171db54 100644 --- a/src/frontend/src/lib/stores/auth.store.ts +++ b/src/frontend/src/lib/stores/auth.store.ts @@ -5,6 +5,7 @@ import { DEV, INTERNET_IDENTITY_CANISTER_ID } from '$lib/constants/constants'; +import type { OptionInvitationCode } from '$lib/types/console'; import type { OptionIdentity } from '$lib/types/itentity'; import { createAuthClient } from '$lib/utils/auth.utils'; import { popupCenter } from '$lib/utils/window.utils'; @@ -13,12 +14,14 @@ import { derived, writable, type Readable } from 'svelte/store'; export interface AuthStoreData { identity: OptionIdentity; + invitationCode: OptionInvitationCode; } let authClient: AuthClient | undefined | null; export interface AuthSignInParams { domain?: 'internetcomputer.org' | 'ic0.app'; + invitationCode?: string; } export interface AuthStore extends Readable { @@ -29,7 +32,8 @@ export interface AuthStore extends Readable { const initAuthStore = (): AuthStore => { const { subscribe, set, update } = writable({ - identity: undefined + identity: undefined, + invitationCode: undefined }); return { @@ -40,11 +44,12 @@ const initAuthStore = (): AuthStore => { const isAuthenticated: boolean = await authClient.isAuthenticated(); set({ - identity: isAuthenticated ? authClient.getIdentity() : null + identity: isAuthenticated ? authClient.getIdentity() : null, + invitationCode: undefined }); }, - signIn: ({ domain }: AuthSignInParams) => + signIn: ({ invitationCode, domain }: AuthSignInParams) => // eslint-disable-next-line no-async-promise-executor new Promise(async (resolve, reject) => { authClient = authClient ?? (await createAuthClient()); @@ -59,7 +64,8 @@ const initAuthStore = (): AuthStore => { onSuccess: () => { update((state: AuthStoreData) => ({ ...state, - identity: authClient?.getIdentity() + identity: authClient?.getIdentity(), + invitationCode })); resolve(); @@ -80,7 +86,8 @@ const initAuthStore = (): AuthStore => { update((state: AuthStoreData) => ({ ...state, - identity: null + identity: null, + invitationCode: null })); } }; diff --git a/src/frontend/src/lib/types/console.ts b/src/frontend/src/lib/types/console.ts new file mode 100644 index 000000000..ca1c2dd0d --- /dev/null +++ b/src/frontend/src/lib/types/console.ts @@ -0,0 +1 @@ +export type OptionInvitationCode = string | undefined | null; diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index d66020649..b84587d2d 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -160,7 +160,7 @@ interface I18nSign_in { quote_8: string; quote_9: string; quote_10: string; - future: string; + deprecated: string; internet_identity: string; } diff --git a/src/frontend/src/routes/(home)/join/+page.svelte b/src/frontend/src/routes/(home)/join/+page.svelte new file mode 100644 index 000000000..c4dda0aea --- /dev/null +++ b/src/frontend/src/routes/(home)/join/+page.svelte @@ -0,0 +1,21 @@ + + +{#if $authSignedInStore} +
+ +
+{:else} + +{/if} diff --git a/src/frontend/src/routes/(home)/join/+page.ts b/src/frontend/src/routes/(home)/join/+page.ts new file mode 100644 index 000000000..4b4a76937 --- /dev/null +++ b/src/frontend/src/routes/(home)/join/+page.ts @@ -0,0 +1,19 @@ +import { browser } from '$app/environment'; +import type { LoadEvent } from '@sveltejs/kit'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = ($event: LoadEvent): { invite: string | null | undefined } => { + if (!browser) { + return { + invite: undefined + }; + } + + const { + url: { searchParams } + } = $event; + + return { + invite: decodeURIComponent(searchParams?.get('invite') ?? '') + }; +}; diff --git a/src/frontend/src/routes/+layout.svelte b/src/frontend/src/routes/+layout.svelte index 4ba4aab7c..7f8ed8008 100644 --- a/src/frontend/src/routes/+layout.svelte +++ b/src/frontend/src/routes/+layout.svelte @@ -7,10 +7,10 @@ import { initAuthWorker } from '$lib/services/worker.auth.services'; import Spinner from '$lib/components/ui/Spinner.svelte'; import Overlays from '$lib/components/core/Overlays.svelte'; - import { toasts } from '$lib/stores/toasts.store'; - import { isNullish } from '@dfinity/utils'; + import { isNullish, nonNullish } from '@dfinity/utils'; import { i18n } from '$lib/stores/i18n.store'; - import { displayAndCleanLogoutMsg, signOut } from '$lib/services/auth.services'; + import { displayAndCleanLogoutMsg, errorSignOut } from '$lib/services/auth.services'; + import { errorDetailToString } from '$lib/utils/error.utils'; const init = async () => await Promise.all([i18n.init(), syncAuthStore()]); @@ -28,7 +28,7 @@ displayAndCleanLogoutMsg(); }; - const initUser = async ({ identity }: AuthStoreData) => { + const initUser = async ({ identity, invitationCode }: AuthStoreData) => { if (isNullish(identity)) { return; } @@ -37,17 +37,17 @@ // Poll to init mission control center await initMissionControl({ identity, + invitationCode, onInitMissionControlSuccess: async (missionControlId) => missionControlStore.set(missionControlId) }); } catch (err: unknown) { - toasts.error({ - text: `Error initializing the user.`, - detail: err - }); + const errMsg = errorDetailToString(err); // There was an error so, we sign the user out otherwise skeleton and other spinners will be displayed forever - await signOut(); + await errorSignOut( + `Error initializing the user.${nonNullish(errMsg) ? ` | Stacktrace: ${errMsg}` : ''}` + ); } }; diff --git a/src/tests/utils/console-tests.utils.ts b/src/tests/utils/console-tests.utils.ts index f04c41acc..ad41438c7 100644 --- a/src/tests/utils/console-tests.utils.ts +++ b/src/tests/utils/console-tests.utils.ts @@ -257,7 +257,7 @@ export const initMissionControls = async ({ actor.setIdentity(user); const { init_user_mission_control_center } = actor; - await init_user_mission_control_center(); + await init_user_mission_control_center([]); await tick(pic); }