Feature flags (#543)
* Re-organize a bit controllers * Added the featureflagcontroller * Implement feature flags in the front-end * Clean env files * Clean console.log messages * Fix feature flag test
This commit is contained in:
parent
1dffd27390
commit
79d3dd7888
|
|
@ -4,6 +4,8 @@ APP_KEY=
|
||||||
APP_DEBUG=false
|
APP_DEBUG=false
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
SELF_HOSTED=true
|
||||||
|
|
||||||
LOG_CHANNEL=errorlog
|
LOG_CHANNEL=errorlog
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
|
@ -44,4 +46,3 @@ MUX_WORKSPACE_ID=
|
||||||
MUX_API_TOKEN=
|
MUX_API_TOKEN=
|
||||||
|
|
||||||
OPEN_AI_API_KEY=
|
OPEN_AI_API_KEY=
|
||||||
SELF_HOSTED=true
|
|
||||||
|
|
@ -5,6 +5,8 @@ APP_DEBUG=true
|
||||||
APP_LOG_LEVEL=debug
|
APP_LOG_LEVEL=debug
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
SELF_HOSTED=true
|
||||||
|
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=stack
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
|
@ -87,3 +89,5 @@ GOOGLE_REDIRECT_URL=http://localhost:3000/settings/connections/callback/google
|
||||||
GOOGLE_AUTH_REDIRECT_URL=http://localhost:3000/oauth/google/callback
|
GOOGLE_AUTH_REDIRECT_URL=http://localhost:3000/oauth/google/callback
|
||||||
|
|
||||||
GOOGLE_FONTS_API_KEY=
|
GOOGLE_FONTS_API_KEY=
|
||||||
|
|
||||||
|
ZAPIER_ENABLED=false
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Models\UserInvite;
|
use App\Models\UserInvite;
|
||||||
use App\Models\Workspace;
|
use App\Models\Workspace;
|
||||||
use App\Service\WorkspaceHelper;
|
use App\Service\WorkspaceHelper;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
class UserInviteController extends Controller
|
class UserInviteController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -31,7 +32,7 @@ class UserInviteController extends Controller
|
||||||
return $this->error(['success' => false, 'message' => 'Invite not found for this workspace.']);
|
return $this->error(['success' => false, 'message' => 'Invite not found for this workspace.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if($userInvite->status == UserInvite::ACCEPTED_STATUS) {
|
if ($userInvite->status == UserInvite::ACCEPTED_STATUS) {
|
||||||
return $this->error(['success' => false, 'message' => 'Invite already accepted.']);
|
return $this->error(['success' => false, 'message' => 'Invite already accepted.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,7 +50,7 @@ class UserInviteController extends Controller
|
||||||
return $this->error(['success' => false, 'message' => 'Invite not found for this workspace.']);
|
return $this->error(['success' => false, 'message' => 'Invite not found for this workspace.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if($userInvite->status == UserInvite::ACCEPTED_STATUS) {
|
if ($userInvite->status == UserInvite::ACCEPTED_STATUS) {
|
||||||
return $this->error(['success' => false, 'message' => 'Invite already accepted.']);
|
return $this->error(['success' => false, 'message' => 'Invite already accepted.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Content;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class FeatureFlagsController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$featureFlags = \Cache::remember('feature_flags', 3600, function () {
|
||||||
|
return [
|
||||||
|
'self_hosted' => config('app.self_hosted', true),
|
||||||
|
'custom_domains' => config('custom_domains.enabled', false),
|
||||||
|
'ai_features' => !empty(config('services.openai.api_key')),
|
||||||
|
|
||||||
|
'billing' => [
|
||||||
|
'enabled' => !empty(config('cashier.key')) && !empty(config('cashier.secret')),
|
||||||
|
'appsumo' => !empty(config('services.appsumo.api_key')) && !empty(config('services.appsumo.api_secret')),
|
||||||
|
],
|
||||||
|
'storage' => [
|
||||||
|
'local' => config('filesystems.default') === 'local',
|
||||||
|
's3' => config('filesystems.default') !== 'local',
|
||||||
|
],
|
||||||
|
'services' => [
|
||||||
|
'unsplash' => !empty(config('services.unsplash.access_key')),
|
||||||
|
'google' => [
|
||||||
|
'fonts' => !empty(config('services.google.fonts_api_key')),
|
||||||
|
'auth' => !empty(config('services.google.client_id')) && !empty(config('services.google.client_secret')),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'integrations' => [
|
||||||
|
'zapier' => config('services.zapier.enabled'),
|
||||||
|
'google_sheets' => !empty(config('services.google.client_id')) && !empty(config('services.google.client_secret')),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json($featureFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers\Content;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
class FontsController extends Controller
|
class FontsController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
|
if (!config('services.google.fonts_api_key')) {
|
||||||
|
return response()->json([]);
|
||||||
|
}
|
||||||
|
|
||||||
return \Cache::remember('google_fonts', 60 * 60, function () {
|
return \Cache::remember('google_fonts', 60 * 60, function () {
|
||||||
$url = "https://www.googleapis.com/webfonts/v1/webfonts?sort=popularity&key=" . config('services.google.fonts_api_key');
|
$url = "https://www.googleapis.com/webfonts/v1/webfonts?sort=popularity&key=" . config('services.google.fonts_api_key');
|
||||||
$response = Http::get($url);
|
$response = Http::get($url);
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers\Content;
|
||||||
|
|
||||||
use App\Models\Template;
|
use App\Models\Template;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
class SitemapController extends Controller
|
class SitemapController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -20,7 +21,7 @@ class SitemapController extends Controller
|
||||||
Template::where('publicly_listed', true)->chunk(100, function ($templates) use (&$urls) {
|
Template::where('publicly_listed', true)->chunk(100, function ($templates) use (&$urls) {
|
||||||
foreach ($templates as $template) {
|
foreach ($templates as $template) {
|
||||||
$urls[] = [
|
$urls[] = [
|
||||||
'loc' => '/templates/'.$template->slug,
|
'loc' => '/templates/' . $template->slug,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers\Forms;
|
||||||
|
|
||||||
use App\Http\Requests\Templates\FormTemplateRequest;
|
use App\Http\Requests\Templates\FormTemplateRequest;
|
||||||
use App\Http\Resources\FormTemplateResource;
|
use App\Http\Resources\FormTemplateResource;
|
||||||
use App\Models\Template;
|
use App\Models\Template;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
class TemplateController extends Controller
|
class TemplateController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -74,4 +74,8 @@ return [
|
||||||
'fonts_api_key' => env('GOOGLE_FONTS_API_KEY'),
|
'fonts_api_key' => env('GOOGLE_FONTS_API_KEY'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'zapier' => [
|
||||||
|
'enabled' => env('ZAPIER_ENABLED', false),
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ use App\Http\Controllers\Settings\PasswordController;
|
||||||
use App\Http\Controllers\Settings\ProfileController;
|
use App\Http\Controllers\Settings\ProfileController;
|
||||||
use App\Http\Controllers\Settings\TokenController;
|
use App\Http\Controllers\Settings\TokenController;
|
||||||
use App\Http\Controllers\SubscriptionController;
|
use App\Http\Controllers\SubscriptionController;
|
||||||
use App\Http\Controllers\TemplateController;
|
use App\Http\Controllers\Forms\TemplateController;
|
||||||
use App\Http\Controllers\UserInviteController;
|
use App\Http\Controllers\Auth\UserInviteController;
|
||||||
use App\Http\Controllers\WorkspaceController;
|
use App\Http\Controllers\WorkspaceController;
|
||||||
use App\Http\Controllers\WorkspaceUserController;
|
use App\Http\Controllers\WorkspaceUserController;
|
||||||
use App\Http\Middleware\Form\ResolveFormMiddleware;
|
use App\Http\Middleware\Form\ResolveFormMiddleware;
|
||||||
|
|
@ -309,13 +309,14 @@ Route::prefix('forms')->name('forms.')->group(function () {
|
||||||
* Other public routes
|
* Other public routes
|
||||||
*/
|
*/
|
||||||
Route::prefix('content')->name('content.')->group(function () {
|
Route::prefix('content')->name('content.')->group(function () {
|
||||||
|
Route::get('/feature-flags', [\App\Http\Controllers\Content\FeatureFlagsController::class, 'index'])->name('feature-flags');
|
||||||
Route::get('changelog/entries', [\App\Http\Controllers\Content\ChangelogController::class, 'index'])->name('changelog.entries');
|
Route::get('changelog/entries', [\App\Http\Controllers\Content\ChangelogController::class, 'index'])->name('changelog.entries');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('/sitemap-urls', [\App\Http\Controllers\SitemapController::class, 'index'])->name('sitemap.index');
|
Route::get('/sitemap-urls', [\App\Http\Controllers\Content\SitemapController::class, 'index'])->name('sitemap.index');
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
Route::get('/fonts', [\App\Http\Controllers\FontsController::class, 'index'])->name('fonts.index');
|
Route::get('/fonts', [\App\Http\Controllers\Content\FontsController::class, 'index'])->name('fonts.index');
|
||||||
|
|
||||||
// Templates
|
// Templates
|
||||||
Route::prefix('templates')->group(function () {
|
Route::prefix('templates')->group(function () {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Content\FeatureFlagsController;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
|
||||||
|
it('returns feature flags', function () {
|
||||||
|
// Arrange
|
||||||
|
Config::set('app.self_hosted', false);
|
||||||
|
Config::set('custom_domains.enabled', true);
|
||||||
|
Config::set('cashier.key', 'stripe_key');
|
||||||
|
Config::set('cashier.secret', 'stripe_secret');
|
||||||
|
Config::set('services.appsumo.api_key', 'appsumo_key');
|
||||||
|
Config::set('services.appsumo.api_secret', 'appsumo_secret');
|
||||||
|
Config::set('filesystems.default', 's3');
|
||||||
|
Config::set('services.openai.api_key', 'openai_key');
|
||||||
|
Config::set('services.unsplash.access_key', 'unsplash_key');
|
||||||
|
Config::set('services.google.fonts_api_key', 'google_fonts_key');
|
||||||
|
Config::set('services.google.client_id', 'google_client_id');
|
||||||
|
Config::set('services.google.client_secret', 'google_client_secret');
|
||||||
|
Config::set('services.zapier.enabled', true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('content.feature-flags'));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'self_hosted' => false,
|
||||||
|
'custom_domains' => true,
|
||||||
|
'ai_features' => true,
|
||||||
|
'billing' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'appsumo' => true,
|
||||||
|
],
|
||||||
|
'storage' => [
|
||||||
|
'local' => false,
|
||||||
|
's3' => true,
|
||||||
|
],
|
||||||
|
'services' => [
|
||||||
|
'unsplash' => true,
|
||||||
|
'google' => [
|
||||||
|
'fonts' => true,
|
||||||
|
'auth' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'integrations' => [
|
||||||
|
'zapier' => true,
|
||||||
|
'google_sheets' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('caches feature flags', function () {
|
||||||
|
// Arrange
|
||||||
|
Cache::shouldReceive('remember')
|
||||||
|
->once()
|
||||||
|
->withArgs(function ($key, $ttl, $callback) {
|
||||||
|
return $key === 'feature_flags' && $ttl === 3600 && is_callable($callback);
|
||||||
|
})
|
||||||
|
->andReturn(['some' => 'data']);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$controller = new FeatureFlagsController();
|
||||||
|
$response = $controller->index();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertEquals(['some' => 'data'], $response->getData(true));
|
||||||
|
});
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
NUXT_PUBLIC_APP_URL=/
|
NUXT_PUBLIC_APP_URL=/
|
||||||
NUXT_PUBLIC_API_BASE=/api
|
NUXT_PUBLIC_API_BASE=/api
|
||||||
NUXT_PRIVATE_API_BASE=http://ingress/api
|
NUXT_PRIVATE_API_BASE=http://ingress/api
|
||||||
NUXT_PUBLIC_AI_FEATURES_ENABLED=false
|
|
||||||
NUXT_PUBLIC_ENV=dev
|
NUXT_PUBLIC_ENV=dev
|
||||||
NUXT_PUBLIC_GOOGLE_ANALYTICS_CODE=
|
|
||||||
NUXT_PUBLIC_H_CAPTCHA_SITE_KEY=
|
|
||||||
NUXT_PUBLIC_S3_ENABLED=false
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
NUXT_LOG_LEVEL=
|
NUXT_LOG_LEVEL=
|
||||||
NUXT_PUBLIC_APP_URL=
|
NUXT_PUBLIC_APP_URL=
|
||||||
NUXT_PUBLIC_API_BASE=
|
NUXT_PUBLIC_API_BASE=
|
||||||
NUXT_PUBLIC_AI_FEATURES_ENABLED=
|
|
||||||
NUXT_PUBLIC_AMPLITUDE_CODE=
|
NUXT_PUBLIC_AMPLITUDE_CODE=
|
||||||
NUXT_PUBLIC_CRISP_WEBSITE_ID=
|
NUXT_PUBLIC_CRISP_WEBSITE_ID=
|
||||||
NUXT_PUBLIC_CUSTOM_DOMAINS_ENABLED=
|
|
||||||
NUXT_PUBLIC_ENV=
|
NUXT_PUBLIC_ENV=
|
||||||
NUXT_PUBLIC_GOOGLE_ANALYTICS_CODE=
|
NUXT_PUBLIC_GOOGLE_ANALYTICS_CODE=
|
||||||
NUXT_PUBLIC_H_CAPTCHA_SITE_KEY=
|
NUXT_PUBLIC_H_CAPTCHA_SITE_KEY=
|
||||||
NUXT_PUBLIC_PAID_PLANS_ENABLED=
|
|
||||||
NUXT_PUBLIC_S3_ENABLED=
|
|
||||||
NUXT_API_SECRET=secret
|
NUXT_API_SECRET=secret
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@
|
||||||
:max-date="maxDate"
|
:max-date="maxDate"
|
||||||
:is-dark="props.isDark"
|
:is-dark="props.isDark"
|
||||||
color="form-color"
|
color="form-color"
|
||||||
@update:modelValue="updateModelValue"
|
@update:model-value="updateModelValue"
|
||||||
/>
|
/>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
v-else
|
v-else
|
||||||
|
|
@ -84,7 +84,7 @@
|
||||||
:max-date="maxDate"
|
:max-date="maxDate"
|
||||||
:is-dark="props.isDark"
|
:is-dark="props.isDark"
|
||||||
color="form-color"
|
color="form-color"
|
||||||
@update:modelValue="updateModelValue"
|
@update:model-value="updateModelValue"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</UPopover>
|
</UPopover>
|
||||||
|
|
@ -201,7 +201,7 @@ const formattedDate = (value) => {
|
||||||
try {
|
try {
|
||||||
return format(new Date(value), props.dateFormat + (props.timeFormat == 12 ? ' p':' HH:mm'))
|
return format(new Date(value), props.dateFormat + (props.timeFormat == 12 ? ' p':' HH:mm'))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log('Error formatting date', e)
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="($route.name !== 'ai-form-builder' && user === null) && (!appStore.selfHosted || appStore.aiFeaturesEnabled)"
|
v-if="($route.name !== 'ai-form-builder' && user === null) && (!useFeatureFlag('self_hosted') || useFeatureFlag('ai_features'))"
|
||||||
:to="{ name: 'ai-form-builder' }"
|
:to="{ name: 'ai-form-builder' }"
|
||||||
:class="navLinkClasses"
|
:class="navLinkClasses"
|
||||||
class="hidden lg:inline"
|
class="hidden lg:inline"
|
||||||
|
|
@ -63,9 +63,9 @@
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="
|
v-if="
|
||||||
(appStore.paidPlansEnabled &&
|
(useFeatureFlag('billing.enabled') &&
|
||||||
(user === null || (user && workspace && !workspace.is_pro)) &&
|
(user === null || (user && workspace && !workspace.is_pro)) &&
|
||||||
$route.name !== 'pricing') && !appStore.selfHosted
|
$route.name !== 'pricing') && !isSelfHosted
|
||||||
"
|
"
|
||||||
:to="{ name: 'pricing' }"
|
:to="{ name: 'pricing' }"
|
||||||
:class="navLinkClasses"
|
:class="navLinkClasses"
|
||||||
|
|
@ -248,7 +248,7 @@
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<v-button
|
<v-button
|
||||||
v-if="!appStore.selfHosted"
|
v-if="!isSelfHosted"
|
||||||
v-track.nav_create_form_click
|
v-track.nav_create_form_click
|
||||||
size="small"
|
size="small"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
|
|
@ -274,6 +274,7 @@ import Dropdown from "~/components/global/Dropdown.vue"
|
||||||
import WorkspaceDropdown from "./WorkspaceDropdown.vue"
|
import WorkspaceDropdown from "./WorkspaceDropdown.vue"
|
||||||
import opnformConfig from "~/opnform.config.js"
|
import opnformConfig from "~/opnform.config.js"
|
||||||
import { useRuntimeConfig } from "#app"
|
import { useRuntimeConfig } from "#app"
|
||||||
|
import { useFeatureFlag } from "~/composables/useFeatureFlag"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -294,6 +295,7 @@ export default {
|
||||||
config: useRuntimeConfig(),
|
config: useRuntimeConfig(),
|
||||||
user: computed(() => authStore.user),
|
user: computed(() => authStore.user),
|
||||||
isIframe: useIsIframe(),
|
isIframe: useIsIframe(),
|
||||||
|
isSelfHosted: computed(() => useFeatureFlag('self_hosted')),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ const user = computed(() => authStore.user)
|
||||||
const workspace = computed(() => workspacesStore.getCurrent)
|
const workspace = computed(() => workspacesStore.getCurrent)
|
||||||
|
|
||||||
const shouldDisplayProTag = computed(() => {
|
const shouldDisplayProTag = computed(() => {
|
||||||
if (!useRuntimeConfig().public.paidPlansEnabled) return false
|
if (!useFeatureFlag('billing.enabled')) return false
|
||||||
if (!user.value || !workspace.value) return true
|
if (!user.value || !workspace.value) return true
|
||||||
return !workspace.value.is_pro
|
return !workspace.value.is_pro
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
<ErrorBoundary @on-error="onFormEditorError">
|
<ErrorBoundary @on-error="onFormEditorError">
|
||||||
|
|
||||||
<template #error="{ error, clearError }">
|
<template #error="{ error, clearError }">
|
||||||
<div class="flex-grow w-full flex items-center justify-center flex-col gap-4">
|
<div class="flex-grow w-full flex items-center justify-center flex-col gap-4">
|
||||||
<h1 class="text-blue-800 text-2xl font-medium">Oops! Something went wrong.</h1>
|
<h1 class="text-blue-800 text-2xl font-medium">
|
||||||
<p class="text-gray-500 max-w-lg text-center">It looks like your last action caused an issue on our side. We
|
Oops! Something went wrong.
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-500 max-w-lg text-center">
|
||||||
|
It looks like your last action caused an issue on our side. We
|
||||||
apologize for
|
apologize for
|
||||||
the
|
the
|
||||||
inconvenience.</p>
|
inconvenience.
|
||||||
|
</p>
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="flex gap-2 mt-4">
|
||||||
<UButton icon="i-material-symbols-undo" @click="clearEditorError(error, clearError)">Go back one step
|
<UButton
|
||||||
|
icon="i-material-symbols-undo"
|
||||||
|
@click="clearEditorError(error, clearError)"
|
||||||
|
>
|
||||||
|
Go back one step
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton variant="outline" icon="i-heroicons-chat-bubble-left-right-16-solid"
|
<UButton
|
||||||
@click="onErrorContact(error)">
|
variant="outline"
|
||||||
|
icon="i-heroicons-chat-bubble-left-right-16-solid"
|
||||||
|
@click="onErrorContact(error)"
|
||||||
|
>
|
||||||
Report this error
|
Report this error
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -21,7 +31,7 @@
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
@ -51,7 +61,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const onErrorContact = (error) => {
|
const onErrorContact = (error) => {
|
||||||
console.log('Contacting via crisp for an error', error)
|
|
||||||
crisp.pauseChatBot()
|
crisp.pauseChatBot()
|
||||||
let errorReport = 'Hi there, I have a technical issue with the form editor.'
|
let errorReport = 'Hi there, I have a technical issue with the form editor.'
|
||||||
if (form.value.slug) {
|
if (form.value.slug) {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
on other sites (Open Graph).
|
on other sites (Open Graph).
|
||||||
</p>
|
</p>
|
||||||
<select-input
|
<select-input
|
||||||
v-if="customDomainAllowed"
|
v-if="useFeatureFlag('custom_domains')"
|
||||||
v-model="form.custom_domain"
|
v-model="form.custom_domain"
|
||||||
:clearable="true"
|
:clearable="true"
|
||||||
:disabled="customDomainOptions.length <= 0"
|
:disabled="customDomainOptions.length <= 0"
|
||||||
|
|
@ -98,9 +98,6 @@ export default {
|
||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
},
|
},
|
||||||
customDomainAllowed() {
|
|
||||||
return useRuntimeConfig().public.customDomainsEnabled
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {},
|
watch: {},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@
|
||||||
label="Form Theme"
|
label="Form Theme"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<template v-if="useFeatureFlag('services.google.fonts')">
|
||||||
<label class="text-gray-700 font-medium text-sm">Font Style</label>
|
<label class="text-gray-700 font-medium text-sm">Font Style</label>
|
||||||
<v-button
|
<v-button
|
||||||
color="white"
|
color="white"
|
||||||
|
|
@ -70,6 +71,7 @@
|
||||||
@close="showGoogleFontPicker=false"
|
@close="showGoogleFontPicker=false"
|
||||||
@apply="onApplyFont"
|
@apply="onApplyFont"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="flex space-x-4 justify-stretch">
|
<div class="flex space-x-4 justify-stretch">
|
||||||
<select-input
|
<select-input
|
||||||
|
|
@ -216,7 +218,7 @@ const user = computed(() => authStore.user)
|
||||||
const workspace = computed(() => workspacesStore.getCurrent)
|
const workspace = computed(() => workspacesStore.getCurrent)
|
||||||
|
|
||||||
const isPro = computed(() => {
|
const isPro = computed(() => {
|
||||||
if (!useRuntimeConfig().public.paidPlansEnabled) return true
|
if (!useFeatureFlag('billing.enabled')) return true
|
||||||
if (!user.value || !workspace.value) return false
|
if (!user.value || !workspace.value) return false
|
||||||
return workspace.value.is_pro
|
return workspace.value.is_pro
|
||||||
})
|
})
|
||||||
|
|
@ -237,7 +239,6 @@ const onChangeNoBranding = (val) => {
|
||||||
subscriptionModalStore.openModal()
|
subscriptionModalStore.openModal()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
form.value.no_branding = false
|
form.value.no_branding = false
|
||||||
console.log("form.value.no_branding", form.value.no_branding)
|
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center mt-5 md:mt-0">
|
<div class="flex justify-center mt-5 md:mt-0">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-4 gap-y-2">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-4 gap-y-2">
|
||||||
|
<template v-if="!useFeatureFlag('self_hosted')">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="!appStore.selfHosted"
|
|
||||||
:to="{ name: 'privacy-policy' }"
|
:to="{ name: 'privacy-policy' }"
|
||||||
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
|
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
|
||||||
>
|
>
|
||||||
|
|
@ -30,12 +30,12 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
v-if="!appStore.selfHosted"
|
|
||||||
:to="{ name: 'terms-conditions' }"
|
:to="{ name: 'terms-conditions' }"
|
||||||
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
|
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
|
||||||
>
|
>
|
||||||
Terms & Conditions
|
Terms & Conditions
|
||||||
</router-link>
|
</router-link>
|
||||||
|
</template>
|
||||||
<a
|
<a
|
||||||
:href="opnformConfig.links.feature_requests"
|
:href="opnformConfig.links.feature_requests"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@
|
||||||
</v-button>
|
</v-button>
|
||||||
|
|
||||||
<v-button
|
<v-button
|
||||||
|
v-if="useFeatureFlag('services.google.auth')"
|
||||||
native-type="button"
|
native-type="button"
|
||||||
color="white"
|
color="white"
|
||||||
class="space-x-4 mt-4 flex items-center w-full"
|
class="space-x-4 mt-4 flex items-center w-full"
|
||||||
|
|
@ -72,7 +73,7 @@
|
||||||
<span class="mx-2">Sign in with Google</span>
|
<span class="mx-2">Sign in with Google</span>
|
||||||
</v-button>
|
</v-button>
|
||||||
<p
|
<p
|
||||||
v-if="!appStore.selfHosted"
|
v-if="!useFeatureFlag('self_hosted')"
|
||||||
class="text-gray-500 text-sm text-center mt-4"
|
class="text-gray-500 text-sm text-center mt-4"
|
||||||
>
|
>
|
||||||
Don't have an account?
|
Don't have an account?
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@
|
||||||
Create account
|
Create account
|
||||||
</v-button>
|
</v-button>
|
||||||
|
|
||||||
|
<template v-if="useFeatureFlag('services.google.auth')">
|
||||||
<p class="text-gray-600/50 text-sm text-center my-4">
|
<p class="text-gray-600/50 text-sm text-center my-4">
|
||||||
Or
|
Or
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -100,6 +101,7 @@
|
||||||
<Icon name="devicon:google" />
|
<Icon name="devicon:google" />
|
||||||
<span class="mx-2">Sign in with Google</span>
|
<span class="mx-2">Sign in with Google</span>
|
||||||
</v-button>
|
</v-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<p class="text-gray-500 mt-4 text-sm text-center">
|
<p class="text-gray-500 mt-4 text-sm text-center">
|
||||||
Already have an account?
|
Already have an account?
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="aiFeaturesEnabled"
|
v-if="useFeatureFlag('ai_features')"
|
||||||
v-track.select_form_base="{ base: 'ai' }"
|
v-track.select_form_base="{ base: 'ai' }"
|
||||||
class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50"
|
class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50"
|
||||||
role="button"
|
role="button"
|
||||||
|
|
@ -185,12 +185,6 @@ export default {
|
||||||
loading: false,
|
loading: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
computed: {
|
|
||||||
aiFeaturesEnabled() {
|
|
||||||
return this.runtimeConfig.public.aiFeaturesEnabled
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
generateForm() {
|
generateForm() {
|
||||||
if (this.loading) return
|
if (this.loading) return
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="customDomainsEnabled"
|
v-if="useFeatureFlag('custom_domains')"
|
||||||
id="custom-domains"
|
id="custom-domains"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
|
|
@ -82,10 +82,6 @@ const customDomainsForm = useForm({
|
||||||
const customDomainsLoading = ref(false)
|
const customDomainsLoading = ref(false)
|
||||||
const showCustomDomainModal = ref(false)
|
const showCustomDomainModal = ref(false)
|
||||||
|
|
||||||
const customDomainsEnabled = computed(
|
|
||||||
() => useRuntimeConfig().public.customDomainsEnabled,
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initCustomDomains()
|
initCustomDomains()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ const showEditUserModal = ref(false)
|
||||||
const selectedUser = ref(null)
|
const selectedUser = ref(null)
|
||||||
const userNewRole = ref("")
|
const userNewRole = ref("")
|
||||||
|
|
||||||
const paidPlansEnabled = computed(() => useRuntimeConfig().public.paidPlansEnabled)
|
const paidPlansEnabled = ref(useFeatureFlag('billing.enabled'))
|
||||||
const canInviteUser = computed(() => {
|
const canInviteUser = computed(() => {
|
||||||
return paidPlansEnabled.value ? workspace.value.is_pro : true
|
return paidPlansEnabled.value ? workspace.value.is_pro : true
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
export function useFeatureFlag(flagName, defaultValue = null) {
|
||||||
|
const featureStore = useFeatureFlagsStore()
|
||||||
|
return featureStore.getFlag(flagName, defaultValue)
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ async function storeLocalFile(file) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storeFile = async (file, options = {}) => {
|
export const storeFile = async (file, options = {}) => {
|
||||||
if (!useRuntimeConfig().public.s3Enabled)
|
if (useFeatureFlag('storage.local'))
|
||||||
return storeLocalFile(file, options)
|
return storeLocalFile(file, options)
|
||||||
|
|
||||||
const response = await opnFetch(
|
const response = await opnFetch(
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@ export function darkModeEnabled (elem = ref(null)) {
|
||||||
|
|
||||||
// Update isDark based on the current class
|
// Update isDark based on the current class
|
||||||
const updateIsDark = () => {
|
const updateIsDark = () => {
|
||||||
console.log(elem.value, 'test')
|
|
||||||
const finalElement = elem.value ?? document.body
|
const finalElement = elem.value ?? document.body
|
||||||
isDark.value = finalElement.classList.contains('dark')
|
isDark.value = finalElement.classList.contains('dark')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export const getDomain = function (url) {
|
||||||
*/
|
*/
|
||||||
export const customDomainUsed = function () {
|
export const customDomainUsed = function () {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
if (!config.public.customDomainsEnabled) return false
|
if (!useFeatureFlag('custom_domains')) return false
|
||||||
const appDomain = getDomain(config.public.appUrl)
|
const appDomain = getDomain(config.public.appUrl)
|
||||||
const host = getHost()
|
const host = getHost()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export default defineNuxtRouteMiddleware((to) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.public.customDomainsEnabled) {
|
if (!useFeatureFlag('custom_domains')) {
|
||||||
// If custom domain not allowed, redirect
|
// If custom domain not allowed, redirect
|
||||||
return redirectToMainDomain({
|
return redirectToMainDomain({
|
||||||
reason: "custom_domains_disabled",
|
reason: "custom_domains_disabled",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { useFeatureFlagsStore } from '~/stores/featureFlags'
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware(async () => {
|
||||||
|
const featureFlagsStore = useFeatureFlagsStore()
|
||||||
|
|
||||||
|
if (import.meta.server && Object.keys(featureFlagsStore.flags).length === 0) {
|
||||||
|
await featureFlagsStore.fetchFlags()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -2,7 +2,7 @@ export default defineNuxtRouteMiddleware(() => {
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const runtimeConfig = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
|
||||||
if (runtimeConfig.public?.selfHosted) {
|
if (useFeatureFlag('self_hosted')) {
|
||||||
if (authStore.check && authStore.user?.email === 'admin@opnform.com') {
|
if (authStore.check && authStore.user?.email === 'admin@opnform.com') {
|
||||||
return navigateTo({ name: "update-credentials" })
|
return navigateTo({ name: "update-credentials" })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
export default defineNuxtRouteMiddleware((from, to, next) => {
|
export default defineNuxtRouteMiddleware((from, to, next) => {
|
||||||
const runtimeConfig = useRuntimeConfig()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
if (runtimeConfig.public?.selfHosted) {
|
if (useFeatureFlag('self_hosted')) {
|
||||||
if (from.name === 'register' && route.query?.email && route.query?.invite_token) {
|
if (from.name === 'register' && route.query?.email && route.query?.invite_token) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (from.name === 'ai-form-builder' && runtimeConfig.public?.aiFeaturesEnabled) {
|
if (from.name === 'ai-form-builder' && useFeatureFlag('ai_features')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return navigateTo({ name: "index" })
|
return navigateTo({ name: "index" })
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
<more-features class="pt-56" />
|
<more-features class="pt-56" />
|
||||||
|
|
||||||
<pricing-table
|
<pricing-table
|
||||||
v-if="paidPlansEnabled"
|
v-if="useFeatureFlag('billing.enabled')"
|
||||||
class="pb-20"
|
class="pb-20"
|
||||||
:home-page="true"
|
:home-page="true"
|
||||||
>
|
>
|
||||||
|
|
@ -337,9 +337,6 @@ export default {
|
||||||
configLinks() {
|
configLinks() {
|
||||||
return this.config.links
|
return this.config.links
|
||||||
},
|
},
|
||||||
paidPlansEnabled() {
|
|
||||||
return this.runtimeConfig.public.paidPlansEnabled
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<p class="text-gray-500 text-sm">
|
<p class="text-gray-500 text-sm">
|
||||||
Sign up in less than 2 minutes.
|
Sign up in less than 2 minutes.
|
||||||
</p>
|
</p>
|
||||||
<template v-if="!appStore.selfHosted || isInvited">
|
<template v-if="!useFeatureFlag('self_hosted') || isInvited">
|
||||||
<register-form />
|
<register-form />
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
You can switch to another workspace in top left corner of the page.</small>
|
You can switch to another workspace in top left corner of the page.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex flex-wrap justify-between gap-2">
|
<div class="w-full flex flex-wrap justify-between gap-2">
|
||||||
<WorkSpaceCustomDomains v-if="customDomainsEnabled && !loading" />
|
<WorkSpaceCustomDomains v-if="useFeatureFlag('custom_domains') && !loading" />
|
||||||
<UButton
|
<UButton
|
||||||
label="New Workspace"
|
label="New Workspace"
|
||||||
icon="i-heroicons-plus"
|
icon="i-heroicons-plus"
|
||||||
|
|
@ -116,9 +116,6 @@ const form = useForm({
|
||||||
const workspaceModal = ref(false)
|
const workspaceModal = ref(false)
|
||||||
|
|
||||||
const workspace = computed(() => workspacesStore.getCurrent)
|
const workspace = computed(() => workspacesStore.getCurrent)
|
||||||
const customDomainsEnabled = computed(
|
|
||||||
() => useRuntimeConfig().public.customDomainsEnabled,
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchAllWorkspaces()
|
fetchAllWorkspaces()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { useFeatureFlagsStore } from '~/stores/featureFlags'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
const featureFlagsStore = useFeatureFlagsStore()
|
||||||
|
|
||||||
|
nuxtApp.provide('featureFlag', (key, defaultValue = false) => {
|
||||||
|
return featureFlagsStore.getFlag(key, defaultValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -22,12 +22,8 @@ export default {
|
||||||
gtmCode: process.env.NUXT_PUBLIC_GTM_CODE || null,
|
gtmCode: process.env.NUXT_PUBLIC_GTM_CODE || null,
|
||||||
amplitudeCode: process.env.NUXT_PUBLIC_AMPLITUDE_CODE || null,
|
amplitudeCode: process.env.NUXT_PUBLIC_AMPLITUDE_CODE || null,
|
||||||
crispWebsiteId: process.env.NUXT_PUBLIC_CRISP_WEBSITE_ID || null,
|
crispWebsiteId: process.env.NUXT_PUBLIC_CRISP_WEBSITE_ID || null,
|
||||||
aiFeaturesEnabled: parseBoolean(process.env.NUXT_PUBLIC_AI_FEATURES_ENABLED),
|
|
||||||
s3Enabled: parseBoolean(process.env.NUXT_PUBLIC_S3_ENABLED),
|
|
||||||
paidPlansEnabled: parseBoolean(process.env.NUXT_PUBLIC_PAID_PLANS_ENABLED),
|
|
||||||
customDomainsEnabled: parseBoolean(process.env.NUXT_PUBLIC_CUSTOM_DOMAINS_ENABLED),
|
|
||||||
featureBaseOrganization: process.env.NUXT_PUBLIC_FEATURE_BASE_ORGANISATION || null,
|
featureBaseOrganization: process.env.NUXT_PUBLIC_FEATURE_BASE_ORGANISATION || null,
|
||||||
selfHosted: parseBoolean(process.env.NUXT_PUBLIC_SELF_HOSTED, true),
|
|
||||||
|
|
||||||
// Config within public will be also exposed to the client
|
// Config within public will be also exposed to the client
|
||||||
SENTRY_DSN_PUBLIC: process.env.SENTRY_DSN_PUBLIC,
|
SENTRY_DSN_PUBLIC: process.env.SENTRY_DSN_PUBLIC,
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,7 @@ export const useAppStore = defineStore("app", {
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
paidPlansEnabled: () => useRuntimeConfig().public.paidPlansEnabled,
|
|
||||||
featureBaseEnabled: () => useRuntimeConfig().public.featureBaseOrganization !== null,
|
featureBaseEnabled: () => useRuntimeConfig().public.featureBaseOrganization !== null,
|
||||||
selfHosted: () => useRuntimeConfig().public.selfHosted,
|
|
||||||
aiFeaturesEnabled: () => useRuntimeConfig().public.aiFeaturesEnabled,
|
|
||||||
crispEnabled: () => useRuntimeConfig().public.crispWebsiteId !== null && useRuntimeConfig().public.crispWebsiteId !== '',
|
crispEnabled: () => useRuntimeConfig().public.crispWebsiteId !== null && useRuntimeConfig().public.crispWebsiteId !== '',
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const useFeatureFlagsStore = defineStore('feature_flags', () => {
|
||||||
|
const flags = ref({})
|
||||||
|
|
||||||
|
async function fetchFlags() {
|
||||||
|
try {
|
||||||
|
const { data } = await useOpnApi('/content/feature-flags')
|
||||||
|
flags.value = data.value
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch feature flags:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFlag(path, defaultValue = false) {
|
||||||
|
return path.split('.').reduce((acc, part) => {
|
||||||
|
if (acc === undefined) return defaultValue
|
||||||
|
return acc && acc[part] !== undefined ? acc[part] : defaultValue
|
||||||
|
}, flags.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { flags, fetchFlags, getFlag }
|
||||||
|
})
|
||||||
|
|
@ -10,16 +10,19 @@ export const useFormIntegrationsStore = defineStore("form_integrations", () => {
|
||||||
|
|
||||||
const availableIntegrations = computed(() => {
|
const availableIntegrations = computed(() => {
|
||||||
const user = useAuthStore().user
|
const user = useAuthStore().user
|
||||||
|
const featureFlagsStore = useFeatureFlagsStore()
|
||||||
if (!user) return integrations.value
|
if (!user) return integrations.value
|
||||||
|
|
||||||
const enrichedIntegrations = new Map()
|
const enrichedIntegrations = new Map()
|
||||||
for (const [key, integration] of integrations.value.entries()) {
|
for (const [key, integration] of integrations.value.entries()) {
|
||||||
|
if (featureFlagsStore.getFlag(`integrations.${key}`, true)) {
|
||||||
enrichedIntegrations.set(key, {
|
enrichedIntegrations.set(key, {
|
||||||
...integration,
|
...integration,
|
||||||
id: key,
|
id: key,
|
||||||
requires_subscription: !user.is_subscribed && integration.is_pro,
|
requires_subscription: !user.is_subscribed && integration.is_pro,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return enrichedIntegrations
|
return enrichedIntegrations
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue