Skip to content

Commit

Permalink
Feature flags (#543)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
JhumanJ authored Aug 27, 2024
1 parent 1dffd27 commit 79d3dd7
Show file tree
Hide file tree
Showing 40 changed files with 304 additions and 147 deletions.
5 changes: 3 additions & 2 deletions api/.env.docker
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost

SELF_HOSTED=true

LOG_CHANNEL=errorlog
LOG_LEVEL=debug

Expand Down Expand Up @@ -43,5 +45,4 @@ JWT_SECRET=
MUX_WORKSPACE_ID=
MUX_API_TOKEN=

OPEN_AI_API_KEY=
SELF_HOSTED=true
OPEN_AI_API_KEY=
4 changes: 4 additions & 0 deletions api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://localhost

SELF_HOSTED=true

LOG_CHANNEL=stack
LOG_LEVEL=debug

Expand Down Expand Up @@ -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_FONTS_API_KEY=

ZAPIER_ENABLED=false
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<?php

namespace App\Http\Controllers;
namespace App\Http\Controllers\Auth;

use App\Models\UserInvite;
use App\Models\Workspace;
use App\Service\WorkspaceHelper;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class UserInviteController extends Controller
{
Expand All @@ -31,7 +32,7 @@ public function resendInvite($workspaceId, $inviteId)
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.']);
}

Expand All @@ -49,7 +50,7 @@ public function cancelInvite($workspaceId, $inviteId)
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.']);
}

Expand Down
41 changes: 41 additions & 0 deletions api/app/Http/Controllers/Content/FeatureFlagsController.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
<?php

namespace App\Http\Controllers;
namespace App\Http\Controllers\Content;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use App\Http\Controllers\Controller;

class FontsController extends Controller
{
public function index(Request $request)
{
if (!config('services.google.fonts_api_key')) {
return response()->json([]);
}

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');
$response = Http::get($url);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<?php

namespace App\Http\Controllers;
namespace App\Http\Controllers\Content;

use App\Models\Template;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class SitemapController extends Controller
{
Expand All @@ -20,7 +21,7 @@ private function getTemplatesUrls()
Template::where('publicly_listed', true)->chunk(100, function ($templates) use (&$urls) {
foreach ($templates as $template) {
$urls[] = [
'loc' => '/templates/'.$template->slug,
'loc' => '/templates/' . $template->slug,
];
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<?php

namespace App\Http\Controllers;
namespace App\Http\Controllers\Forms;

use App\Http\Requests\Templates\FormTemplateRequest;
use App\Http\Resources\FormTemplateResource;
use App\Models\Template;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;

class TemplateController extends Controller
{
Expand All @@ -23,7 +24,7 @@ public function index(Request $request)
} else {
$query->where(function ($q) {
$q->where('publicly_listed', true)
->orWhere('creator_id', Auth::id());
->orWhere('creator_id', Auth::id());
});
}
} else {
Expand Down
4 changes: 4 additions & 0 deletions api/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,8 @@
'fonts_api_key' => env('GOOGLE_FONTS_API_KEY'),
],

'zapier' => [
'enabled' => env('ZAPIER_ENABLED', false),
],

];
9 changes: 5 additions & 4 deletions api/routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
use App\Http\Controllers\Settings\ProfileController;
use App\Http\Controllers\Settings\TokenController;
use App\Http\Controllers\SubscriptionController;
use App\Http\Controllers\TemplateController;
use App\Http\Controllers\UserInviteController;
use App\Http\Controllers\Forms\TemplateController;
use App\Http\Controllers\Auth\UserInviteController;
use App\Http\Controllers\WorkspaceController;
use App\Http\Controllers\WorkspaceUserController;
use App\Http\Middleware\Form\ResolveFormMiddleware;
Expand Down Expand Up @@ -309,13 +309,14 @@
* Other public routes
*/
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('/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
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
Route::prefix('templates')->group(function () {
Expand Down
69 changes: 69 additions & 0 deletions api/tests/Feature/FeatureFlagsControllerTest.php
Original file line number Diff line number Diff line change
@@ -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));
});
6 changes: 1 addition & 5 deletions client/.env.docker
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
NUXT_PUBLIC_APP_URL=/
NUXT_PUBLIC_API_BASE=/api
NUXT_PRIVATE_API_BASE=http://ingress/api
NUXT_PUBLIC_AI_FEATURES_ENABLED=false
NUXT_PUBLIC_ENV=dev
NUXT_PUBLIC_GOOGLE_ANALYTICS_CODE=
NUXT_PUBLIC_H_CAPTCHA_SITE_KEY=
NUXT_PUBLIC_S3_ENABLED=false
NUXT_PUBLIC_ENV=dev
4 changes: 0 additions & 4 deletions client/.env.example
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
NUXT_LOG_LEVEL=
NUXT_PUBLIC_APP_URL=
NUXT_PUBLIC_API_BASE=
NUXT_PUBLIC_AI_FEATURES_ENABLED=
NUXT_PUBLIC_AMPLITUDE_CODE=
NUXT_PUBLIC_CRISP_WEBSITE_ID=
NUXT_PUBLIC_CUSTOM_DOMAINS_ENABLED=
NUXT_PUBLIC_ENV=
NUXT_PUBLIC_GOOGLE_ANALYTICS_CODE=
NUXT_PUBLIC_H_CAPTCHA_SITE_KEY=
NUXT_PUBLIC_PAID_PLANS_ENABLED=
NUXT_PUBLIC_S3_ENABLED=
NUXT_API_SECRET=secret
6 changes: 3 additions & 3 deletions client/components/forms/DateInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
:max-date="maxDate"
:is-dark="props.isDark"
color="form-color"
@update:modelValue="updateModelValue"
@update:model-value="updateModelValue"
/>
<DatePicker
v-else
Expand All @@ -84,7 +84,7 @@
:max-date="maxDate"
:is-dark="props.isDark"
color="form-color"
@update:modelValue="updateModelValue"
@update:model-value="updateModelValue"
/>
</template>
</UPopover>
Expand Down Expand Up @@ -201,7 +201,7 @@ const formattedDate = (value) => {
try {
return format(new Date(value), props.dateFormat + (props.timeFormat == 12 ? ' p':' HH:mm'))
} catch (e) {
console.log(e)
console.log('Error formatting date', e)
return ''
}
}
Expand Down
10 changes: 6 additions & 4 deletions client/components/global/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
</a>
</template>
<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' }"
:class="navLinkClasses"
class="hidden lg:inline"
Expand All @@ -63,9 +63,9 @@
</NuxtLink>
<NuxtLink
v-if="
(appStore.paidPlansEnabled &&
(useFeatureFlag('billing.enabled') &&
(user === null || (user && workspace && !workspace.is_pro)) &&
$route.name !== 'pricing') && !appStore.selfHosted
$route.name !== 'pricing') && !isSelfHosted
"
:to="{ name: 'pricing' }"
:class="navLinkClasses"
Expand Down Expand Up @@ -248,7 +248,7 @@
</NuxtLink>

<v-button
v-if="!appStore.selfHosted"
v-if="!isSelfHosted"
v-track.nav_create_form_click
size="small"
class="shrink-0"
Expand All @@ -274,6 +274,7 @@ import Dropdown from "~/components/global/Dropdown.vue"
import WorkspaceDropdown from "./WorkspaceDropdown.vue"
import opnformConfig from "~/opnform.config.js"
import { useRuntimeConfig } from "#app"
import { useFeatureFlag } from "~/composables/useFeatureFlag"
export default {
components: {
Expand All @@ -294,6 +295,7 @@ export default {
config: useRuntimeConfig(),
user: computed(() => authStore.user),
isIframe: useIsIframe(),
isSelfHosted: computed(() => useFeatureFlag('self_hosted')),
}
},
Expand Down
2 changes: 1 addition & 1 deletion client/components/global/ProTag.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const user = computed(() => authStore.user)
const workspace = computed(() => workspacesStore.getCurrent)
const shouldDisplayProTag = computed(() => {
if (!useRuntimeConfig().public.paidPlansEnabled) return false
if (!useFeatureFlag('billing.enabled')) return false
if (!user.value || !workspace.value) return true
return !workspace.value.is_pro
})
Expand Down
Loading

0 comments on commit 79d3dd7

Please sign in to comment.