diff --git a/.env.example b/.env.example index 42d6ed47..339471ce 100644 --- a/.env.example +++ b/.env.example @@ -62,4 +62,7 @@ AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false +LEAD_REDIRECT_URL=https://google.es +HIDE_AUTH_BUTTONS=false + VITE_APP_NAME="${APP_NAME}" diff --git a/app/Http/Controllers/Sync/AccountSyncController.php b/app/Http/Controllers/Sync/AccountSyncController.php index 07aa6b0c..62a84ccc 100644 --- a/app/Http/Controllers/Sync/AccountSyncController.php +++ b/app/Http/Controllers/Sync/AccountSyncController.php @@ -26,3 +26,4 @@ class AccountSyncController extends Controller ]); } } + diff --git a/app/Http/Controllers/Sync/BankSyncController.php b/app/Http/Controllers/Sync/BankSyncController.php index 2980df14..c8c63fa6 100644 --- a/app/Http/Controllers/Sync/BankSyncController.php +++ b/app/Http/Controllers/Sync/BankSyncController.php @@ -27,3 +27,4 @@ class BankSyncController extends Controller ]); } } + diff --git a/app/Http/Controllers/UserLeadController.php b/app/Http/Controllers/UserLeadController.php new file mode 100644 index 00000000..011b2583 --- /dev/null +++ b/app/Http/Controllers/UserLeadController.php @@ -0,0 +1,27 @@ +validated()); + + $redirectUrl = config('landing.lead_redirect_url'); + + if ($redirectUrl) { + return response('', 409)->header('X-Inertia-Location', $redirectUrl); + } + + return to_route('home')->with('success', 'Thank you for your interest!'); + } +} diff --git a/app/Http/Requests/StoreUserLeadRequest.php b/app/Http/Requests/StoreUserLeadRequest.php new file mode 100644 index 00000000..d968c79c --- /dev/null +++ b/app/Http/Requests/StoreUserLeadRequest.php @@ -0,0 +1,43 @@ +|string> + */ + public function rules(): array + { + return [ + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:user_leads,email'], + ]; + } + + /** + * Get custom error messages for validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'email.required' => 'Please enter your email address.', + 'email.email' => 'Please enter a valid email address.', + 'email.unique' => 'This email has already been registered.', + ]; + } +} diff --git a/app/Models/UserLead.php b/app/Models/UserLead.php new file mode 100644 index 00000000..8d9acdaa --- /dev/null +++ b/app/Models/UserLead.php @@ -0,0 +1,17 @@ + */ + use HasFactory, HasUuids; + + protected $fillable = [ + 'email', + ]; +} diff --git a/config/landing.php b/config/landing.php new file mode 100644 index 00000000..058c790e --- /dev/null +++ b/config/landing.php @@ -0,0 +1,29 @@ + env('HIDE_AUTH_BUTTONS', false), + + /* + |-------------------------------------------------------------------------- + | Lead Redirect URL + |-------------------------------------------------------------------------- + | + | The external URL to redirect users to after they submit their email + | on the landing page. + | + */ + + 'lead_redirect_url' => env('LEAD_REDIRECT_URL', null), + +]; diff --git a/database/factories/UserLeadFactory.php b/database/factories/UserLeadFactory.php new file mode 100644 index 00000000..e56a2b7a --- /dev/null +++ b/database/factories/UserLeadFactory.php @@ -0,0 +1,23 @@ + + */ +class UserLeadFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'email' => fake()->unique()->safeEmail(), + ]; + } +} diff --git a/database/migrations/2025_11_18_184409_create_user_leads_table.php b/database/migrations/2025_11_18_184409_create_user_leads_table.php new file mode 100644 index 00000000..3b56eddf --- /dev/null +++ b/database/migrations/2025_11_18_184409_create_user_leads_table.php @@ -0,0 +1,28 @@ +char('id', 36)->primary(); + $table->string('email')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_leads'); + } +}; diff --git a/public/images/landing/bank-accounts.png b/public/images/landing/bank-accounts.png new file mode 100644 index 00000000..916acc65 Binary files /dev/null and b/public/images/landing/bank-accounts.png differ diff --git a/public/images/landing/transactions.png b/public/images/landing/transactions.png new file mode 100644 index 00000000..bd345426 Binary files /dev/null and b/public/images/landing/transactions.png differ diff --git a/public/images/landing/unlock-key.png b/public/images/landing/unlock-key.png new file mode 100644 index 00000000..44073301 Binary files /dev/null and b/public/images/landing/unlock-key.png differ diff --git a/public/images/whisper_money_logo.png b/public/images/whisper_money_logo.png new file mode 100644 index 00000000..e68dfc4b Binary files /dev/null and b/public/images/whisper_money_logo.png differ diff --git a/public/logo.svg b/public/logo.svg deleted file mode 100644 index 3b7c63d9..00000000 --- a/public/logo.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/resources/css/app.css b/resources/css/app.css index 7015d586..ecb49c6a 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -13,6 +13,8 @@ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + --font-mono: "Google Sans Code", monospace; + --radius-lg: var(--radius); --radius-md: calc(var(--radius) - 2px); --radius-sm: calc(var(--radius) - 4px); @@ -140,4 +142,58 @@ body { @apply bg-background text-foreground; } + + .fade-bottom { + -webkit-mask-image: linear-gradient(#000 65%, #0000); + mask-image: linear-gradient(#000 65%, #0000); + } + + .bg-radial { + background-image: radial-gradient(ellipse, var(--tw-gradient-stops)); + } + + .glass-3 { + background: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.1), + rgba(255, 255, 255, 0.05) + ); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .dark .glass-3 { + background: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.05), + rgba(255, 255, 255, 0.02) + ); + } + + @keyframes marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(calc(-100% - var(--gap))); + } + } + + + .font-heading { + font-family: 'Stack Sans Text', ui-sans-serif, system-ui, sans-serif; + } + + .animate-marquee { + animation: marquee var(--duration) linear infinite; + } + + .bg-linear-to-r { + background: linear-gradient(to right, var(--tw-gradient-stops)); + } + + .bg-linear-to-l { + background: linear-gradient(to left, var(--tw-gradient-stops)); + } } diff --git a/resources/js/components/app-logo-icon.tsx b/resources/js/components/app-logo-icon.tsx index 9bd62ad8..c34846f3 100644 --- a/resources/js/components/app-logo-icon.tsx +++ b/resources/js/components/app-logo-icon.tsx @@ -1,13 +1,12 @@ -import { SVGAttributes } from 'react'; +import { cn } from '@/lib/utils'; +import { BirdIcon, SVGAttributes } from 'lucide-react'; -export default function AppLogoIcon(props: SVGAttributes) { +export default function AppLogoIcon({ className }: { className?: SVGAttributes['className'] }) { return ( - - - + ); } diff --git a/resources/js/components/app-logo.tsx b/resources/js/components/app-logo.tsx index 42d998fd..e3e7e821 100644 --- a/resources/js/components/app-logo.tsx +++ b/resources/js/components/app-logo.tsx @@ -8,7 +8,7 @@ export default function AppLogo() {
- Laravel Starter Kit + Whisper Money
diff --git a/resources/js/components/sync-status-button.tsx b/resources/js/components/sync-status-button.tsx index 2b000973..be9dca93 100644 --- a/resources/js/components/sync-status-button.tsx +++ b/resources/js/components/sync-status-button.tsx @@ -74,3 +74,4 @@ export function SyncStatusButton() { ); } + diff --git a/resources/js/components/ui/input.tsx b/resources/js/components/ui/input.tsx index 4bfe9cc5..9d53c5a5 100644 --- a/resources/js/components/ui/input.tsx +++ b/resources/js/components/ui/input.tsx @@ -2,20 +2,24 @@ import * as React from "react" import { cn } from "@/lib/utils" -function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - - ) -} +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" export { Input } diff --git a/resources/js/hooks/use-online-status.ts b/resources/js/hooks/use-online-status.ts index 1718aad6..4bf6643d 100644 --- a/resources/js/hooks/use-online-status.ts +++ b/resources/js/hooks/use-online-status.ts @@ -21,3 +21,4 @@ export function useOnlineStatus() { return isOnline; } + diff --git a/resources/js/lib/track-event.ts b/resources/js/lib/track-event.ts new file mode 100644 index 00000000..13627892 --- /dev/null +++ b/resources/js/lib/track-event.ts @@ -0,0 +1,66 @@ +import { usePage } from '@inertiajs/react'; +import type { SharedData } from '@/types'; + +export interface TrackEventPayload { + step: string; + user_id?: string | number; + [key: string]: unknown; +} + +export interface TrackEventOptions { + method?: 'GET' | 'POST'; +} + +function isLocalEnvironment(): boolean { + const hostname = window.location.hostname; + return hostname === 'localhost' || hostname.endsWith('.test'); +} + +export async function trackEvent( + eventUuid: string, + payload: TrackEventPayload, + options: TrackEventOptions = {}, +): Promise { + const { method = 'POST' } = options; + + if (isLocalEnvironment()) { + console.log('[Track Event]', { + eventUuid, + method, + payload, + }); + return; + } + + const url = `https://metricswave.com/webhooks/${eventUuid}`; + + if (method === 'GET') { + const queryParams = new URLSearchParams(); + Object.entries(payload).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + queryParams.append(key, String(value)); + } + }); + await fetch(`${url}?${queryParams.toString()}`); + } else { + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + } +} + +export function useTrackEvent() { + const { auth } = usePage().props; + + return (eventUuid: string, payload: Omit, options?: TrackEventOptions) => { + const finalPayload: TrackEventPayload = { + ...payload, + user_id: auth?.user?.id, + }; + return trackEvent(eventUuid, finalPayload, options); + }; +} diff --git a/resources/js/pages/privacy.tsx b/resources/js/pages/privacy.tsx new file mode 100644 index 00000000..66304fbf --- /dev/null +++ b/resources/js/pages/privacy.tsx @@ -0,0 +1,106 @@ +import { Head, Link } from '@inertiajs/react'; + +export default function Privacy() { + return ( + <> + + + +
+
+ + ← Back to home + + +

+ Privacy Policy +

+ +
+

+ Last updated: {new Date().toLocaleDateString()} +

+ +
+

+ Information We Collect +

+

+ We collect information you provide directly to + us, such as when you create an account, submit + your email address, or communicate with us. This + may include your name, email address, and any + other information you choose to provide. +

+
+ +
+

+ How We Use Your Information +

+

+ We use the information we collect to provide, + maintain, and improve our services, to + communicate with you, and to comply with legal + obligations. +

+
+ +
+

+ Information Sharing +

+

+ We do not sell, trade, or otherwise transfer + your personal information to third parties + without your consent, except as required by law + or as necessary to provide our services. +

+
+ +
+

+ Data Security +

+

+ We implement appropriate technical and + organizational measures to protect your personal + information against unauthorized access, loss, or + misuse. +

+
+ +
+

+ Your Rights +

+

+ You have the right to access, correct, or delete + your personal information. You may also object to + or restrict certain processing of your data. + Please contact us to exercise these rights. +

+
+ +
+

+ Contact Us +

+

+ If you have any questions about this Privacy + Policy, please contact us through our website. +

+
+
+
+
+ + ); +} + diff --git a/resources/js/pages/terms.tsx b/resources/js/pages/terms.tsx new file mode 100644 index 00000000..154db143 --- /dev/null +++ b/resources/js/pages/terms.tsx @@ -0,0 +1,119 @@ +import { Head, Link } from '@inertiajs/react'; + +export default function Terms() { + return ( + <> + + + +
+
+ + ← Back to home + + +

+ Terms of Service +

+ +
+

+ Last updated: {new Date().toLocaleDateString()} +

+ +
+

+ Acceptance of Terms +

+

+ By accessing and using this service, you accept + and agree to be bound by the terms and provisions + of this agreement. If you do not agree to these + terms, please do not use our service. +

+
+ +
+

+ Use of Service +

+

+ You agree to use this service only for lawful + purposes and in accordance with these Terms. You + agree not to use the service in any way that + could damage, disable, overburden, or impair the + service. +

+
+ +
+

+ User Accounts +

+

+ When you create an account with us, you must + provide accurate, complete, and current + information. You are responsible for maintaining + the confidentiality of your account and password. +

+
+ +
+

+ Intellectual Property +

+

+ The service and its original content, features, + and functionality are owned by us and are + protected by international copyright, trademark, + patent, trade secret, and other intellectual + property laws. +

+
+ +
+

+ Limitation of Liability +

+

+ In no event shall we be liable for any indirect, + incidental, special, consequential, or punitive + damages resulting from your use of or inability + to use the service. +

+
+ +
+

+ Changes to Terms +

+

+ We reserve the right to modify or replace these + Terms at any time. If a revision is material, we + will provide at least 30 days notice prior to any + new terms taking effect. +

+
+ +
+

+ Contact Us +

+

+ If you have any questions about these Terms, + please contact us through our website. +

+
+
+
+
+ + ); +} + diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index 93bb676f..567ef53e 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -1,807 +1,818 @@ -import { dashboard, login, register } from '@/routes'; +import { dashboard } from '@/routes'; +import { store } from '@/routes/user-leads'; import { type SharedData } from '@/types'; -import { Head, Link, usePage } from '@inertiajs/react'; +import { Form, Head, Link, usePage } from '@inertiajs/react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import InputError from '@/components/input-error'; +import { Avatar, AvatarImage } from '@/components/ui/avatar'; +import { + LockIcon, + ShieldCheckIcon, + TrendingUpIcon, + BellIcon, + PieChartIcon, + SmartphoneIcon, + ZapIcon, + EyeOffIcon, + BirdIcon, + CodeIcon, +} from 'lucide-react'; +import { useEffect, useRef } from 'react'; +import { trackEvent } from '@/lib/track-event'; export default function Welcome({ canRegister = true, + hideAuthButtons = false, }: { canRegister?: boolean; + hideAuthButtons?: boolean; }) { const { auth } = usePage().props; + const emailInputRef = useRef(null); + const visitTrackedRef = useRef(false); + + useEffect(() => { + if (visitTrackedRef.current) { + return; + } + visitTrackedRef.current = true; + trackEvent('9668a06c-dee9-47a8-9bee-eaaa2a3a5915', { + step: 'Visit', + }); + }, []); return ( <> - - + + + + + + + + + + + + + + + + -
-
- +
-
-
-
-

- Let's get started -

-

- Laravel has an incredibly rich ecosystem. -
- We suggest starting with the following. -

- - + {({ processing, errors }) => ( + <> + + + {errors.email && ( +
+ +
+ )} + + )} + +

+ Your data is yours alone. Join our + waitlist for early access. +

+
+
+ +
+
+
+
+
+ App screenshot showing transactions +
+
+
+
+
+
+
+
+ App screenshot showing transactions +
+
+
+
+
+
+
+
+ App screenshot showing transactions +
+
+
+
+ +
+
+
+
+
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ + +
+
+

+ Everything you need. Nothing you don't. +

+
+
+

+
+ +
+ End-to-end encryption +

+
+ Your financial data is encrypted on your + device. Only you can access it. +
+
+
+

+
+ +
+ Smart budgeting +

+
+ Create budgets that adapt to your + spending habits and goals. +
+
+
+

+
+ +
+ Intelligent insights +

+
+ Are you overspending? + Know exactly where you stand. +
+
+
+

+
+ +
+ Visual insights +

+
+ Understand your spending with beautiful + charts and reports. +
+
+
+

+
+ +
+ Works everywhere +

+
+ Access your finances on any device, + anytime, anywhere. +
+
+
+

+
+ +
+ Lightning fast +

+
+ Built for speed with instant sync and + smooth interactions. +
+
+
+

+
+ +
+ Zero tracking +

+
+ We don't track, sell, or share your + data. Ever. +
+
+
+

+
+ +
+ Open source +

+
+ Fully transparent and open source. Review + the code yourself. +
+
+
- -
-
+ + +
+
+
+

+ Trusted by people who value their privacy +

+

+ Join thousands of users who have taken + control of their finances without + compromising their privacy. +

+
+
+
+
+
+
+
+

+ Sarah M. +

+

+ @sarahm_finance +

+
+
+

+ Finally, a finance app that + respects my privacy. The + encryption gives me peace of + mind. +

+
+
+
+
+

+ Michael R. +

+

+ @mike_tech +

+
+
+

+ The budgeting features are + intuitive and the dark mode is + gorgeous. Best finance app I've + used. +

+
+
+
+
+

+ Emma L. +

+

+ @emmalou +

+
+
+

+ Love that my financial data is + encrypted. No more worrying about + data breaches! +

+
+
+
+
+
+
+

+ Sarah M. +

+

+ @sarahm_finance +

+
+
+

+ Finally, a finance app that + respects my privacy. The + encryption gives me peace of + mind. +

+
+
+
+
+

+ Michael R. +

+

+ @mike_tech +

+
+
+

+ The budgeting features are + intuitive and the dark mode is + gorgeous. Best finance app I've + used. +

+
+
+
+
+

+ Emma L. +

+

+ @emmalou +

+
+
+

+ Love that my financial data is + encrypted. No more worrying about + data breaches! +

+
+
+
+
+
+
+

+ Sarah M. +

+

+ @sarahm_finance +

+
+
+

+ Finally, a finance app that + respects my privacy. The + encryption gives me peace of + mind. +

+
+
+
+
+

+ Michael R. +

+

+ @mike_tech +

+
+
+

+ The budgeting features are + intuitive and the dark mode is + gorgeous. Best finance app I've + used. +

+
+
+
+
+

+ Emma L. +

+

+ @emmalou +

+
+
+

+ Love that my financial data is + encrypted. No more worrying about + data breaches! +

+
+
+
+
+
+
+

+ Sarah M. +

+

+ @sarahm_finance +

+
+
+

+ Finally, a finance app that + respects my privacy. The + encryption gives me peace of + mind. +

+
+
+
+
+

+ Michael R. +

+

+ @mike_tech +

+
+
+

+ The budgeting features are + intuitive and the dark mode is + gorgeous. Best finance app I've + used. +

+
+
+
+
+

+ Emma L. +

+

+ @emmalou +

+
+
+

+ Love that my financial data is + encrypted. No more worrying about + data breaches! +

+
+
+
+
+
+
+
+
+

+ David K. +

+

+ @davidk_dev +

+
+
+

+ As a developer, I appreciate the + security architecture. This is + how finance apps should be built. +

+
+
+
+
+

+ Jessica P. +

+

+ @jessicap +

+
+
+

+ The automation rules save me so + much time. And knowing my data is + private? Priceless. +

+
+
+
+
+

+ Alex T. +

+

+ @alext_money +

+
+
+

+ Clean interface, powerful + features, and zero compromise on + privacy. What more could you + want? +

+
+
+
+
+
+
+

+ David K. +

+

+ @davidk_dev +

+
+
+

+ As a developer, I appreciate the + security architecture. This is + how finance apps should be built. +

+
+
+
+
+

+ Jessica P. +

+

+ @jessicap +

+
+
+

+ The automation rules save me so + much time. And knowing my data is + private? Priceless. +

+
+
+
+
+

+ Alex T. +

+

+ @alext_money +

+
+
+

+ Clean interface, powerful + features, and zero compromise on + privacy. What more could you + want? +

+
+
+
+
+
+
+

+ David K. +

+

+ @davidk_dev +

+
+
+

+ As a developer, I appreciate the + security architecture. This is + how finance apps should be built. +

+
+
+
+
+

+ Jessica P. +

+

+ @jessicap +

+
+
+

+ The automation rules save me so + much time. And knowing my data is + private? Priceless. +

+
+
+
+
+

+ Alex T. +

+

+ @alext_money +

+
+
+

+ Clean interface, powerful + features, and zero compromise on + privacy. What more could you + want? +

+
+
+
+
+
+
+

+ David K. +

+

+ @davidk_dev +

+
+
+

+ As a developer, I appreciate the + security architecture. This is + how finance apps should be built. +

+
+
+
+
+

+ Jessica P. +

+

+ @jessicap +

+
+
+

+ The automation rules save me so + much time. And knowing my data is + private? Priceless. +

+
+
+
+
+

+ Alex T. +

+

+ @alext_money +

+
+
+

+ Clean interface, powerful + features, and zero compromise on + privacy. What more could you + want? +

+
+
+
+
+
+
+
+
+ + +
+
+

+ © {new Date().getFullYear()} Whisper Money. All + rights reserved. +

+
+ + Privacy Policy + + + Terms of Service + +
+
+
); diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index c84bff73..3fdca6dc 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -45,5 +45,10 @@ @inertia + + diff --git a/routes/web.php b/routes/web.php index 0f8fd5af..c90fbc36 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Sync\AccountSyncController; use App\Http\Controllers\Sync\BankSyncController; use App\Http\Controllers\Sync\CategorySyncController; use App\Http\Controllers\TransactionController; +use App\Http\Controllers\UserLeadController; use Illuminate\Support\Facades\Route; use Inertia\Inertia; use Laravel\Fortify\Features; @@ -14,9 +15,30 @@ use Laravel\Fortify\Features; Route::get('/', function () { return Inertia::render('welcome', [ 'canRegister' => Features::enabled(Features::registration()), + 'hideAuthButtons' => config('landing.hide_auth_buttons', false), ]); })->name('home'); +Route::post('user-leads', [UserLeadController::class, 'store'])->name('user-leads.store'); + +Route::get('privacy', function () { + return Inertia::render('privacy'); +})->name('privacy'); + +Route::get('terms', function () { + return Inertia::render('terms'); +})->name('terms'); + +if (config('landing.hide_auth_buttons')) { + Route::match(['GET', 'POST'], 'login', fn () => abort(404)); + Route::match(['GET', 'POST'], 'register', fn () => abort(404)); + Route::post('logout', fn () => abort(404)); + Route::get('forgot-password', fn () => abort(404)); + Route::post('forgot-password', fn () => abort(404)); + Route::get('reset-password/{token}', fn () => abort(404)); + Route::post('reset-password', fn () => abort(404)); +} + Route::middleware(['auth'])->group(function () { Route::get('setup-encryption', function () { return Inertia::render('auth/setup-encryption'); diff --git a/tests/Feature/Sync/AccountSyncTest.php b/tests/Feature/Sync/AccountSyncTest.php index d5be0633..9dda17a1 100644 --- a/tests/Feature/Sync/AccountSyncTest.php +++ b/tests/Feature/Sync/AccountSyncTest.php @@ -78,3 +78,4 @@ it('requires authentication', function () { $response->assertUnauthorized(); }); + diff --git a/tests/Feature/Sync/CategorySyncTest.php b/tests/Feature/Sync/CategorySyncTest.php index 871e91a8..d1a99c9a 100644 --- a/tests/Feature/Sync/CategorySyncTest.php +++ b/tests/Feature/Sync/CategorySyncTest.php @@ -52,3 +52,4 @@ it('requires authentication', function () { $response->assertUnauthorized(); }); +