Compare commits

..

40 Commits

Author SHA1 Message Date
Mythie ede9eb052d fix: named exports 2023-10-13 23:56:11 +11:00
Mythie 4d5275f915 fix: create custom pricing table 2023-10-13 23:33:40 +11:00
Lucas Smith 01e6367b72 Merge branch 'feat/refresh' into feat/stripe-free-tier 2023-10-11 17:24:01 +11:00
Lucas Smith 565602f8e1 Merge pull request #530 from anikdhabal/issue#518
fix: Add gitpod configuration
2023-10-11 16:56:59 +11:00
Nafees Nazik e0271cace3 feat: delete draft document (#491) 2023-10-10 13:55:58 +11:00
Anik Dhabal Babu cc8c4b8297 Merge branch 'feat/refresh' into issue#518 2023-10-09 15:21:33 +05:30
Mythie a287aab4f4 chore: disable dependabot for now 2023-10-09 20:35:19 +11:00
Lucas Smith b5ed703553 Merge pull request #545 from anikdhabal/fix_dotenv-cli
fix: mismatch the version of dotenv-cli
2023-10-09 19:49:07 +11:00
Anik Dhabal Babu f49880125a fix: mismatch the version of dotenv-cli 2023-10-09 08:41:13 +00:00
Anik Dhabal Babu 8380c357d9 Merge branch 'feat/refresh' into issue#518 2023-10-09 10:03:58 +05:30
Anik Dhabal Babu 4e010c5624 fix : add gittpod configuration 2023-10-09 09:58:12 +05:30
hallidayo f53cdbace9 fix: frequency focus ring (#533) 2023-10-09 12:04:01 +11:00
Lucas Smith b4d04e2ce9 Merge pull request #516 from adityadeshlahre/feat/refresh
fix(script): [fix : DOC-36] Use dotenv for Prisma package scripts #523
2023-10-09 10:12:08 +11:00
Mythie 2470aeee1f fix: update script, docs and devcontainer 2023-10-08 21:51:15 +11:00
Lucas Smith fd07b47325 Merge pull request #526 from mittalsam98/fix/507-signature-modal-center-align
fix: non responsiveness of Add your sign modal
2023-10-07 22:31:18 +11:00
Lucas Smith 9257a05831 Merge pull request #527 from documenso/docs/render-deploy
docs: add render one click deploy for refresh
2023-10-07 22:25:42 +11:00
Ephraim Atta-Duncan 1faa6f2944 Merge pull request #528 from documenso/chore/github-templates 2023-10-07 02:35:35 +00:00
Ephraim Atta-Duncan 5584bbe9ca Merge branch 'feat/refresh' into chore/github-templates 2023-10-07 02:07:54 +00:00
Anik Dhabal Babu cc65537ea3 fix: Add gitpod configuration 2023-10-06 23:03:13 +05:30
Anik Dhabal Babu 04a80b7c03 fix: add gitpod configuration 2023-10-06 11:06:34 +05:30
Anik Dhabal Babu c71a89d1b7 fix: Add gitpod configuration 2023-10-05 12:21:34 +00:00
Aditya Deshlahre e2abfd2312 Merge branch 'documenso:feat/refresh' into feat/refresh 2023-10-05 15:46:14 +05:30
Anjy Gupta 49d55227e8 fix: sign up with existing account email bug (#517)
* fix: sign up with existing account email bug
2023-10-05 20:59:43 +11:00
Aditya @ArchLinux 0dadec3b8d fix(script): minor change on scipt 2023-10-05 15:26:53 +05:30
Aditya Deshlahre e2d8591d66 Merge branch 'documenso:feat/refresh' into feat/refresh 2023-10-05 15:19:42 +05:30
zahid47 eac7aa84b0 fix: add defaultValue to SignaturePad to persist signatures (#522)
* fix: add defaultValue to SignaturePad to persist signatures
2023-10-05 12:54:52 +11:00
hallidayo bd941202c8 changed text of stepper (#513) 2023-10-05 11:25:09 +11:00
Ephraim Atta-Duncan b854f0eedc chore: add pull request templates 2023-10-04 20:05:55 +00:00
Ephraim Atta-Duncan 1814bd4167 chore: add issue template 2023-10-04 20:04:10 +00:00
Sachin 7c54913bf5 fix: non responsiveness of Add your sign modal 2023-10-05 00:09:30 +05:30
Aditya @ArchLinux ddf097ede3 Merge branch 'adityadeshlahre/documenso' into feat/refresh 2023-10-04 20:05:33 +05:30
Aditya Deshlahre 1bad85e1d6 Merge branch 'documenso:feat/refresh' into feat/refresh 2023-10-04 20:04:48 +05:30
Aditya @ArchLinux 68458b50d2 fix(script): added script envprisma in root package.json
dotenv loads all environment variable before running prisma:migrate-dev script
2023-10-04 20:04:07 +05:30
Aditya Deshlahre 4cc34ec50a Merge branch 'documenso:feat/refresh' into feat/refresh 2023-10-04 13:00:52 +05:30
Aditya @ArchLinux f637381198 style(ui/ux): added margin to dialogprimitive.content & dialogprimitive.close (m-4) 2023-10-04 01:39:16 +05:30
David Nguyen 4d485940ea fix: stripe customer fetch logic 2023-09-19 15:30:58 +10:00
David Nguyen cbe118b74f fix: merge issues 2023-09-19 15:14:47 +10:00
David Nguyen de9116e9b2 Merge branch 'feat/refresh' into feat/stripe-free-tier 2023-09-19 15:12:40 +10:00
David Nguyen 027a588604 feat: wip 2023-09-18 22:47:46 +10:00
David Nguyen 773566f193 feat: add free tier Stripe subscription 2023-09-18 22:33:07 +10:00
41 changed files with 1007 additions and 88 deletions
+1 -6
View File
@@ -9,10 +9,5 @@ npm install
# Copy the env file
cp .env.example .env
# Source the env file, export the variables
set -a
source .env
set +a
# Run the migrations
npm run -w @documenso/prisma prisma:migrate-dev
npm run prisma:migrate-dev
+1
View File
@@ -68,6 +68,7 @@ NEXT_PRIVATE_STRIPE_API_KEY=
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID=
# [[FEATURES]]
# OPTIONAL: Leave blank to disable PostHog and feature flags.
+50
View File
@@ -0,0 +1,50 @@
---
name: Bug Report
about: Create a bug report to help us improve
---
<!--- Please provide a general summary of the issue in the Title above -->
## Issue Description
<!--- Please provide a clear and concise description of the problem. -->
## Steps to Reproduce
<!--- Please provide step-by-step instructions to reproduce the issue. -->
<!--- Include code snippets, error messages, and any other relevant information. -->
1. Step 1
2. Step 2
3. ...
## Expected Behavior
<!--- Describe what you expected to happen. -->
## Current Behavior
<!--- Describe what is currently happening. -->
## Screenshots (optional)
<!--- If applicable, add screenshots to help explain the issue. -->
## Environment
<!--- Please provide information about your environment, such as operating system, browser, version, etc. -->
- OS: [e.g., Windows 10]
- Browser: [e.g., Chrome, Firefox]
- Version: [e.g., 2.0.1]
## Checklist
<!--- Please check the boxes that apply to this issue report. -->
<!--- You can add or remove items as needed. -->
- [ ] I have searched the existing issues to make sure this is not a duplicate.
- [ ] I have provided steps to reproduce the issue.
- [ ] I have included relevant environment information.
- [ ] I have included any relevant screenshots.
- [ ] I understand that this is a voluntary contribution and that there is no guarantee of resolution.
+41
View File
@@ -0,0 +1,41 @@
---
name: Feature Request
about: Suggest a new idea or enhancement for this project
---
<!--- Please provide a clear and concise title for your feature request -->
## Feature Description
<!--- Describe the feature you are requesting in detail. -->
<!--- Explain what problem it solves or what value it adds to the project. -->
## Use Case
<!--- Provide a scenario or use case where this feature would be beneficial. -->
<!--- Explain how users would interact with this feature and why it's important. -->
## Proposed Solution
<!--- If you have an idea of how this feature could be implemented, describe it here. -->
<!--- Include any technical details, UI/UX considerations, or design suggestions. -->
## Alternatives (optional)
<!--- Are there any alternative ways to achieve the same goal? -->
<!--- Describe other approaches that could be considered if this feature is not implemented. -->
## Additional Context
<!--- Add any additional context or information that might be relevant to the feature request. -->
## Checklist
<!--- Please check the boxes that apply to this feature request. -->
<!--- You can add or remove items as needed. -->
- [ ] I have searched the existing feature requests to make sure this is not a duplicate.
- [ ] I have provided a detailed description of the requested feature.
- [ ] I have explained the use case or scenario for this feature.
- [ ] I have included any relevant technical details or design suggestions.
- [ ] I understand that this is a suggestion and that there is no guarantee of implementation.
+41
View File
@@ -0,0 +1,41 @@
---
name: General Improvement
about: Suggest a minor enhancement or improvement for this project
---
<!--- Please provide a clear and concise title for your improvement suggestion -->
## Improvement Description
<!--- Describe the improvement you are suggesting in detail. -->
<!--- Explain what specific aspect of the project it addresses or enhances. -->
## Rationale
<!--- Explain why this improvement would be beneficial. -->
<!--- Share any context, pain points, or reasons for suggesting this change. -->
## Proposed Solution
<!--- If you have a suggestion for how this improvement could be implemented, describe it here. -->
<!--- Include any technical details, design suggestions, or other relevant information. -->
## Alternatives (optional)
<!--- Are there any alternative approaches to achieve the same improvement? -->
<!--- Describe other ways to address the issue or enhance the project. -->
## Additional Context
<!--- Add any additional context or information that might be relevant to the improvement suggestion. -->
## Checklist
<!--- Please check the boxes that apply to this improvement suggestion. -->
<!--- You can add or remove items as needed. -->
- [ ] I have searched the existing issues and improvement suggestions to avoid duplication.
- [ ] I have provided a clear description of the improvement being suggested.
- [ ] I have explained the rationale behind this improvement.
- [ ] I have included any relevant technical details or design suggestions.
- [ ] I understand that this is a suggestion and that there is no guarantee of implementation.
+49
View File
@@ -0,0 +1,49 @@
---
name: Pull Request
about: Submit changes to the project for review and inclusion
---
## Description
<!--- Describe the changes introduced by this pull request. -->
<!--- Explain what problem it solves or what feature/fix it adds. -->
## Related Issue
<!--- If this pull request is related to a specific issue, reference it here using #issue_number. -->
<!--- For example, "Fixes #123" or "Addresses #456". -->
## Changes Made
<!--- Provide a summary of the changes made in this pull request. -->
<!--- Include any relevant technical details or architecture changes. -->
- Change 1
- Change 2
- ...
## Testing Performed
<!--- Describe the testing that you have performed to validate these changes. -->
<!--- Include information about test cases, testing environments, and results. -->
- Tested feature X in scenario Y.
- Ran unit tests for component Z.
- Tested on browsers A, B, and C.
- ...
## Checklist
<!--- Please check the boxes that apply to this pull request. -->
<!--- You can add or remove items as needed. -->
- [ ] I have tested these changes locally and they work as expected.
- [ ] I have added/updated tests that prove the effectiveness of these changes.
- [ ] I have updated the documentation to reflect these changes, if applicable.
- [ ] I have followed the project's coding style guidelines.
- [ ] I have addressed the code review feedback from the previous submission, if applicable.
## Additional Notes
<!--- Provide any additional context or notes for the reviewers. -->
<!--- This might include details about design decisions, potential concerns, or anything else relevant. -->
@@ -0,0 +1,40 @@
---
name: Test Addition
about: Submit a new test, either unit or end-to-end (E2E), for review and inclusion
---
## Description
<!--- Provide a clear and concise description of the new test you are adding. -->
<!--- Explain the purpose of the test and what it aims to validate. -->
## Related Issue
<!--- If this test addition is related to a specific issue, reference it here using #issue_number. -->
<!--- For example, "Fixes #123" or "Addresses #456". -->
## Test Details
<!--- Describe the details of the test you're adding. -->
<!--- Include information about inputs, expected outputs, and any specific scenarios. -->
- Test Name: Name of the test
- Type: [Unit / E2E]
- Description: Brief description of what the test checks
- Inputs: What inputs the test uses (if applicable)
- Expected Output: What output or behavior the test expects
## Checklist
<!--- Please check the boxes that apply to this pull request. -->
<!--- You can add or remove items as needed. -->
- [ ] I have written the new test and ensured it works as intended.
- [ ] I have added necessary documentation to explain the purpose of the test.
- [ ] I have followed the project's testing guidelines and coding style.
- [ ] I have addressed any review feedback from previous submissions, if applicable.
## Additional Notes
<!--- Provide any additional context or notes for the reviewers. -->
<!--- This might include explanations about the testing approach or any potential concerns. -->
+3 -3
View File
@@ -9,7 +9,7 @@ updates:
labels:
- "ci dependencies"
- "ci"
open-pull-requests-limit: 2
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/apps/marketing"
@@ -19,7 +19,7 @@ updates:
labels:
- "npm dependencies"
- "frontend"
open-pull-requests-limit: 2
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/apps/web"
@@ -29,4 +29,4 @@ updates:
labels:
- "npm dependencies"
- "frontend"
open-pull-requests-limit: 2
open-pull-requests-limit: 0
+55
View File
@@ -0,0 +1,55 @@
tasks:
- init: |
npm i &&
npm run dx:up &&
cp .env.example .env &&
set -a; source .env &&
export NEXTAUTH_URL="$(gp url 3000)" &&
export NEXT_PUBLIC_WEBAPP_URL="$(gp url 3000)" &&
export NEXT_PUBLIC_MARKETING_URL="$(gp url 3001)"
command: npm run d
ports:
- port: 3000
visibility: public
onOpen: open-preview
- port: 3001
visibility: public
onOpen: open-preview
- port: 9000
visibility: public
onOpen: ignore
- port: 1100
visibility: private
onOpen: ignore
- port: 2500
visibility: private
onOpen: ignore
- port: 54320
visibility: private
onOpen: ignore
github:
prebuilds:
master: true
pullRequests: true
pullRequestsFromForks: true
addCheck: true
addComment: true
addBadge: true
vscode:
extensions:
- aaron-bond.better-comments
- bradlc.vscode-tailwindcss
- dbaeumer.vscode-eslint
- esbenp.prettier-vscode
- mikestead.dotenv
- unifiedjs.vscode-mdx
- GitHub.copilot-chat
- GitHub.copilot-labs
- GitHub.copilot
- GitHub.vscode-pull-request-github
- Prisma.prisma
- VisualStudioExptTeam.vscodeintellicode
+17 -1
View File
@@ -179,7 +179,7 @@ git clone https://github.com/documenso/documenso
- NEXT_PRIVATE_SMTP_FROM_NAME
- NEXT_PRIVATE_SMTP_FROM_ADDRESS
5. Create the database schema by running `npm run prisma:migrate-dev -w @documenso/prisma`
5. Create the database schema by running `npm run prisma:migrate-dev`
6. Run `npm run dev` root directory to start
@@ -254,6 +254,22 @@ containers:
- '::'
```
### I can't see environment variables in my package scripts
Wrap your package script with the `with:env` script like such:
```
npm run with:env -- npm run myscript
```
The same can be done when using `npx` for one of bin scripts:
```
npm run with:env -- npx myscript
```
This will load environment variables from your `.env` and `.env.local` files.
## Repo Activity
![Repository Activity](https://repobeats.axiom.co/api/embed/622a2e9aa709696f7226304b5b7178a5741b3868.svg)
+1
View File
@@ -7,6 +7,7 @@ declare namespace NodeJS {
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
NEXT_PRIVATE_STRIPE_API_KEY: string;
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
@@ -377,7 +377,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
</Card>
<Dialog open={showSigningDialog} onOpenChange={setShowSigningDialog}>
<DialogContent>
<DialogContent position="center">
<DialogHeader>
<DialogTitle>Add your signature</DialogTitle>
</DialogHeader>
@@ -391,6 +391,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<SignaturePad
className="aspect-video w-full rounded-md border"
defaultValue={signatureDataUrl || ''}
onChange={setDraftSignatureDataUrl}
/>
+1
View File
@@ -7,6 +7,7 @@ declare namespace NodeJS {
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
NEXT_PRIVATE_STRIPE_API_KEY: string;
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
@@ -1,5 +1,7 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import {
@@ -32,6 +34,8 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
import { DeleteDraftDocumentDialog } from './delete-draft-document-dialog';
export type DataTableActionDropdownProps = {
row: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
@@ -44,6 +48,8 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
const { toast } = useToast();
const [, copyToClipboard] = useCopyToClipboard();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
if (!session) {
return null;
}
@@ -59,6 +65,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
// const isPending = row.status === DocumentStatus.PENDING;
const isComplete = row.status === DocumentStatus.COMPLETED;
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const isDocumentDeletable = isOwner && row.status === DocumentStatus.DRAFT;
const onShareClick = async () => {
const { slug } = await createOrGetShareLink({
@@ -147,7 +154,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
Void
</DropdownMenuItem>
<DropdownMenuItem disabled>
<DropdownMenuItem onClick={() => setDeleteDialogOpen(true)} disabled={!isDocumentDeletable}>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
@@ -168,6 +175,14 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
Share
</DropdownMenuItem>
</DropdownMenuContent>
{isDocumentDeletable && (
<DeleteDraftDocumentDialog
id={row.id}
open={isDeleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
/>
)}
</DropdownMenu>
);
};
@@ -0,0 +1,89 @@
import { useRouter } from 'next/navigation';
import { trpc as trpcReact } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
type DeleteDraftDocumentDialogProps = {
id: number;
open: boolean;
onOpenChange: (_open: boolean) => void;
};
export const DeleteDraftDocumentDialog = ({
id,
open,
onOpenChange,
}: DeleteDraftDocumentDialogProps) => {
const router = useRouter();
const { toast } = useToast();
const { mutateAsync: deleteDocument, isLoading } =
trpcReact.document.deleteDraftDocument.useMutation({
onSuccess: () => {
router.refresh();
toast({
title: 'Document deleted',
description: 'Your document has been successfully deleted.',
duration: 5000,
});
onOpenChange(false);
},
});
const onDraftDelete = async () => {
try {
await deleteDocument({ id });
} catch {
toast({
title: 'Something went wrong',
description: 'This document could not be deleted at this time. Please try again.',
variant: 'destructive',
duration: 7500,
});
}
};
return (
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Do you want to delete this document?</DialogTitle>
<DialogDescription>
Please note that this action is irreversible. Once confirmed, your document will be
permanently deleted.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<div className="flex w-full flex-1 flex-nowrap gap-4">
<Button
type="button"
variant="secondary"
onClick={() => onOpenChange(false)}
className="flex-1"
>
Cancel
</Button>
<Button type="button" loading={isLoading} onClick={onDraftDelete} className="flex-1">
Delete
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -66,7 +66,7 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
<div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
<h1 className="text-4xl font-semibold">Documents</h1>
<div className="flex flex-wrap gap-x-4 gap-y-6 overflow-hidden">
<div className="-m-1 flex flex-wrap gap-x-4 gap-y-6 overflow-hidden p-1">
<Tabs defaultValue={status} className="overflow-x-auto">
<TabsList>
{[
@@ -0,0 +1,133 @@
'use client';
import { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { PriceIntervals } from '@documenso/ee/server-only/stripe/get-prices-by-interval';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { toHumanPrice } from '@documenso/lib/universal/stripe/to-human-price';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { createCheckout } from './create-checkout.action';
type Interval = keyof PriceIntervals;
const INTERVALS: Interval[] = ['day', 'week', 'month', 'year'];
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const isInterval = (value: unknown): value is Interval => INTERVALS.includes(value as Interval);
const FRIENDLY_INTERVALS: Record<Interval, string> = {
day: 'Daily',
week: 'Weekly',
month: 'Monthly',
year: 'Yearly',
};
const MotionCard = motion(Card);
export type BillingPlansProps = {
prices: PriceIntervals;
};
export const BillingPlans = ({ prices }: BillingPlansProps) => {
const { toast } = useToast();
const isMounted = useIsMounted();
const [interval, setInterval] = useState<Interval>('month');
const [isFetchingCheckoutSession, setIsFetchingCheckoutSession] = useState(false);
const onSubscribeClick = async (priceId: string) => {
try {
setIsFetchingCheckoutSession(true);
const url = await createCheckout({ priceId });
if (!url) {
throw new Error('Unable to create session');
}
window.open(url);
} catch (_err) {
toast({
title: 'Something went wrong',
description: 'An error occurred while trying to create a checkout session.',
variant: 'destructive',
});
} finally {
setIsFetchingCheckoutSession(false);
}
};
return (
<div>
<Tabs value={interval} onValueChange={(value) => isInterval(value) && setInterval(value)}>
<TabsList>
{INTERVALS.map(
(interval) =>
prices[interval].length > 0 && (
<TabsTrigger key={interval} className="min-w-[150px]" value={interval}>
{FRIENDLY_INTERVALS[interval]}
</TabsTrigger>
),
)}
</TabsList>
</Tabs>
<div className="mt-8 grid gap-8 lg:grid-cols-2 2xl:grid-cols-3">
<AnimatePresence mode="wait">
{prices[interval].map((price) => (
<MotionCard
key={price.id}
initial={{ opacity: isMounted ? 0 : 1, y: isMounted ? 20 : 0 }}
animate={{ opacity: 1, y: 0, transition: { duration: 0.3 } }}
exit={{ opacity: 0, transition: { duration: 0.3 } }}
>
<CardContent className="flex h-full flex-col p-6">
<CardTitle>{price.product.name}</CardTitle>
<div className="text-muted-foreground mt-2 text-lg font-medium">
${toHumanPrice(price.unit_amount ?? 0)} {price.currency.toUpperCase()}{' '}
<span className="text-xs">per {interval}</span>
</div>
<div className="text-muted-foreground mt-1.5 text-sm">
{price.product.description}
</div>
{price.product.features && price.product.features.length > 0 && (
<div className="text-muted-foreground mt-4">
<div className="text-sm font-medium">Includes:</div>
<ul className="mt-1 divide-y text-sm">
{price.product.features.map((feature, index) => (
<li key={index} className="py-2">
{feature.name}
</li>
))}
</ul>
</div>
)}
<div className="flex-1" />
<Button
className="mt-4"
loading={isFetchingCheckoutSession}
onClick={() => void onSubscribeClick(price.id)}
>
Subscribe
</Button>
</CardContent>
</MotionCard>
))}
</AnimatePresence>
</div>
</div>
);
};
@@ -0,0 +1,55 @@
'use client';
import { useState } from 'react';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { createBillingPortal } from './create-billing-portal.action';
export const BillingPortalButton = () => {
const { toast } = useToast();
const [isFetchingPortalUrl, setIsFetchingPortalUrl] = useState(false);
const handleFetchPortalUrl = async () => {
if (isFetchingPortalUrl) {
return;
}
setIsFetchingPortalUrl(true);
try {
const sessionUrl = await createBillingPortal();
if (!sessionUrl) {
throw new Error('NO_SESSION');
}
window.open(sessionUrl, '_blank');
} catch (e) {
let description =
'We are unable to proceed to the billing portal at this time. Please try again, or contact support.';
if (e.message === 'CUSTOMER_NOT_FOUND') {
description =
'You do not currently have a customer record, this should not happen. Please contact support for assistance.';
}
toast({
title: 'Something went wrong',
description,
variant: 'destructive',
duration: 10000,
});
}
setIsFetchingPortalUrl(false);
};
return (
<Button onClick={async () => handleFetchPortalUrl()} loading={isFetchingPortalUrl}>
Manage Subscription
</Button>
);
};
@@ -0,0 +1,48 @@
'use server';
import {
getStripeCustomerByEmail,
getStripeCustomerById,
} from '@documenso/ee/server-only/stripe/get-customer';
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
export const createBillingPortal = async () => {
const { user } = await getRequiredServerComponentSession();
const existingSubscription = await getSubscriptionByUserId({ userId: user.id });
let stripeCustomer: Stripe.Customer | null = null;
// Find the Stripe customer for the current user subscription.
if (existingSubscription) {
stripeCustomer = await getStripeCustomerById(existingSubscription.customerId);
if (!stripeCustomer) {
throw new Error('Missing Stripe customer for subscription');
}
}
// Fallback to check if a Stripe customer already exists for the current user email.
if (!stripeCustomer) {
stripeCustomer = await getStripeCustomerByEmail(user.email);
}
// Create a Stripe customer if it does not exist for the current user.
if (!stripeCustomer) {
stripeCustomer = await stripe.customers.create({
name: user.name ?? undefined,
email: user.email,
metadata: {
userId: user.id,
},
});
}
return getPortalSession({
customerId: stripeCustomer.id,
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
});
};
@@ -0,0 +1,59 @@
'use server';
import { getCheckoutSession } from '@documenso/ee/server-only/stripe/get-checkout-session';
import {
getStripeCustomerByEmail,
getStripeCustomerById,
} from '@documenso/ee/server-only/stripe/get-customer';
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
export type CreateCheckoutOptions = {
priceId: string;
};
export const createCheckout = async ({ priceId }: CreateCheckoutOptions) => {
const { user } = await getRequiredServerComponentSession();
const existingSubscription = await getSubscriptionByUserId({ userId: user.id });
let stripeCustomer: Stripe.Customer | null = null;
// Find the Stripe customer for the current user subscription.
if (existingSubscription) {
stripeCustomer = await getStripeCustomerById(existingSubscription.customerId);
if (!stripeCustomer) {
throw new Error('Missing Stripe customer for subscription');
}
return getPortalSession({
customerId: stripeCustomer.id,
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
});
}
// Fallback to check if a Stripe customer already exists for the current user email.
if (!stripeCustomer) {
stripeCustomer = await getStripeCustomerByEmail(user.email);
}
// Create a Stripe customer if it does not exist for the current user.
if (!stripeCustomer) {
stripeCustomer = await stripe.customers.create({
name: user.name ?? undefined,
email: user.email,
metadata: {
userId: user.id,
},
});
}
return getCheckoutSession({
customerId: stripeCustomer.id,
priceId,
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
});
};
@@ -1,16 +1,18 @@
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
import { match } from 'ts-pattern';
import { getPricesByInterval } from '@documenso/ee/server-only/stripe/get-prices-by-interval';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
import { SubscriptionStatus } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { LocaleDate } from '~/components/formatter/locale-date';
import { BillingPlans } from './billing-plans';
import { BillingPortalButton } from './billing-portal-button';
export default async function BillingSettingsPage() {
const { user } = await getRequiredServerComponentSession();
@@ -21,57 +23,75 @@ export default async function BillingSettingsPage() {
redirect('/settings/profile');
}
const subscription = await getSubscriptionByUserId({ userId: user.id }).then(async (sub) => {
if (sub) {
return sub;
}
const [subscription, prices] = await Promise.all([
getSubscriptionByUserId({ userId: user.id }),
getPricesByInterval(),
]);
// If we don't have a customer record, create one as well as an empty subscription.
return createCustomer({ user });
});
let subscriptionProduct: Stripe.Product | null = null;
let billingPortalUrl = '';
if (subscription?.planId) {
const foundSubscriptionProduct = (await stripe.products.list()).data.find(
(item) => item.default_price === subscription.planId,
);
if (subscription.customerId) {
billingPortalUrl = await getPortalSession({
customerId: subscription.customerId,
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
});
subscriptionProduct = foundSubscriptionProduct ?? null;
}
const isMissingOrInactiveOrFreePlan = !subscription || subscription.status === 'INACTIVE';
return (
<div>
<h3 className="text-lg font-medium">Billing</h3>
<p className="text-muted-foreground mt-2 text-sm">
Your subscription is{' '}
{subscription.status !== SubscriptionStatus.INACTIVE ? 'active' : 'inactive'}.
{subscription?.periodEnd && (
<>
{' '}
Your next payment is due on{' '}
<span className="font-semibold">
<LocaleDate date={subscription.periodEnd} />
</span>
.
</>
<div className="text-muted-foreground mt-2 text-sm">
{isMissingOrInactiveOrFreePlan && (
<p>
You are currently on the <span className="font-semibold">Free Plan</span>.
</p>
)}
</p>
{!isMissingOrInactiveOrFreePlan &&
match(subscription.status)
.with('ACTIVE', () => (
<p>
{subscriptionProduct ? (
<span>
You are currently subscribed to{' '}
<span className="font-semibold">{subscriptionProduct.name}</span>
</span>
) : (
<span>You currently have an active plan</span>
)}
{subscription.periodEnd && (
<span>
{' '}
which is set to{' '}
{subscription.cancelAtPeriodEnd ? (
<span>
end on{' '}
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
</span>
) : (
<span>
automatically renew on{' '}
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
</span>
)}
</span>
)}
</p>
))
.with('PAST_DUE', () => (
<p>Your current plan is past due. Please update your payment information.</p>
))
.otherwise(() => null)}
</div>
<hr className="my-4" />
{billingPortalUrl && (
<Button asChild>
<Link href={billingPortalUrl}>Manage Subscription</Link>
</Button>
)}
{!billingPortalUrl && (
<p className="text-muted-foreground max-w-[60ch] text-base">
You do not currently have a customer record, this should not happen. Please contact
support for assistance.
</p>
)}
{isMissingOrInactiveOrFreePlan ? <BillingPlans prices={prices} /> : <BillingPortalButton />}
</div>
);
}
@@ -3,6 +3,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
import { randomBytes } from 'crypto';
import { readFileSync } from 'fs';
import { buffer } from 'micro';
import { match } from 'ts-pattern';
import { insertImageInPDF } from '@documenso/lib/server-only/pdf/insert-image-in-pdf';
import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf';
@@ -16,6 +17,7 @@ import {
ReadStatus,
SendStatus,
SigningStatus,
SubscriptionStatus,
} from '@documenso/prisma/client';
const log = (...args: unknown[]) => console.log('[stripe]', ...args);
@@ -54,6 +56,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
);
log('event-type:', event.type);
if (event.type === 'customer.subscription.updated') {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const subscription = event.data.object as Stripe.Subscription;
await handleCustomerSubscriptionUpdated(subscription);
return res.status(200).json({
success: true,
message: 'Webhook received',
});
}
if (event.type === 'checkout.session.completed') {
// This is required since we don't want to create a guard for every event type
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -195,3 +209,29 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
message: 'Unhandled webhook event',
});
}
const handleCustomerSubscriptionUpdated = async (subscription: Stripe.Subscription) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const { plan } = subscription as unknown as Stripe.SubscriptionItem;
const customerId =
typeof subscription.customer === 'string' ? subscription.customer : subscription.customer.id;
const status = match(subscription.status)
.with('active', () => SubscriptionStatus.ACTIVE)
.with('past_due', () => SubscriptionStatus.PAST_DUE)
.otherwise(() => SubscriptionStatus.INACTIVE);
await prisma.subscription.update({
where: {
customerId: customerId,
},
data: {
planId: plan.id,
status,
cancelAtPeriodEnd: subscription.cancel_at_period_end,
periodEnd: new Date(subscription.current_period_end * 1000),
updatedAt: new Date(),
},
});
};
+8 -9
View File
@@ -15,8 +15,8 @@
"devDependencies": {
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"dotenv": "^16.0.3",
"dotenv-cli": "^7.2.1",
"dotenv": "^16.3.1",
"dotenv-cli": "^7.3.0",
"eslint": "^8.40.0",
"eslint-config-custom": "*",
"husky": "^8.0.0",
@@ -9103,7 +9103,6 @@
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
"dev": true,
"engines": {
"node": ">=12"
},
@@ -9112,13 +9111,12 @@
}
},
"node_modules/dotenv-cli": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.2.1.tgz",
"integrity": "sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ==",
"dev": true,
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.3.0.tgz",
"integrity": "sha512-314CA4TyK34YEJ6ntBf80eUY+t1XaFLyem1k9P0sX1gn30qThZ5qZr/ZwE318gEnzyYP9yj9HJk6SqwE0upkfw==",
"dependencies": {
"cross-spawn": "^7.0.3",
"dotenv": "^16.0.0",
"dotenv": "^16.3.0",
"dotenv-expand": "^10.0.0",
"minimist": "^1.2.6"
},
@@ -9130,7 +9128,6 @@
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
"integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
"dev": true,
"engines": {
"node": ">=12"
}
@@ -19889,6 +19886,8 @@
"license": "MIT",
"dependencies": {
"@prisma/client": "5.3.1",
"dotenv": "^16.3.1",
"dotenv-cli": "^7.3.0",
"prisma": "5.3.1"
},
"devDependencies": {
+7 -4
View File
@@ -11,9 +11,12 @@
"commitlint": "commitlint --edit",
"clean": "turbo run clean && rimraf node_modules",
"d": "npm run dx && npm run dev",
"dx": "npm i && npm run dx:up && npm run prisma:migrate-dev -w @documenso/prisma",
"dx": "npm i && npm run dx:up && npm run prisma:migrate-dev",
"dx:up": "docker compose -f docker/compose-services.yml up -d",
"dx:down": "docker compose -f docker/compose-services.yml down"
"dx:down": "docker compose -f docker/compose-services.yml down",
"prisma:migrate-dev": "npm run with:env -- npm run prisma:migrate-dev -w @documenso/prisma",
"prisma:migrate-deploy": "npm run with:env -- npm run prisma:migrate-deploy -w @documenso/prisma",
"with:env": "dotenv -e .env -e .env.local --"
},
"engines": {
"npm": ">=8.6.0",
@@ -22,8 +25,8 @@
"devDependencies": {
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"dotenv": "^16.0.3",
"dotenv-cli": "^7.2.1",
"dotenv": "^16.3.1",
"dotenv-cli": "^7.3.0",
"eslint": "^8.40.0",
"eslint-config-custom": "*",
"husky": "^8.0.0",
@@ -0,0 +1,31 @@
'use server';
import { stripe } from '@documenso/lib/server-only/stripe';
export type GetCheckoutSessionOptions = {
customerId: string;
priceId: string;
returnUrl: string;
};
export const getCheckoutSession = async ({
customerId,
priceId,
returnUrl,
}: GetCheckoutSessionOptions) => {
'use server';
const session = await stripe.checkout.sessions.create({
customer: customerId,
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: `${returnUrl}?success=true`,
cancel_url: `${returnUrl}?canceled=true`,
});
return session.url;
};
@@ -0,0 +1,19 @@
import { stripe } from '@documenso/lib/server-only/stripe';
export const getStripeCustomerByEmail = async (email: string) => {
const foundStripeCustomers = await stripe.customers.list({
email,
});
return foundStripeCustomers.data[0] ?? null;
};
export const getStripeCustomerById = async (stripeCustomerId: string) => {
try {
const stripeCustomer = await stripe.customers.retrieve(stripeCustomerId);
return !stripeCustomer.deleted ? stripeCustomer : null;
} catch {
return null;
}
};
@@ -0,0 +1,40 @@
import Stripe from 'stripe';
import { stripe } from '@documenso/lib/server-only/stripe';
// Utility type to handle usage of the `expand` option.
type PriceWithProduct = Stripe.Price & { product: Stripe.Product };
export type PriceIntervals = Record<Stripe.Price.Recurring.Interval, PriceWithProduct[]>;
export const getPricesByInterval = async () => {
const { data: prices } = await stripe.prices.search({
query: `active:'true' type:'recurring'`,
expand: ['data.product'],
limit: 100,
});
const intervals: PriceIntervals = {
day: [],
week: [],
month: [],
year: [],
};
// Add each price to the correct interval.
for (const price of prices) {
if (price.recurring?.interval) {
// We use `expand` to get the product, but it's not typed as part of the Price type.
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
intervals[price.recurring.interval].push(price as PriceWithProduct);
}
}
// Order all prices by unit_amount.
intervals.day.sort((a, b) => Number(a.unit_amount) - Number(b.unit_amount));
intervals.week.sort((a, b) => Number(a.unit_amount) - Number(b.unit_amount));
intervals.month.sort((a, b) => Number(a.unit_amount) - Number(b.unit_amount));
intervals.year.sort((a, b) => Number(a.unit_amount) - Number(b.unit_amount));
return intervals;
};
@@ -0,0 +1,13 @@
'use server';
import { prisma } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client';
export type DeleteDraftDocumentOptions = {
id: number;
userId: number;
};
export const deleteDraftDocument = async ({ id, userId }: DeleteDraftDocumentOptions) => {
return await prisma.document.delete({ where: { id, userId, status: DocumentStatus.DRAFT } });
};
+1
View File
@@ -1,3 +1,4 @@
/// <reference types="./stripe.d.ts" />
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.NEXT_PRIVATE_STRIPE_API_KEY ?? '', {
+7
View File
@@ -0,0 +1,7 @@
declare module 'stripe' {
namespace Stripe {
interface Product {
features?: Array<{ name: string }>;
}
}
}
@@ -0,0 +1,3 @@
export const toHumanPrice = (price: number) => {
return Number(price / 100).toFixed(2);
};
@@ -0,0 +1,17 @@
/*
Warnings:
- A unique constraint covering the columns `[userId]` on the table `Subscription` will be added. If there are existing duplicate values, this will fail.
- Made the column `customerId` on table `Subscription` required. This step will fail if there are existing NULL values in that column.
*/
DELETE FROM "Subscription"
WHERE "customerId" IS NULL;
-- AlterTable
ALTER TABLE "Subscription" ADD COLUMN "cancelAtPeriodEnd" BOOLEAN NOT NULL DEFAULT false,
ALTER COLUMN "customerId" SET NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "Subscription_userId_key" ON "Subscription"("userId");
+2
View File
@@ -18,6 +18,8 @@
},
"dependencies": {
"@prisma/client": "5.3.1",
"dotenv": "^16.3.1",
"dotenv-cli": "^7.3.0",
"prisma": "5.3.1"
},
"devDependencies": {
+10 -9
View File
@@ -51,15 +51,16 @@ enum SubscriptionStatus {
}
model Subscription {
id Int @id @default(autoincrement())
status SubscriptionStatus @default(INACTIVE)
planId String?
priceId String?
customerId String?
periodEnd DateTime?
userId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @id @default(autoincrement())
status SubscriptionStatus @default(INACTIVE)
planId String?
priceId String?
customerId String
periodEnd DateTime?
userId Int @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cancelAtPeriodEnd Boolean @default(false)
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
+7 -3
View File
@@ -12,12 +12,16 @@ export const authRouter = router({
return await createUser({ name, email, password, signature });
} catch (err) {
console.error(err);
let message =
'We were unable to create your account. Please review the information you provided and try again.';
if (err instanceof Error && err.message === 'User already exists') {
message = 'User with this email already exists. Please use a different email address.';
}
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to create your account. Please review the information you provided and try again.',
message,
});
}
}),
@@ -1,6 +1,7 @@
import { TRPCError } from '@trpc/server';
import { createDocument } from '@documenso/lib/server-only/document/create-document';
import { deleteDraftDocument } from '@documenso/lib/server-only/document/delete-draft-document';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
@@ -10,6 +11,7 @@ import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/s
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
ZCreateDocumentMutationSchema,
ZDeleteDraftDocumentMutationSchema,
ZGetDocumentByIdQuerySchema,
ZGetDocumentByTokenQuerySchema,
ZSendDocumentMutationSchema,
@@ -76,6 +78,25 @@ export const documentRouter = router({
}
}),
deleteDraftDocument: authenticatedProcedure
.input(ZDeleteDraftDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { id } = input;
const userId = ctx.user.id;
return await deleteDraftDocument({ id, userId });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to delete this document. Please try again later.',
});
}
}),
setRecipientsForDocument: authenticatedProcedure
.input(ZSetRecipientsForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
@@ -61,3 +61,9 @@ export const ZSendDocumentMutationSchema = z.object({
});
export type TSendDocumentMutationSchema = z.infer<typeof ZSendDocumentMutationSchema>;
export const ZDeleteDraftDocumentMutationSchema = z.object({
id: z.number().min(1),
});
export type TDeleteDraftDocumentMutationSchema = z.infer<typeof ZDeleteDraftDocumentMutationSchema>;
+1
View File
@@ -10,6 +10,7 @@ declare namespace NodeJS {
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
NEXT_PRIVATE_STRIPE_API_KEY: string;
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
+5 -2
View File
@@ -16,12 +16,13 @@ const DialogPortal = ({
children,
position = 'start',
...props
}: DialogPrimitive.DialogPortalProps & { position?: 'start' | 'end' }) => (
}: DialogPrimitive.DialogPortalProps & { position?: 'start' | 'end' | 'center' }) => (
<DialogPrimitive.Portal className={cn(className)} {...props}>
<div
className={cn('fixed inset-0 z-50 flex justify-center sm:items-center', {
'items-start': position === 'start',
'items-end': position === 'end',
'items-center': position === 'center',
})}
>
{children}
@@ -49,7 +50,9 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { position?: 'start' | 'end' }
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
position?: 'start' | 'end' | 'center';
}
>(({ className, children, position = 'start', ...props }, ref) => (
<DialogPortal position={position}>
<DialogOverlay />
@@ -97,10 +97,7 @@ export const DocumentFlowFormContainerStep = ({
return (
<div>
<p className="text-muted-foreground text-sm">
{title}{' '}
<span>
({step}/{maxStep})
</span>
Step <span>{`${step} of ${maxStep}`}</span>
</p>
<div className="bg-muted relative mt-4 h-[2px] rounded-md">
+2 -1
View File
@@ -34,6 +34,7 @@
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID",
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID",
"NEXT_PUBLIC_STRIPE_FREE_PLAN_ID",
"NEXT_PRIVATE_DATABASE_URL",
"NEXT_PRIVATE_DIRECT_DATABASE_URL",
"NEXT_PRIVATE_GOOGLE_CLIENT_ID",
@@ -75,4 +76,4 @@
"POSTGRES_PRISMA_URL",
"POSTGRES_URL_NON_POOLING"
]
}
}