diff --git a/apps/next-app/package.json b/apps/next-app/package.json
index a34886c..d19dad7 100644
--- a/apps/next-app/package.json
+++ b/apps/next-app/package.json
@@ -26,7 +26,7 @@
"@types/react": "18.0.37",
"@types/react-dom": "18.0.11",
"autoprefixer": "10.4.14",
- "axios": "^1.3.6",
+ "date-fns": "^2.29.3",
"eslint": "8.38.0",
"eslint-config-next": "13.3.0",
"graphql": "^16.6.0",
diff --git a/apps/next-app/src/constants/ROUTES.ts b/apps/next-app/src/constants/ROUTES.ts
index d0ecf49..be36b61 100644
--- a/apps/next-app/src/constants/ROUTES.ts
+++ b/apps/next-app/src/constants/ROUTES.ts
@@ -7,4 +7,5 @@ export enum ROUTES {
SUBSCRIPTION = '/subscription',
SUBSCRIPTION_SUCCESS = '/subscription/success',
SUBSCRIPTION_CANCEL = '/subscription/cancel',
+ ANALYTICS = '/analytics',
}
diff --git a/apps/next-app/src/middleware.ts b/apps/next-app/src/middleware.ts
index 2cdff00..f7027c7 100644
--- a/apps/next-app/src/middleware.ts
+++ b/apps/next-app/src/middleware.ts
@@ -1,5 +1,8 @@
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs';
+import { ROUTES } from 'constants/ROUTES';
import { NextRequest, NextResponse } from 'next/server';
+import { GET_PROFILE } from 'shared/queries/index.graphql';
+import { getApolloServerClient } from 'shared/services/apollo';
export async function middleware(req: NextRequest) {
const res = NextResponse.next();
@@ -10,7 +13,19 @@ export async function middleware(req: NextRequest) {
} = await supabase.auth.getSession();
if (session) {
- // Authentication successful, forward request to protected route.
+ if (req.nextUrl.pathname.startsWith(ROUTES.ANALYTICS)) {
+ const client = getApolloServerClient(session.access_token);
+
+ const { data } = await client.query({
+ query: GET_PROFILE,
+ variables: { profileId: session.user.id },
+ });
+ if (!data.profilesCollection?.edges[0].node.subscription) {
+ const redirectUrl = req.nextUrl.clone();
+ redirectUrl.pathname = ROUTES.SUBSCRIPTION;
+ return NextResponse.redirect(redirectUrl);
+ }
+ }
return res;
}
@@ -21,5 +36,5 @@ export async function middleware(req: NextRequest) {
}
export const config = {
- matcher: ['/dashboard', '/profile', '/subscription'],
+ matcher: ['/dashboard', '/profile', '/subscription', '/analytics'],
};
diff --git a/apps/next-app/src/pages/_app.tsx b/apps/next-app/src/pages/_app.tsx
index 9ccffa3..f422921 100644
--- a/apps/next-app/src/pages/_app.tsx
+++ b/apps/next-app/src/pages/_app.tsx
@@ -3,17 +3,22 @@ import { Nunito } from 'next/font/google';
import type { AppProps } from 'next/app';
import { AppProviders } from 'providers/AppProviders';
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react';
-import { useState } from 'react';
+import { ReactElement, ReactNode, useState } from 'react';
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs';
+import { NextPage } from 'next';
+import { Layout } from 'shared/components/Layout';
const nunito = Nunito({ subsets: ['latin'], weight: ['300', '500', '700'] });
-export default function App({
- Component,
- pageProps,
-}: AppProps<{
+export type NextPageWithLayout
= NextPage
& {
+ getLayout?: (page: ReactElement) => ReactNode;
+};
+
+type CustomAppProps = AppProps & {
initialSession: Session;
-}>) {
+};
+
+export default function App({ Component, pageProps }: CustomAppProps) {
const [supabaseClient] = useState(() => createBrowserSupabaseClient());
return (
@@ -23,7 +28,9 @@ export default function App({
>
-
+
+ )
+
diff --git a/apps/next-app/src/pages/analytics/index.tsx b/apps/next-app/src/pages/analytics/index.tsx
new file mode 100644
index 0000000..9988c32
--- /dev/null
+++ b/apps/next-app/src/pages/analytics/index.tsx
@@ -0,0 +1,9 @@
+const Analytics = () => {
+ return (
+
+
Hello Analytics!
+
+ );
+};
+
+export default Analytics;
diff --git a/apps/next-app/src/pages/profile/index.tsx b/apps/next-app/src/pages/profile/index.tsx
index 8faa536..944b8c7 100644
--- a/apps/next-app/src/pages/profile/index.tsx
+++ b/apps/next-app/src/pages/profile/index.tsx
@@ -5,12 +5,15 @@ import { GetServerSidePropsContext } from 'next';
import { AvatarChanger } from 'shared/components/profile/AvatarChanger';
import { EditProfileForm } from 'shared/components/profile/EditProfileForm';
import { getApolloServerClient } from 'shared/services/apollo';
+import { EDGE_FUNCTION_NAMES } from 'constants/EDGE_FUNCTION_NAMES';
+import { TransactionHistory } from 'shared/components/subscriptions/TransactionHistory';
interface ProfileProps {
profile: GetProfileQuery['profilesCollection'];
+ charges: any;
}
-const Profile = ({ profile }: ProfileProps) => {
+const Profile = ({ profile, charges }: ProfileProps) => {
const userProfile = profile?.edges[0];
return (
@@ -19,6 +22,7 @@ const Profile = ({ profile }: ProfileProps) => {
+
);
};
@@ -34,5 +38,9 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
query: GET_PROFILE,
variables: { profileId: session.data.session?.user.id },
});
- return { props: { profile: data.profilesCollection } };
+
+ const { data: chargesData } = await supabase.functions.invoke(
+ EDGE_FUNCTION_NAMES.GET_STRIPE_CHARGES
+ );
+ return { props: { profile: data.profilesCollection, charges: chargesData } };
};
diff --git a/apps/next-app/src/shared/components/Crossmark/crossmark.component.tsx b/apps/next-app/src/shared/components/Crossmark/crossmark.component.tsx
index de07503..72699a3 100644
--- a/apps/next-app/src/shared/components/Crossmark/crossmark.component.tsx
+++ b/apps/next-app/src/shared/components/Crossmark/crossmark.component.tsx
@@ -1,7 +1,15 @@
import CrossmarkSVG from 'assets/cross-mark.svg';
-export const Crossmark = () => {
+interface CrossmarkProps {
+ isError?: boolean;
+}
+
+export const Crossmark = ({ isError }: CrossmarkProps) => {
return (
-
+
);
};
diff --git a/apps/next-app/src/shared/components/Layout/index.ts b/apps/next-app/src/shared/components/Layout/index.ts
new file mode 100644
index 0000000..53176c3
--- /dev/null
+++ b/apps/next-app/src/shared/components/Layout/index.ts
@@ -0,0 +1 @@
+export { Layout } from './layout.component';
diff --git a/apps/next-app/src/shared/components/Layout/layout.component.tsx b/apps/next-app/src/shared/components/Layout/layout.component.tsx
new file mode 100644
index 0000000..afca3e8
--- /dev/null
+++ b/apps/next-app/src/shared/components/Layout/layout.component.tsx
@@ -0,0 +1,29 @@
+import { ROUTES } from 'constants/ROUTES';
+import Link from 'next/link';
+import { ReactNode } from 'react';
+
+interface LayoutProps {
+ children: ReactNode;
+}
+
+export const Layout = ({ children }: LayoutProps) => {
+ return (
+
+
+
+ Profile
+
+
+ Dashboard
+
+
+ Subscription
+
+
+ Analytics
+
+
+ {children}
+
+ );
+};
diff --git a/apps/next-app/src/shared/components/subscriptions/TransactionHistory/index.ts b/apps/next-app/src/shared/components/subscriptions/TransactionHistory/index.ts
new file mode 100644
index 0000000..a0b66fb
--- /dev/null
+++ b/apps/next-app/src/shared/components/subscriptions/TransactionHistory/index.ts
@@ -0,0 +1 @@
+export { TransactionHistory } from './transactionHistory.component';
diff --git a/apps/next-app/src/shared/components/subscriptions/TransactionHistory/transactionHistory.component.tsx b/apps/next-app/src/shared/components/subscriptions/TransactionHistory/transactionHistory.component.tsx
new file mode 100644
index 0000000..1cf9400
--- /dev/null
+++ b/apps/next-app/src/shared/components/subscriptions/TransactionHistory/transactionHistory.component.tsx
@@ -0,0 +1,39 @@
+import { format, fromUnixTime } from 'date-fns';
+import { Checkmark } from 'shared/components/Checkmark';
+import { Crossmark } from 'shared/components/Crossmark';
+import { getSubscriptionPrice } from 'utils/getSubscriptionPrice';
+
+interface TransactionHistoryProps {
+ transactions: any;
+}
+
+export const TransactionHistory = ({
+ transactions,
+}: TransactionHistoryProps) => {
+ return (
+
+
Transaction history
+
+ {transactions.charges.data.map(
+ ({ id, created, description, amount, currency, status }: any) => (
+
+
+ {description}{' '}
+
+ ({format(fromUnixTime(created), 'Pp')})
+
+
+
+ {getSubscriptionPrice(amount)} {currency.toUpperCase()}
+
+ {status === 'succeeded' ?
:
}
+
+ )
+ )}
+
+
+ );
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index adb1782..17dd3f0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -42,9 +42,9 @@ importers:
autoprefixer:
specifier: 10.4.14
version: 10.4.14(postcss@8.4.22)
- axios:
- specifier: ^1.3.6
- version: 1.3.6
+ date-fns:
+ specifier: ^2.29.3
+ version: 2.29.3
eslint:
specifier: 8.38.0
version: 8.38.0
@@ -3329,6 +3329,7 @@ packages:
/asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+ dev: true
/auto-bind@4.0.0:
resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==}
@@ -3360,16 +3361,6 @@ packages:
engines: {node: '>=4'}
dev: false
- /axios@1.3.6:
- resolution: {integrity: sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==}
- dependencies:
- follow-redirects: 1.15.2
- form-data: 4.0.0
- proxy-from-env: 1.1.0
- transitivePeerDependencies:
- - debug
- dev: false
-
/axobject-query@3.1.1:
resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==}
dependencies:
@@ -3857,6 +3848,7 @@ packages:
engines: {node: '>= 0.8'}
dependencies:
delayed-stream: 1.0.0
+ dev: true
/commander@10.0.1:
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
@@ -4043,6 +4035,11 @@ packages:
resolution: {integrity: sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==}
dev: true
+ /date-fns@2.29.3:
+ resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
+ engines: {node: '>=0.11'}
+ dev: false
+
/debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
dev: true
@@ -4143,6 +4140,7 @@ packages:
/delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
+ dev: true
/dependency-graph@0.11.0:
resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==}
@@ -4880,16 +4878,6 @@ packages:
/flatted@3.2.7:
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
- /follow-redirects@1.15.2:
- resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
- engines: {node: '>=4.0'}
- peerDependencies:
- debug: '*'
- peerDependenciesMeta:
- debug:
- optional: true
- dev: false
-
/for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
dependencies:
@@ -4902,6 +4890,7 @@ packages:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
+ dev: true
/formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
@@ -6442,12 +6431,14 @@ packages:
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
+ dev: true
/mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
+ dev: true
/mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
@@ -7121,10 +7112,6 @@ packages:
react-is: 16.13.1
dev: false
- /proxy-from-env@1.1.0:
- resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
- dev: false
-
/psl@1.9.0:
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
dev: true