Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,7 @@ SHELLHUB_INTERNAL_HTTP_CLIENT_ENTERPRISE_BASE_URL=http://cloud:8080

# Set false to disable access logs for gateway nginx
SHELLHUB_GATEWAY_ACCESS_LOGS=true

# The URL for the onboarding survey form.
# NOTICE: Leave empty to disable the onboarding survey.
SHELLHUB_ONBOARDING_URL=https://forms.infra.ossystems.io/s/f3fo9q3lkda8rrss9xpjus99
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ services:
- SHELLHUB_CONNECTOR=${SHELLHUB_CONNECTOR}
- SHELLHUB_WEB_ENDPOINTS=${SHELLHUB_WEB_ENDPOINTS}
- SHELLHUB_WEB_ENDPOINTS_DOMAIN=${SHELLHUB_WEB_ENDPOINTS_DOMAIN}
- SHELLHUB_ONBOARDING_URL=${SHELLHUB_ONBOARDING_URL}
networks:
- shellhub
healthcheck:
Expand Down
1 change: 1 addition & 0 deletions ui/src/envVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ export const envVariables = {
stripeKey: env.VUE_APP_SHELLHUB_STRIPE_PUBLISHABLE_KEY as string,
sentryDsn: env.VUE_APP_SHELLHUB_SENTRY_DSN_VERSION as string,
googleAnalyticsID: env.VUE_APP_SHELLHUB_GOOGLE_ANALYTICS_ID as string,
onboardingUrl: env.VUE_APP_SHELLHUB_ONBOARDING_URL as string,
};
6 changes: 4 additions & 2 deletions ui/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,12 @@ export const routes: Array<RouteRecordRaw> = [
requiresAuth: false,
},
beforeEnter: (to, from, next) => {
if (envVariables.isCloud || useUsersStore().systemInfo.setup) {
const forceSetup = to.query.force === "true";
if (!forceSetup && (envVariables.isCloud || useUsersStore().systemInfo.setup)) {
next({ name: "Login" });
} else {
next();
}
next();
},
component: () => import("../views/Setup.vue"),
},
Expand Down
77 changes: 63 additions & 14 deletions ui/src/views/Setup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
Welcome to ShellHub!
</v-card-title>
<v-window v-model="step">
<v-window-item :value="1">
<v-window-item :value="SetupStep.Sign">
<v-card-subtitle
class="text-wrap text-justify px-0"
data-test="subtitle-1"
Expand Down Expand Up @@ -47,46 +47,51 @@
variant="tonal"
block
text="Next"
@click="step = 2"
@click="step = showOnboardingStep ? SetupStep.Onboarding : SetupStep.Account"
/>
</v-window-item>

<v-window-item :value="2">
<v-window-item
v-if="showOnboardingStep"
:value="SetupStep.Onboarding"
>
<v-card-subtitle
class="text-wrap text-center mb-4"
data-test="subtitle-2"
>
Help us improve ShellHub by sharing your feedback
</v-card-subtitle>

<div style="position: relative; height: 500px; overflow: auto;">
<div style="position: relative; height:60dvh; overflow:auto;">
<iframe
:src="formbricksUrl"
:src="onboardingUrl"
frameborder="0"
style="position: absolute; left: 0; top: 0; width: 100%; height: 500px; border: 0; border-radius: 4px;"
style="position: absolute; left:0; top:0; width:100%; height:100%; border:0;"
/>
</div>

<v-card-actions class="mt-4">
<v-btn
color="primary"
variant="text"
@click="step = 1"
@click="step = SetupStep.Sign"
>
Back
</v-btn>
<v-spacer />
<v-btn
:disabled="!surveyCompleted"
color="primary"
variant="tonal"
@click="step = 3"
data-test="continue-btn"
@click="step = SetupStep.Account"
>
Continue
</v-btn>
</v-card-actions>
</v-window-item>

<v-window-item :value="3">
<v-window-item :value="SetupStep.Account">
<v-card-subtitle
class="text-wrap text-center mb-3"
data-test="subtitle-3"
Expand Down Expand Up @@ -153,7 +158,7 @@
<v-btn
color="primary"
variant="text"
@click="step = 2"
@click="step = showOnboardingStep ? SetupStep.Onboarding : SetupStep.Sign"
>
Back
</v-btn>
Expand All @@ -177,25 +182,44 @@
import { ref, computed, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useField } from "vee-validate";
import { useEventListener } from "@vueuse/core";
import * as yup from "yup";
import useUsersStore from "@/store/modules/users";
import CopyCommandField from "@/components/CopyCommandField.vue";
import { envVariables } from "@/envVariables";

enum SetupStep {
Sign = 1,
Onboarding = 2,
Account = 3,
}

const usersStore = useUsersStore();
const router = useRouter();
const route = useRoute();

const showPassword = ref(false);
const showConfirmPassword = ref(false);
const alertMessage = ref("");
const alertType = ref<"success" | "error">("success");
const step = ref<number>(1);
const step = ref<SetupStep>(SetupStep.Sign);
const surveyCompleted = ref(false);
const hasQuery = computed(() => route.query.sign as string);
const formbricksUrl = computed(() => {
const baseUrl = "https://forms.infra.ossystems.io/s/nhq8yq73j9lp3qor3jwxrhs2";

// Onboarding survey is only available in Community Edition
const showOnboardingStep = computed(() => envVariables.isCommunity && !!envVariables.onboardingUrl);

const onboardingUrl = computed(() => {
if (!envVariables.onboardingUrl) {
return "";
}

const baseUrl = envVariables.onboardingUrl;
const params = new URLSearchParams({
consent_to_contact: "accepted",
source: "self-hosted",
embed: "true",
instance_domain: window.location.hostname,
});

if (import.meta.env.DEV) {
Expand Down Expand Up @@ -271,7 +295,32 @@ const isFormValid = computed(() => (
&& !passwordConfirmError.value
));

onMounted(() => { if (hasQuery.value) step.value = 2; });
// Listen for FormBricks survey completion
useEventListener(window, "message", (event: MessageEvent) => {
// Verify the message is from FormBricks
if (!envVariables.onboardingUrl) return;

try {
const formbricksOrigin = new URL(envVariables.onboardingUrl).origin;
if (event.origin !== formbricksOrigin) {
return;
}
} catch {
return;
}

// Check if the survey was completed
// FormBricks sends the completion event as a simple string
if (event.data === "formbricksSurveyCompleted") {
surveyCompleted.value = true;
}
});

onMounted(() => {
if (hasQuery.value) {
step.value = showOnboardingStep.value ? SetupStep.Onboarding : SetupStep.Account;
}
});

const setupAccount = async () => {
if (isFormValid.value) {
Expand Down
2 changes: 2 additions & 0 deletions ui/tests/views/Setup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ describe("Setup Account", () => {
const mockUsersApi = new MockAdapter(usersApi.getAxios());
beforeEach(() => {
envVariables.isCloud = false;
envVariables.isCommunity = true;
envVariables.onboardingUrl = "https://forms.example.com/survey";

wrapper = mount(Setup, {
global: {
Expand Down
Loading