onboarding completed

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-04-16 11:41:15 +02:00
committed by Carl-Gerhard Lindesvärd
parent 97627583ec
commit 7d22d2ddad
79 changed files with 2542 additions and 805 deletions

View 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 &quot;my_event&quot;.</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>&quot;name&quot; (string): The name of the event.</li>
<li>&quot;properties&quot; (object): The properties of the event.</li>
<li>
&quot;timestamp&quot; (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&apos;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>
);
}

View File

@@ -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 {

View 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&apos;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&apos;ll help you out.
</p>
</ModalContent>
);
}

View 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>
);
}

View File

@@ -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 } =