Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add New Services and Notification Feature #21

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
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
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,23 @@ src/
### SQL Script for Creating the `subscriptions` Table in Supabase:

```sql
create table subscriptions (
id uuid primary key default uuid_generate_v4(),
user_id uuid not null references auth.users(id) on delete cascade,
stripe_subscription_id text unique,
plan text check (plan in ('free', 'starter', 'creator', 'pro')) not null default 'free',
status text check (status in ('active', 'canceled', 'past_due', 'incomplete', 'trialing')) not null default 'active',
current_period_start timestamp with time zone,
current_period_end timestamp with time zone,
created_at timestamp with time zone default now()
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
stripe_subscription_id TEXT UNIQUE,
plan TEXT CHECK (plan IN ('free', 'starter', 'creator', 'pro')) NOT NULL DEFAULT 'free',
status TEXT CHECK (status IN ('active', 'canceled', 'past_due', 'incomplete', 'trialing')) NOT NULL DEFAULT 'active',
current_period_start TIMESTAMP WITH TIME ZONE,
current_period_end TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
message TEXT NOT NULL,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
```

Expand Down
6 changes: 3 additions & 3 deletions src/app/(domains)/(auth)/confirm-signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import Spinner from "@/components/Spinner";
import { useI18n } from '@/hooks/useI18n';
import { supabase } from "@/libs/supabase/client";
import SupabaseService from "@/services/supabase";
import AuthService from "@/services/auth";

type State = {
isLoading: boolean;
Expand Down Expand Up @@ -57,14 +57,14 @@
} else {
dispatch({ type: "CONFIRMATION_FAILURE", error: translate("confirm-signup-error-token-missing") });
}
}, []);

Check warning on line 60 in src/app/(domains)/(auth)/confirm-signup/page.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has missing dependencies: 'handleConfirmSignup' and 'translate'. Either include them or remove the dependency array

Check warning on line 60 in src/app/(domains)/(auth)/confirm-signup/page.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has missing dependencies: 'handleConfirmSignup' and 'translate'. Either include them or remove the dependency array

async function handleConfirmSignup(token: string) {
dispatch({ type: "SET_LOADING", isLoading: true });
const SupabaseServiceInstance = new SupabaseService(supabase);
const AuthServiceInstance = new AuthService(supabase);

try {
const response = await SupabaseServiceInstance.confirmEmail(token, 'signup');
const response = await AuthServiceInstance.confirmEmail(token, 'signup');
if (response?.id) {
dispatch({ type: "CONFIRMATION_SUCCESS" });
} else {
Expand Down
6 changes: 3 additions & 3 deletions src/app/(domains)/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ButtonComponent from "@/components/Button";
import InputComponent from "@/components/Input";
import { useI18n } from "@/hooks/useI18n";
import { supabase } from "@/libs/supabase/client";
import SupabaseService from "@/services/supabase";
import AuthService from "@/services/auth";
import { isValidEmail } from "@/utils/isValidEmail";

const initialState = {
Expand Down Expand Up @@ -66,8 +66,8 @@ export default function ForgotPassword() {
throw new Error("Validation Error");
}

const SupabaseServiceInstance = new SupabaseService(supabase);
const response = await SupabaseServiceInstance.forgotPassword(state.inputValue.email);
const AuthServiceInstance = new AuthService(supabase);
const response = await AuthServiceInstance.forgotPassword(state.inputValue.email);

if (response) {
dispatch({ type: "SET_SUCCESS", payload: true });
Expand Down
8 changes: 4 additions & 4 deletions src/app/(domains)/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { redirect } from 'next/navigation';

import { createClient } from '@/libs/supabase/server';
import SupabaseService from '@/services/supabase';
import AuthService from '@/services/auth';

type Props = {
children: React.ReactNode;
}

export default async function AuthLayout({ children }: Props) {
const supabase = await createClient();
const SupabaseServiceInstance = new SupabaseService(supabase);

const userId = await SupabaseServiceInstance.getUserId();
const AuthServiceInstance = new AuthService(supabase);
const userId = await AuthServiceInstance.getUserId();

if (userId) {
redirect('/dashboard');
}
Expand Down
6 changes: 3 additions & 3 deletions src/app/(domains)/(auth)/new-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import PasswordStrengthIndicator from "@/components/PasswordStrength";
import { useI18n } from '@/hooks/useI18n';
import { supabase } from "@/libs/supabase/client";
import SupabaseService from "@/services/supabase";
import AuthService from "@/services/auth";

const initialState = {
isLoading: false,
Expand Down Expand Up @@ -69,7 +69,7 @@
} else {
dispatch({ type: "SET_TOKEN_VALUE", payload: token });
}
}, [searchParams]);

Check warning on line 72 in src/app/(domains)/(auth)/new-password/page.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'translate'. Either include it or remove the dependency array

Check warning on line 72 in src/app/(domains)/(auth)/new-password/page.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'translate'. Either include it or remove the dependency array

async function handleNewPassword() {
try {
Expand All @@ -90,9 +90,9 @@
throw new Error("Validation Error");
}

const SupabaseServiceInstance = new SupabaseService(supabase);
const AuthServiceInstance = new AuthService(supabase);

const response = await SupabaseServiceInstance.newPassword(state.inputValue.password);
const response = await AuthServiceInstance.updatePassword(state.inputValue.password);

if (response) {
dispatch({ type: "SET_PASSWORD_CHANGED", payload: true });
Expand Down
6 changes: 3 additions & 3 deletions src/app/(domains)/(auth)/signin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import OAuth from "@/components/OAuth";
import { ROUTES } from '@/constants/ROUTES';
import { useI18n } from '@/hooks/useI18n';
import { supabase } from '@/libs/supabase/client';
import SupabaseService from '@/services/supabase';
import AuthService from '@/services/auth';
import { isValidEmail } from '@/utils/isValidEmail';

const initialState = {
Expand Down Expand Up @@ -72,8 +72,8 @@ export default function SignIn() {
throw new Error("Validation Error");
}

const SupabaseServiceInstance = new SupabaseService(supabase);
const response = await SupabaseServiceInstance.signIn(state.inputValue.email, state.inputValue.password);
const AuthServiceInstance = new AuthService(supabase);
const response = await AuthServiceInstance.signIn(state.inputValue.email, state.inputValue.password);

if (response?.id) {
router.push(ROUTES.dashboard);
Expand Down
6 changes: 3 additions & 3 deletions src/app/(domains)/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import OAuth from "@/components/OAuth";
import PasswordStrengthIndicator from "@/components/PasswordStrength";
import { useI18n } from '@/hooks/useI18n';
import { supabase } from "@/libs/supabase/client";
import SupabaseService from "@/services/supabase";
import AuthService from "@/services/auth";
import { isValidEmail } from "@/utils/isValidEmail";

const initialState = {
Expand Down Expand Up @@ -90,8 +90,8 @@ export default function SignUp() {
throw new Error("Terms not accepted");
}

const SupabaseServiceInstance = new SupabaseService(supabase);
const response = await SupabaseServiceInstance.signUp(state.inputValue.email, state.inputValue.password);
const AuthServiceInstance = new AuthService(supabase);
const response = await AuthServiceInstance.signUp(state.inputValue.email, state.inputValue.password);

if (response?.id) {
dispatch({ type: "SET_REGISTRATION_COMPLETE", payload: true });
Expand Down
6 changes: 3 additions & 3 deletions src/app/(domains)/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { headers } from "next/headers";

import SettingsOptions from "@/components/SettingsOptions";
import { createClient } from "@/libs/supabase/server";
import SupabaseService from "@/services/supabase";
import AuthService from "@/services/auth";
import { capitalize } from "@/utils/capitalize";
import { loadTranslationsSSR } from '@/utils/loadTranslationsSSR';

export default async function Settings() {
const { translate } = await loadTranslationsSSR();
const supabase = await createClient();
const SupabaseServiceInstance = new SupabaseService(supabase);
const data = await SupabaseServiceInstance.getUser();
const AuthServiceInstance = new AuthService(supabase);
const data = await AuthServiceInstance.getUser();
const sharedData = JSON.parse((await headers()).get('x-shared-data') || '{}');

return (
Expand Down
6 changes: 3 additions & 3 deletions src/app/(domains)/dashboard/subscription/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import { headers } from "next/headers";
import ManageBilling from "@/components/ManageBilling";
import PricingSection from "@/components/Pricing";
import { createClient } from "@/libs/supabase/server";
import SupabaseService from "@/services/supabase";
import AuthService from "@/services/auth";
import { capitalize } from "@/utils/capitalize";
import { loadTranslationsSSR } from '@/utils/loadTranslationsSSR';

export default async function Subscription() {
const { translate } = await loadTranslationsSSR();
const sharedData = JSON.parse((await headers()).get('x-shared-data') || '{}');
const supabase = await createClient();
const SupabaseServiceInstance = new SupabaseService(supabase);
const session = await SupabaseServiceInstance.getSession();
const AuthServiceInstance = new AuthService(supabase);
const session = await AuthServiceInstance.getSession();

const currentPlanText = translate("subscription-current-plan-description");
const currentPlan = capitalize(sharedData?.plan);
Expand Down
6 changes: 3 additions & 3 deletions src/components/MyAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { useState, useEffect, useRef } from 'react';

import { useI18n } from "@/hooks/useI18n";
import { supabase } from '@/libs/supabase/client';
import SupabaseService from '@/services/supabase';
import AuthService from '@/services/auth';

const SupabaseServiceInstance = new SupabaseService(supabase);
const AuthServiceInstance = new AuthService(supabase);

function MyAccount() {
const { translate } = useI18n();
Expand Down Expand Up @@ -58,7 +58,7 @@ function MyAccount() {
<a
className="block px-4 py-2 text-gray-700 hover:bg-gray-100 cursor-pointer flex items-center justify-between"
onClick={async () => {
await SupabaseServiceInstance.signOut();
await AuthServiceInstance.signOut();
window.location.reload();
}}
>
Expand Down
6 changes: 3 additions & 3 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useEffect, useState } from "react";

import { useI18n } from "@/hooks/useI18n";
import { supabase } from '@/libs/supabase/client';
import SupabaseService from '@/services/supabase';
import AuthService from '@/services/auth';

import LanguageSelector from "./LanguageSelector";
import Spinner from "./Spinner";
Expand All @@ -20,8 +20,8 @@ export default function Navbar() {

useEffect(() => {
const getUserSession = async () => {
const SupabaseServiceInstance = new SupabaseService(supabase);
const user = await SupabaseServiceInstance.getUserId();
const AuthServiceInstance = new AuthService(supabase);
const user = await AuthServiceInstance.getUserId();
if (!!user) {
setIsLogged(true);
} else {
Expand Down
10 changes: 5 additions & 5 deletions src/components/OAuth.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { PROVIDERS_IMAGE_URL } from "@/constants/PROVIDERS_IMAGE_URL";
import { supabase } from "@/libs/supabase/client";
import SupabaseService from "@/services/supabase";
import AuthService from "@/services/auth";

export default function OAuth() {
const SupabaseServiceInstance = new SupabaseService(supabase);
const AuthServiceInstance = new AuthService(supabase);

const PROVIDERS_MAP = [
{
provider: 'Google',
logo: PROVIDERS_IMAGE_URL.Google,
onClick: () => SupabaseServiceInstance.signInProvider('google')
onClick: () => AuthServiceInstance.signInWithProvider('google')
},
{
provider: 'Facebook',
logo: PROVIDERS_IMAGE_URL.Facebook,
onClick: () => SupabaseServiceInstance.signInProvider('facebook')
onClick: () => AuthServiceInstance.signInWithProvider('facebook')
},
{
provider: 'Twitter',
logo: PROVIDERS_IMAGE_URL.Twitter,
onClick: () => SupabaseServiceInstance.signInProvider('twitter')
onClick: () => AuthServiceInstance.signInWithProvider('twitter')
}
]
return (
Expand Down Expand Up @@ -51,7 +51,7 @@
className="p-2 border border-gray-300 rounded-md bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={onClick}
>
<img

Check warning on line 54 in src/components/OAuth.tsx

View workflow job for this annotation

GitHub Actions / lint

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element

Check warning on line 54 in src/components/OAuth.tsx

View workflow job for this annotation

GitHub Actions / lint

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={logo}
alt={provider}
className="w-5 h-5"
Expand Down
6 changes: 3 additions & 3 deletions src/components/SettingsOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ButtonComponent from "@/components/Button";
import { useI18n } from "@/hooks/useI18n";
import { useToast } from "@/hooks/useToast";
import { supabase } from "@/libs/supabase/client";
import SupabaseService from "@/services/supabase";
import AuthService from "@/services/auth";

type SettingsOptionsProps = {
userEmail?: string;
Expand All @@ -15,15 +15,15 @@ type SettingsOptionsProps = {

function SettingsOptions({ userEmail, currentPlan }: SettingsOptionsProps) {
const { translate } = useI18n();
const SupabaseServiceInstance = new SupabaseService(supabase);
const AuthServiceInstance = new AuthService(supabase);
const { addToast } = useToast();
const [isLoading, setIsLoading] = useState({
forgotPassword: false,
});

const handleForgotPassword = async (userEmail: string) => {
setIsLoading((data) => ({ ...data, forgotPassword: true }));
const response = await SupabaseServiceInstance.forgotPassword(userEmail);
const response = await AuthServiceInstance.forgotPassword(userEmail);

if (response) {
await addToast({
Expand Down
10 changes: 5 additions & 5 deletions src/hooks/useCheckout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { FIXED_CURRENCY } from "@/constants/FIXED_CURRENCY";
import { HAS_FREE_TRIAL } from "@/constants/HAS_FREE_TRIAL";
import { useToast } from "@/hooks/useToast";
import { supabase } from '@/libs/supabase/client';
import StripeService from '@/services/stripe';
import SupabaseService from '@/services/supabase';
import AuthService from '@/services/auth';
import PaymentService from '@/services/payment';

export const useCheckout = () => {
const { addToast } = useToast();
Expand All @@ -15,8 +15,8 @@ export const useCheckout = () => {
}

setIsLoading(true);
const SupabaseServiceInstance = new SupabaseService(supabase);
const user = await SupabaseServiceInstance.getUserId();
const AuthServiceInstance = new AuthService(supabase);
const user = await AuthServiceInstance.getUserId();

if (!user) {
window.location.href = '/signin';
Expand All @@ -35,7 +35,7 @@ export const useCheckout = () => {
const sessionId = jsonResponse.id;

if (sessionId) {
await StripeService.redirectToCheckout(sessionId);
await PaymentService.redirectToCheckout(sessionId);
} else {
addToast({
id: Date.now().toString(),
Expand Down
24 changes: 19 additions & 5 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,39 @@ import type { NextRequest } from 'next/server';

import { updateSession } from '@/libs/supabase/middleware';
import { createClient } from '@/libs/supabase/server';
import { fetchSubscription } from '@/services/api/subscription';
import SupabaseService from '@/services/supabase';
import AuthService from '@/services/auth';

export async function middleware(request: NextRequest) {
await updateSession(request);
const url = request.nextUrl.clone();

if (url.pathname.startsWith('/dashboard')) {
const supabase = await createClient();
const SupabaseServiceInstance = new SupabaseService(supabase);
const AuthServiceInstance = new AuthService(supabase);

const userId = await SupabaseServiceInstance.getUserId();
const userId = await AuthServiceInstance.getUserId();

if (!userId) {
const redirectUrl = new URL('/signin', request.url);
return NextResponse.redirect(redirectUrl);
}

const subscription = await fetchSubscription(userId);

const subscriptionRequest = await fetch(
`${process.env.NEXT_PUBLIC_PROJECT_URL}/api/payments/get-subscription?userId=${userId}`,
{
method: 'GET',
cache: 'no-store',
}
);

if (!subscriptionRequest.ok) {
console.error('Failed to fetch subscription:', subscriptionRequest.statusText);
return null;
}

const data = await subscriptionRequest.json();
const subscription = data.subscription;

const plan = subscription &&
subscription?.status === 'active'
Expand Down
Loading
Loading