Skip to content

Commit

Permalink
web/satellite: disable some UI features for trial expired users
Browse files Browse the repository at this point in the history
Added new composable to reuse handling of trial expired status.
Restricted create/join project, create AG, create/open/share bucket, add team member, setup application.

Issue:
storj/storj-private#604

Change-Id: Ifc8250e8f86a038080c71870164ca12d48de85e0
  • Loading branch information
VitaliiShpital committed Mar 5, 2024
1 parent f58448e commit d6d38d1
Show file tree
Hide file tree
Showing 18 changed files with 264 additions and 107 deletions.
5 changes: 2 additions & 3 deletions web/satellite/src/App.vue
Expand Up @@ -8,7 +8,7 @@
<router-view />
<trial-expiration-dialog
v-if="!user.paidTier"
v-model="isTrialExpirationDialogShown"
v-model="appStore.state.isExpirationDialogShown"
:expired="user.freezeStatus.trialExpiredFrozen"
/>
</template>
Expand Down Expand Up @@ -54,7 +54,6 @@ const theme = useTheme();
const route = useRoute();
const isLoading = ref<boolean>(true);
const isTrialExpirationDialogShown = ref<boolean>(false);
/**
* Indicates whether an error page should be shown in place of the router view.
Expand Down Expand Up @@ -155,7 +154,7 @@ usersStore.$onAction(({ name, after }) => {
const expirationInfo = user.value.getExpirationInfo(configStore.state.config.daysBeforeTrialEndNotification);
if (user.value.freezeStatus.trialExpiredFrozen || expirationInfo.isCloseToExpiredTrial) {
isTrialExpirationDialogShown.value = true;
appStore.toggleExpirationDialog(true);
}
});
});
Expand Down
9 changes: 7 additions & 2 deletions web/satellite/src/components/ApplicationItem.vue
Expand Up @@ -46,13 +46,16 @@ import { mdiArrowRight, mdiOpenInNew } from '@mdi/js';
import { Application } from '@/types/applications';
import { AccessType, Exposed } from '@/types/createAccessGrant';
import { useTrialCheck } from '@/composables/useTrialCheck';
import CreateAccessDialog from '@/components/dialogs/CreateAccessDialog.vue';
const props = defineProps<{
app: Application
}>();
const { withTrialCheck } = useTrialCheck();
const accessDialog = ref<Exposed>();
const dialog = ref<boolean>(false);
Expand All @@ -61,7 +64,9 @@ const dialog = ref<boolean>(false);
* Starts create S3 credentials flow.
*/
function onSetup(): void {
accessDialog.value?.setTypes([AccessType.S3]);
dialog.value = true;
withTrialCheck(() => {
accessDialog.value?.setTypes([AccessType.S3]);
dialog.value = true;
});
}
</script>
73 changes: 40 additions & 33 deletions web/satellite/src/components/BucketsDataTable.vue
Expand Up @@ -172,6 +172,7 @@ import { RouteConfig } from '@/types/router';
import { EdgeCredentials } from '@/types/accessGrants';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { ROUTES } from '@/router';
import { useTrialCheck } from '@/composables/useTrialCheck';
import IconTrash from '@/components/icons/IconTrash.vue';
import IconShare from '@/components/icons/IconShare.vue';
Expand All @@ -187,8 +188,10 @@ const analyticsStore = useAnalyticsStore();
const bucketsStore = useBucketsStore();
const projectsStore = useProjectsStore();
const configStore = useConfigStore();
const notify = useNotify();
const router = useRouter();
const { withTrialCheck } = useTrialCheck();
const FIRST_PAGE = 1;
const areBucketsFetching = ref<boolean>(true);
Expand Down Expand Up @@ -412,33 +415,35 @@ function onUpdateSort(value: SortItem[]): void {
/**
* Navigates to bucket page.
*/
async function openBucket(bucketName: string): Promise<void> {
if (!bucketName) {
return;
}
bucketsStore.setFileComponentBucketName(bucketName);
if (!promptForPassphrase.value) {
if (!edgeCredentials.value.accessKeyId) {
try {
await bucketsStore.setS3Client(projectsStore.state.selectedProject.id);
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.BUCKET_TABLE);
return;
}
function openBucket(bucketName: string): void {
withTrialCheck(async () => {
if (!bucketName) {
return;
}
bucketsStore.setFileComponentBucketName(bucketName);
if (!promptForPassphrase.value) {
if (!edgeCredentials.value.accessKeyId) {
try {
await bucketsStore.setS3Client(projectsStore.state.selectedProject.id);
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.BUCKET_TABLE);
return;
}
}
analyticsStore.pageVisit(RouteConfig.Buckets.with(RouteConfig.UploadFile).path);
await router.push({
name: ROUTES.Bucket.name,
params: {
browserPath: bucketsStore.state.fileComponentBucketName,
id: projectsStore.state.selectedProject.urlId,
},
});
return;
}
passphraseDialogCallback = () => openBucket(selectedBucketName.value);
isBucketPassphraseDialogOpen.value = true;
analyticsStore.pageVisit(RouteConfig.Buckets.with(RouteConfig.UploadFile).path);
await router.push({
name: ROUTES.Bucket.name,
params: {
browserPath: bucketsStore.state.fileComponentBucketName,
id: projectsStore.state.selectedProject.urlId,
},
});
return;
}
passphraseDialogCallback = () => openBucket(selectedBucketName.value);
isBucketPassphraseDialogOpen.value = true;
});
}
/**
Expand All @@ -461,14 +466,16 @@ function showDeleteBucketDialog(bucketName: string): void {
* Displays the Share Bucket dialog.
*/
function showShareBucketDialog(bucketName: string): void {
shareBucketName.value = bucketName;
if (promptForPassphrase.value) {
bucketsStore.setFileComponentBucketName(bucketName);
isBucketPassphraseDialogOpen.value = true;
passphraseDialogCallback = () => isShareBucketDialogShown.value = true;
return;
}
isShareBucketDialogShown.value = true;
withTrialCheck(() => {
shareBucketName.value = bucketName;
if (promptForPassphrase.value) {
bucketsStore.setFileComponentBucketName(bucketName);
isBucketPassphraseDialogOpen.value = true;
passphraseDialogCallback = () => isShareBucketDialogShown.value = true;
return;
}
isShareBucketDialogShown.value = true;
});
}
/**
Expand Down
1 change: 1 addition & 0 deletions web/satellite/src/components/billing/BillingHistoryTab.vue
Expand Up @@ -7,6 +7,7 @@
:loading="isLoading"
:headers="headers"
:items="historyItems"
:items-length="historyItems.length"
:must-sort="false"
no-data-text="No results found"
hover
Expand Down
Expand Up @@ -101,7 +101,7 @@ const projectsStore = useProjectsStore();
const notify = useNotify();
const props = withDefaults(defineProps<{
projectID: string
projectID?: string
}>(), {
projectID: '',
});
Expand Down
Expand Up @@ -21,7 +21,7 @@
:disabled="currentStep !== OnboardingStep.EncryptionPassphrase"
:prepend-icon="isPassphraseDone ? mdiCheck : ''"
block
@click="isManagePassphraseDialogOpen = true"
@click="onManagePassphrase"
>
Set a Passphrase
</v-btn>
Expand All @@ -45,7 +45,7 @@
:disabled="currentStep !== OnboardingStep.CreateBucket"
:prepend-icon="isBucketDone ? mdiCheck : ''"
block
@click="isBucketDialogOpen = true"
@click="onCreateBucket"
>
Create a Bucket
</v-btn>
Expand Down Expand Up @@ -139,6 +139,7 @@ import { useNotify } from '@/utils/hooks';
import { ROUTES } from '@/router';
import { OnboardingInfo } from '@/types/common';
import { AccessType, Exposed } from '@/types/createAccessGrant';
import { useTrialCheck } from '@/composables/useTrialCheck';

import CreateBucketDialog from '@/components/dialogs/CreateBucketDialog.vue';
import NewAccessDialog from '@/components/dialogs/CreateAccessDialog.vue';
Expand All @@ -153,6 +154,7 @@ const userStore = useUsersStore();

const notify = useNotify();
const router = useRouter();
const { withTrialCheck } = useTrialCheck();

let passphraseDialogCallback: () => void = () => {};

Expand Down Expand Up @@ -243,28 +245,48 @@ const edgeCredentials = computed((): EdgeCredentials => {
return bucketsStore.state.edgeCredentials;
});

/**
* Starts set passphrase flow if user's free trial is not expired.
*/
function onManagePassphrase(): void {
withTrialCheck(() => {
isManagePassphraseDialogOpen.value = true;
});
}

/**
* Starts create bucket flow if user's free trial is not expired.
*/
function onCreateBucket(): void {
withTrialCheck(() => {
isBucketDialogOpen.value = true;
});
}

/**
* Opens the file browser for the bucket being tracked in onboarding if any
* or select the only bucket the user has created
* or redirect to the buckets list.
*/
async function uploadFilesClicked() {
if (trackedBucketName.value) {
await openTrackedBucket();
} else {
await bucketsStore.getBuckets(1, projectsStore.state.selectedProject.id);
const buckets = bucketsStore.state.page.buckets;
if (buckets.length === 1) {
trackedBucketName.value = buckets[0].name;
function uploadFilesClicked(): void {
withTrialCheck(async () => {
if (trackedBucketName.value) {
await openTrackedBucket();
} else {
await progressStep();
await router.push({
name: ROUTES.Buckets.name,
params: { id: selectedProject.value.urlId },
});
await bucketsStore.getBuckets(1, projectsStore.state.selectedProject.id);
const buckets = bucketsStore.state.page.buckets;
if (buckets.length === 1) {
trackedBucketName.value = buckets[0].name;
await openTrackedBucket();
} else {
await progressStep();
await router.push({
name: ROUTES.Buckets.name,
params: { id: selectedProject.value.urlId },
});
}
}
}
});
}

/**
Expand Down Expand Up @@ -299,22 +321,24 @@ async function openTrackedBucket(): Promise<void> {
isBucketPassphraseDialogOpen.value = true;
}

function onBucketCreated(bucketName: string) {
function onBucketCreated(bucketName: string): void {
trackedBucketName.value = bucketName;
progressStep();
}

async function openAccessDialog() {
if (currentStep.value === OnboardingStep.UploadFiles) {
// progress to access step so the onboarding will
// end correctly after the access is created.
await progressStep();
}
accessDialog.value?.setTypes([AccessType.S3]);
isAccessDialogOpen.value = true;
function openAccessDialog(): void {
withTrialCheck(async () => {
if (currentStep.value === OnboardingStep.UploadFiles) {
// progress to access step so the onboarding will
// end correctly after the access is created.
await progressStep();
}
accessDialog.value?.setTypes([AccessType.S3]);
isAccessDialogOpen.value = true;
});
}

function onAccessCreated() {
function onAccessCreated(): void {
// arbitrary delay so the disappear animation
// of the access dialog is visible
setTimeout(() => progressStep(true), 500);
Expand All @@ -325,7 +349,7 @@ function onAccessCreated() {
* and saves the progress in the user settings, conditionally
* ending the onboarding.
*/
async function progressStep(onboardingEnd = false) {
async function progressStep(onboardingEnd = false): Promise<void> {
let onboardingStep = currentStep.value;
switch (userSettings.value.onboardingStep) {
case OnboardingStep.EncryptionPassphrase:
Expand Down Expand Up @@ -357,7 +381,7 @@ async function progressStep(onboardingEnd = false) {
/**
* Dismisses the onboarding stepper and abandons the onboarding process.
*/
async function endOnboarding() {
async function endOnboarding(): Promise<void> {
try {
await userStore.updateSettings({ onboardingEnd: true });
analyticsStore.eventTriggered(AnalyticsEvent.ONBOARDING_ABANDONED);
Expand Down
43 changes: 43 additions & 0 deletions web/satellite/src/composables/useTrialCheck.ts
@@ -0,0 +1,43 @@
// Copyright (C) 2024 Storj Labs, Inc.
// See LICENSE for copying information.

import { computed } from 'vue';

import { useUsersStore } from '@/store/modules/usersStore';
import { useAppStore } from '@/store/modules/appStore';
import { useConfigStore } from '@/store/modules/configStore';
import { User } from '@/types/users';

export function useTrialCheck() {
const userStore = useUsersStore();
const appStore = useAppStore();
const configStore = useConfigStore();

const user = computed<User>(() => userStore.state.user);

const isTrialExpirationBanner = computed<boolean>(() => {
if (user.value.paidTier) return false;

const expirationInfo = user.value.getExpirationInfo(configStore.state.config.daysBeforeTrialEndNotification);

return user.value.freezeStatus.trialExpiredFrozen || expirationInfo.isCloseToExpiredTrial;
});

const isExpired = computed<boolean>(() => user.value.freezeStatus.trialExpiredFrozen);

function withTrialCheck(callback: () => void | Promise<void>): void {
const user = userStore.state.user;
if (!user.paidTier && user.freezeStatus.trialExpiredFrozen) {
appStore.toggleExpirationDialog(true);
return;
}

callback();
}

return {
isTrialExpirationBanner,
isExpired,
withTrialCheck,
};
}

0 comments on commit d6d38d1

Please sign in to comment.