onboarding completed
This commit is contained in:
committed by
Carl-Gerhard Lindesvärd
parent
97627583ec
commit
7d22d2ddad
385
apps/dashboard/src/modals/Instructions.tsx
Normal file
385
apps/dashboard/src/modals/Instructions.tsx
Normal file
@@ -0,0 +1,385 @@
|
||||
import Syntax from '@/components/syntax';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Button, LinkButton } from '@/components/ui/button';
|
||||
import {
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet';
|
||||
import { ExternalLinkIcon, XIcon } from 'lucide-react';
|
||||
|
||||
import type { IServiceClient } from '@openpanel/db';
|
||||
import type { frameworks } from '@openpanel/sdk-info';
|
||||
|
||||
import { popModal } from '.';
|
||||
|
||||
type Props = {
|
||||
client: IServiceClient | null;
|
||||
framework:
|
||||
| (typeof frameworks.website)[number]
|
||||
| (typeof frameworks.app)[number]
|
||||
| (typeof frameworks.backend)[number];
|
||||
};
|
||||
|
||||
const Header = ({ framework }: Pick<Props, 'framework'>) => (
|
||||
<SheetHeader>
|
||||
<SheetTitle>Instructions for {framework.name}</SheetTitle>
|
||||
</SheetHeader>
|
||||
);
|
||||
|
||||
const Footer = ({ framework }: Pick<Props, 'framework'>) => (
|
||||
<SheetFooter>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
className="flex-1"
|
||||
onClick={() => popModal()}
|
||||
icon={XIcon}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<LinkButton
|
||||
target="_blank"
|
||||
href={framework.href}
|
||||
className="flex-1"
|
||||
icon={ExternalLinkIcon}
|
||||
>
|
||||
More details
|
||||
</LinkButton>
|
||||
</SheetFooter>
|
||||
);
|
||||
|
||||
const Instructions = ({ framework, client }: Props) => {
|
||||
const { name } = framework;
|
||||
const clientId = client?.id || 'REPLACE_WITH_YOUR_CLIENT';
|
||||
const clientSecret = client?.secret || 'REPLACE_WITH_YOUR_SECRET';
|
||||
if (
|
||||
name === 'HTML / Script' ||
|
||||
name === 'React' ||
|
||||
name === 'Astro' ||
|
||||
name === 'Remix' ||
|
||||
name === 'Vue'
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>Copy the code below and insert it to you website</p>
|
||||
<Syntax
|
||||
code={`<script src="https://openpanel.dev/op.js" defer async></script>
|
||||
<script>
|
||||
window.op = window.op || function (...args) { (window.op.q = window.op.q || []).push(args); };
|
||||
window.op('ctor', {
|
||||
clientId: '${clientId}',
|
||||
trackScreenViews: true,
|
||||
trackOutgoingLinks: true,
|
||||
trackAttributes: true,
|
||||
});
|
||||
</script>`}
|
||||
/>
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
We have already added your client id to the snippet.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'Next.js') {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>Install dependencies</p>
|
||||
<Syntax code={`pnpm install @openpanel/nextjs`} />
|
||||
<p>Add OpenpanelProvider to your root layout</p>
|
||||
<Syntax
|
||||
code={`import { OpenpanelProvider } from '@openpanel/nextjs';
|
||||
|
||||
export default RootLayout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<OpenpanelProvider
|
||||
clientId="${clientId}"
|
||||
trackScreenViews={true}
|
||||
trackAttributes={true}
|
||||
trackOutgoingLinks={true}
|
||||
// If you have a user id, you can pass it here to identify the user
|
||||
// profileId={'123'}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}`}
|
||||
/>
|
||||
<p>
|
||||
This will track regular page views and outgoing links. You can also
|
||||
track custom events.
|
||||
</p>
|
||||
<Syntax
|
||||
code={`import { trackEvent } from '@openpanel/nextjs';
|
||||
|
||||
// Sends an event with payload foo: bar
|
||||
trackEvent('my_event', { foo: 'bar' });
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'Laravel') {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>Install dependencies</p>
|
||||
<Syntax code={`composer require bleckert/openpanel-laravel`} />
|
||||
<p>Add environment variables</p>
|
||||
<Syntax
|
||||
code={`OPENPANEL_CLIENT_ID=${clientId}
|
||||
OPENPANEL_CLIENT_SECRET=${clientSecret}`}
|
||||
/>
|
||||
<p>Usage</p>
|
||||
<Syntax
|
||||
code={`use Bleckert\\OpenpanelLaravel\\Openpanel;
|
||||
|
||||
$openpanel = app(Openpanel::class);
|
||||
|
||||
// Identify user
|
||||
$openpanel->setProfileId(1);
|
||||
|
||||
// Update user profile
|
||||
$openpanel->setProfile(
|
||||
id: 1,
|
||||
firstName: 'John Doe',
|
||||
// ...
|
||||
);
|
||||
|
||||
// Track event
|
||||
$openpanel->event(
|
||||
name: 'User registered',
|
||||
);
|
||||
`}
|
||||
/>
|
||||
<Alert>
|
||||
<AlertTitle>Shoutout!</AlertTitle>
|
||||
<AlertDescription>
|
||||
Huge shoutout to{' '}
|
||||
<a
|
||||
href="https://twitter.com/tbleckert"
|
||||
target="_blank"
|
||||
className="underline"
|
||||
>
|
||||
@tbleckert
|
||||
</a>{' '}
|
||||
for creating this package.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'Rest API') {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<strong>Authentication</strong>
|
||||
<p>You will need to pass your client ID and secret via headers.</p>
|
||||
<strong>Usage</strong>
|
||||
<p>Create a custom event called "my_event".</p>
|
||||
<Syntax
|
||||
code={`curl 'https://api.openpanel.dev/event' \\
|
||||
-H 'content-type: application/json' \\
|
||||
-H 'openpanel-client-id: ${clientId}' \\
|
||||
-H 'openpanel-client-secret: ${clientSecret}' \\
|
||||
--data-raw '{"name":"my_event","properties":{"foo":"bar"},"timestamp":"2024-03-28T08:42:54.319Z"}'`}
|
||||
/>
|
||||
<p>The payload should be a JSON object with the following fields:</p>
|
||||
<ul className="list-inside list-disc">
|
||||
<li>"name" (string): The name of the event.</li>
|
||||
<li>"properties" (object): The properties of the event.</li>
|
||||
<li>
|
||||
"timestamp" (string): The timestamp of the event in ISO
|
||||
8601 format.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'Express') {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<strong>Install dependencies</strong>
|
||||
<Syntax code={`npm install @openpanel/express`} />
|
||||
|
||||
<strong>Usage</strong>
|
||||
<p>Connect the middleware to your app.</p>
|
||||
<Syntax
|
||||
code={`import express from 'express';
|
||||
import createOpenpanelMiddleware from '@openpanel/express';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
createOpenpanelMiddleware({
|
||||
clientId: '${clientId}',
|
||||
clientSecret: '${clientSecret}',
|
||||
// trackRequest(url) {
|
||||
// return url.includes('/v1')
|
||||
// },
|
||||
// getProfileId(req) {
|
||||
// return req.user.id
|
||||
// }
|
||||
})
|
||||
);
|
||||
|
||||
app.get('/sign-up', (req, res) => {
|
||||
// track sign up events
|
||||
req.op.event('sign-up', {
|
||||
email: req.body.email,
|
||||
});
|
||||
res.send('Hello World');
|
||||
});
|
||||
|
||||
app.listen(3000, () => {
|
||||
console.log('Server is running on http://localhost:3000');
|
||||
});`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'Node') {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<strong>Install dependencies</strong>
|
||||
<Syntax code={`pnpm install @openpanel/sdk`} />
|
||||
|
||||
<strong>Create a instance</strong>
|
||||
<p>
|
||||
Create a new instance of OpenpanelSdk. You can use this SDK in any JS
|
||||
environment. You should omit clientSecret if you use this on web!
|
||||
</p>
|
||||
<Syntax
|
||||
code={`import { OpenpanelSdk } from '@openpanel/sdk';
|
||||
|
||||
const op = new OpenpanelSdk({
|
||||
clientId: '${clientId}',
|
||||
// mostly for backend and apps that can't rely on CORS
|
||||
clientSecret: '${clientSecret}',
|
||||
});`}
|
||||
/>
|
||||
<strong>Usage</strong>
|
||||
<Syntax
|
||||
code={`import { op } from './openpanel';
|
||||
|
||||
// Sends an event with payload foo: bar
|
||||
op.event('my_event', { foo: 'bar' });
|
||||
|
||||
// Identify with profile id
|
||||
op.setProfileId('123');
|
||||
|
||||
// or with additional data
|
||||
op.setProfile({
|
||||
profileId: '123',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@openpanel.dev',
|
||||
});
|
||||
|
||||
// Increment a property
|
||||
op.increment('app_opened'); // increment by 1
|
||||
op.increment('app_opened', 5); // increment by 5
|
||||
|
||||
// Decrement a property
|
||||
op.decrement('app_opened'); // decrement by 1
|
||||
op.decrement('app_opened', 5); // decrement by 5`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'React-Native') {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<strong>Install dependencies</strong>
|
||||
<p>Don't forget to install the peer dependencies as well!</p>
|
||||
<Syntax
|
||||
code={`pnpm install @openpanel/react-native
|
||||
npx expo install --pnpm expo-application expo-constants`}
|
||||
/>
|
||||
<strong>Create a instance</strong>
|
||||
<p>
|
||||
Create a new instance of OpenpanelSdk. You can use this SDK in any JS
|
||||
environment. You should omit clientSecret if you use this on web!
|
||||
</p>
|
||||
<Syntax
|
||||
code={`import { Openpanel } from '@openpanel/react-native';
|
||||
|
||||
const op = new Openpanel({
|
||||
clientId: '${clientId}',
|
||||
clientSecret: '${clientSecret}',
|
||||
});`}
|
||||
/>
|
||||
<strong>Usage</strong>
|
||||
<Syntax
|
||||
code={`import { op } from './openpanel';
|
||||
|
||||
// Sends an event with payload foo: bar
|
||||
op.event('my_event', { foo: 'bar' });
|
||||
|
||||
// Identify with profile id
|
||||
op.setProfileId('123');
|
||||
|
||||
// or with additional data
|
||||
op.setProfile({
|
||||
profileId: '123',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@openpanel.dev',
|
||||
});
|
||||
|
||||
// Increment a property
|
||||
op.increment('app_opened'); // increment by 1
|
||||
op.increment('app_opened', 5); // increment by 5
|
||||
|
||||
// Decrement a property
|
||||
op.decrement('app_opened'); // decrement by 1
|
||||
op.decrement('app_opened', 5); // decrement by 5`}
|
||||
/>
|
||||
<strong>Navigation</strong>
|
||||
<p>
|
||||
Check out our{' '}
|
||||
<a
|
||||
href="https://github.com/Openpanel-dev/examples/tree/main/expo-app"
|
||||
target="_blank"
|
||||
className="underline"
|
||||
>
|
||||
example app
|
||||
</a>{' '}
|
||||
. See below for a quick demo.
|
||||
</p>
|
||||
<Syntax
|
||||
code={`function RootLayoutNav() {
|
||||
const pathname = usePathname()
|
||||
|
||||
useEffect(() => {
|
||||
op.screenView(pathname)
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{/*... */}
|
||||
</Stack>
|
||||
);
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function InsdtructionsWithModalContent(props: Props) {
|
||||
return (
|
||||
<SheetContent>
|
||||
<Header framework={props.framework} />
|
||||
<Instructions {...props} />
|
||||
<Footer framework={props.framework} />
|
||||
</SheetContent>
|
||||
);
|
||||
}
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DialogContent } from '@/components/ui/dialog';
|
||||
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
import { popModal } from '..';
|
||||
|
||||
interface ModalContentProps {
|
||||
interface ModalContentProps extends DialogContentProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ModalContent({ children, className }: ModalContentProps) {
|
||||
return <DialogContent className={className}>{children}</DialogContent>;
|
||||
export function ModalContent({ children, ...props }: ModalContentProps) {
|
||||
return <DialogContent {...props}>{children}</DialogContent>;
|
||||
}
|
||||
|
||||
interface ModalHeaderProps {
|
||||
|
||||
51
apps/dashboard/src/modals/OnboardingTroubleshoot.tsx
Normal file
51
apps/dashboard/src/modals/OnboardingTroubleshoot.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { GlobeIcon, KeyIcon, UserIcon } from 'lucide-react';
|
||||
|
||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||
|
||||
export default function OnboardingTroubleshoot() {
|
||||
return (
|
||||
<ModalContent>
|
||||
<ModalHeader
|
||||
title="Troubleshoot"
|
||||
text="Hmm, you have troubles? Well, let's solve them together."
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Alert>
|
||||
<UserIcon size={16} />
|
||||
<AlertTitle>Wrong client ID</AlertTitle>
|
||||
<AlertDescription>
|
||||
Make sure your <code>clientId</code> is correct
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert>
|
||||
<GlobeIcon size={16} />
|
||||
<AlertTitle>Wrong domain on web</AlertTitle>
|
||||
<AlertDescription>
|
||||
For web apps its important that the domain is correctly configured.
|
||||
We authenticate the requests based on the domain.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert>
|
||||
<KeyIcon size={16} />
|
||||
<AlertTitle>Wrong client secret</AlertTitle>
|
||||
<AlertDescription>
|
||||
For app and backend events it's important that you have correct{' '}
|
||||
<code>clientId</code> and <code>clientSecret</code>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
<p className="mt-4 text-sm">
|
||||
Still have issues? Join our{' '}
|
||||
<a href="https://go.openpanel.dev/discord" className="underline">
|
||||
discord channel
|
||||
</a>{' '}
|
||||
give us an email at{' '}
|
||||
<a href="mailto:hello@openpanel.dev" className="underline">
|
||||
hello@openpanel.dev
|
||||
</a>{' '}
|
||||
and we'll help you out.
|
||||
</p>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
91
apps/dashboard/src/modals/VerifyEmail.tsx
Normal file
91
apps/dashboard/src/modals/VerifyEmail.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from '@/components/ui/input-otp';
|
||||
import { getClerkError } from '@/utils/clerk-error';
|
||||
import { useSignUp } from '@clerk/nextjs';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { popModal } from '.';
|
||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
export default function VerifyEmail({ email }: Props) {
|
||||
const { signUp, setActive, isLoaded } = useSignUp();
|
||||
const router = useRouter();
|
||||
const [code, setCode] = useState('');
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<ModalHeader
|
||||
title="Verify your email"
|
||||
text={
|
||||
<p>
|
||||
Please enter the verification code sent to your{' '}
|
||||
<span className="font-semibold">{email}</span>.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={code}
|
||||
onChange={setCode}
|
||||
onComplete={async () => {
|
||||
if (!isLoaded) {
|
||||
return toast.info('Sign up is not available at the moment');
|
||||
}
|
||||
|
||||
try {
|
||||
const completeSignUp = await signUp.attemptEmailAddressVerification(
|
||||
{
|
||||
code,
|
||||
}
|
||||
);
|
||||
|
||||
if (completeSignUp.status !== 'complete') {
|
||||
// The status can also be `abandoned` or `missing_requirements`
|
||||
// Please see https://clerk.com/docs/references/react/use-sign-up#result-status for more information
|
||||
return toast.error('Invalid code');
|
||||
}
|
||||
|
||||
// Check the status to see if it is complete
|
||||
// If complete, the user has been created -- set the session active
|
||||
if (completeSignUp.status === 'complete') {
|
||||
await setActive({ session: completeSignUp.createdSessionId });
|
||||
router.push('/onboarding');
|
||||
popModal();
|
||||
}
|
||||
} catch (e) {
|
||||
const error = getClerkError(e);
|
||||
if (error) {
|
||||
toast.error(error.longMessage);
|
||||
} else {
|
||||
toast.error('An error occurred, please try again later');
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
@@ -47,6 +47,15 @@ const modals = {
|
||||
AddReference: dynamic(() => import('./AddReference'), {
|
||||
loading: Loading,
|
||||
}),
|
||||
Instructions: dynamic(() => import('./Instructions'), {
|
||||
loading: Loading,
|
||||
}),
|
||||
OnboardingTroubleshoot: dynamic(() => import('./OnboardingTroubleshoot'), {
|
||||
loading: Loading,
|
||||
}),
|
||||
VerifyEmail: dynamic(() => import('./VerifyEmail'), {
|
||||
loading: Loading,
|
||||
}),
|
||||
};
|
||||
|
||||
export const { pushModal, popModal, popAllModals, ModalProvider } =
|
||||
|
||||
Reference in New Issue
Block a user