Compare commits

..

25 Commits

Author SHA1 Message Date
Trisha Lim
b11018da81 Styling for pricing section 2024-10-10 17:40:07 +01:00
Anselm
3311828432 Address some wording feedback 2024-10-10 17:37:28 +01:00
Anselm
f03f8dfeaa Better CTA subtext 2024-10-10 16:35:43 +01:00
Anselm
05e0b3d795 Less cringe CTA 2024-10-10 16:33:24 +01:00
Anselm
7ce08753f1 Simplify pricing 2024-10-10 16:27:33 +01:00
Trisha Lim
01e9255d3b Copy changes
Co-authored-by: Anselm Eickhoff <anselm.eickhoff@gmail.com>
2024-10-10 15:06:05 +01:00
Trisha Lim
0e918f60e3 Add showcase page 2024-10-10 15:06:05 +01:00
Trisha Lim
b08c5ec1d2 Add CTA to become an early adopter (#522) 2024-10-10 15:04:38 +01:00
Anselm Eickhoff
87f24fa985 Merge pull request #521 from gardencmp/feature/local-test-runner
chore(dx): configure vitest to run in watch mode on the local machine
2024-10-10 14:46:55 +01:00
Anselm Eickhoff
86df2f0132 Merge pull request #514 from gardencmp/JAZZ-314/react-native-proof-of-concept-continued
react-native chat demo with Clerk auth
2024-10-10 11:56:24 +01:00
pax-k
0e1654a474 chore: pnpm lock 2024-10-10 12:39:07 +03:00
pax-k
05f447baae Merge branch 'main' into JAZZ-314/react-native-proof-of-concept-continued 2024-10-10 12:35:54 +03:00
Guido D'Orsi
85e2bb230e chore(dx): configure vitest to run in watch mode on the local machine 2024-10-10 01:45:53 +02:00
Guido D'Orsi
123a57169a Merge pull request #520 from gardencmp/fix/radix-react-version
fix: add react to the pnpm overrides because some examples are experiencing version mismatch
2024-10-09 23:56:20 +02:00
Guido D'Orsi
b0101f8157 fix: add react to the pnpm overrides because some examples are experiencing version mismatch 2024-10-09 23:52:13 +02:00
pax-k
1e94004364 chore(react-native): chat demo bundle id 2024-10-09 22:19:36 +03:00
pax-k
76e7a81eef chore(react-native): DRY platform exports 2024-10-09 22:01:54 +03:00
pax-k
1445e4ab80 chore: cleanup 2024-10-09 19:55:36 +03:00
pax-k
6c8534a709 chore: cleanup 2024-10-09 19:51:29 +03:00
pax-k
508720745f chore: cleanup 2024-10-09 17:58:13 +03:00
pax-k
2fde6a47aa chore: cleanup 2024-10-09 17:49:19 +03:00
pax-k
22709e6fe0 chore: pnpm lock 2024-10-09 17:46:30 +03:00
pax-k
4ee3f7229c Merge branch 'main' into JAZZ-314/react-native-proof-of-concept-continued 2024-10-09 17:46:00 +03:00
pax-k
09e0e17559 feat(react-native): demo chat with clerk auth 2024-10-09 17:44:09 +03:00
pax-k
7b6656e99e feat(react-native): clerk auth (wip) 2024-10-09 14:36:10 +03:00
78 changed files with 5448 additions and 1442 deletions

View File

@@ -44,4 +44,4 @@ jobs:
run: pnpm turbo build
- name: Unit Tests
run: pnpm test
run: pnpm test:ci

View File

@@ -0,0 +1 @@
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_ZXZpZGVudC1kYW5lLTg5LmNsZXJrLmFjY291bnRzLmRldiQ

View File

@@ -0,0 +1,4 @@
// https://docs.expo.dev/guides/using-eslint/
module.exports = {
extends: 'expo',
};

17
examples/chat-rn-clerk/.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
# macOS
.DS_Store
ios
android

View File

@@ -0,0 +1,76 @@
<p align="center">
<a href="https://clerk.com?utm_source=github&utm_medium=clerk_docs" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./assets/images/light-logo.png">
<img alt="Clerk Logo for light background" src="./assets/images/dark-logo.png" height="64">
</picture>
</a>
<br />
</p>
<div align="center">
<h1>
Clerk and Expo Quickstart
</h1>
<a href="https://www.npmjs.com/package/@clerk/clerk-js">
<img alt="Downloads" src="https://img.shields.io/npm/dm/@clerk/clerk-js" />
</a>
<a href="https://discord.com/invite/b5rXHjAg7A">
<img alt="Discord" src="https://img.shields.io/discord/856971667393609759?color=7389D8&label&logo=discord&logoColor=ffffff" />
</a>
<a href="https://twitter.com/clerkdev">
<img alt="Twitter" src="https://img.shields.io/twitter/url.svg?label=%40clerkdev&style=social&url=https%3A%2F%2Ftwitter.com%2Fclerkdev" />
</a>
<br />
<br />
<img alt="Clerk Hero Image" src="./assets/images/hero.png">
</div>
## Introduction
Clerk is a developer-first authentication and user management solution. It provides pre-built React components and hooks for sign-in, sign-up, user profile, and organization management. Clerk is designed to be easy to use and customize, and can be dropped into any React or Next.js application.
After following the quickstart you'll have learned how to:
- Install `@clerk/clerk-expo`
- Setup your environment key
- Wrap your Expo app in `<ClerkProvider />` and supply your `tokenCache`
- Conditionally show content based on your auth state
- Build your sign-in and sign-up pages
## Running the template
```bash
git clone https://github.com/clerk/clerk-expo-quickstart
```
To run the example locally, you'll need to make sure you have XCode installed and configured properly, then:
1. Sign up for a Clerk account at [https://clerk.com](https://dashboard.clerk.com/sign-up?utm_source=DevRel&utm_medium=docs&utm_campaign=templates&utm_content=10-24-2023&utm_term=clerk-expo-quickstart).
2. Go to the [Clerk dashboard](https://dashboard.clerk.com?utm_source=DevRel&utm_medium=docs&utm_campaign=templates&utm_content=10-24-2023&utm_term=clerk-expo-quickstart) and create an application.
3. Set the required Clerk environment variable as shown in [the example `env` file](./.env.example).
4. `npm install` the required dependencies.
5. `npm run start` to launch the development server.
## Learn more
To learn more about Clerk and Expo, check out the following resources:
- [Quickstart: Get started with Expo and Clerk](https://clerk.com/docs/quickstarts/expo?utm_source=DevRel&utm_medium=docs&utm_campaign=templates&utm_content=10-24-2023&utm_term=clerk-expo-quickstart)
- [Clerk Documentation](https://clerk.com/docs/references/expo/overview?utm_source=DevRel&utm_medium=docs&utm_campaign=templates&utm_content=10-24-2023&utm_term=clerk-expo-quickstart)
- [Expo Documentation](https://docs.expo.dev/)
## Found an issue or want to leave feedback
Feel free to create a support thread on our [Discord](https://clerk.com/discord). Our support team will be happy to assist you in the `#support` channel.
## Connect with us
You can discuss ideas, ask questions, and meet others from the community in our [Discord](https://discord.com/invite/b5rXHjAg7A).
If you prefer, you can also find support through our [Twitter](https://twitter.com/ClerkDev), or you can [email](mailto:support@clerk.dev) us!

View File

@@ -0,0 +1,45 @@
{
"expo": {
"name": "jazz-chat-rn-clerk",
"scheme": "jazz-chat-rn-clerk",
"slug": "jazz-chat-rn-clerk",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/images/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.jazz.chatrnclerk"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.jazz.chatrnclerk"
},
"plugins": [
[
"expo-build-properties",
{
"ios": {
"newArchEnabled": true
},
"android": {
"newArchEnabled": true
}
}
]
],
"extra": {
"eas": {
"projectId": "ca3d46e5-a10a-47ec-9d77-3b841e1c62d4"
}
}
}
}

View File

@@ -0,0 +1,17 @@
import React from "react";
import { useAuth } from "../../src/auth-context";
import { Redirect, Stack } from "expo-router";
export default function HomeLayout() {
const { isAuthenticated } = useAuth();
if (isAuthenticated) {
return <Redirect href={"/chat"} />;
}
return (
<Stack
screenOptions={{ headerShown: false, headerBackVisible: true }}
/>
);
}

View File

@@ -0,0 +1,33 @@
import React from "react";
import { SignedOut } from "@clerk/clerk-expo";
import { Link } from "expo-router";
import { Text, View } from "react-native";
export default function HomePage() {
return (
<View className="flex-1 justify-center items-center bg-gray-100 p-6">
<SignedOut>
<View className="bg-white p-6 rounded-lg shadow-lg w-11/12 max-w-md">
<Text className="text-2xl font-bold text-center text-gray-900 mb-4">
Jazz 🤝 Clerk 🤝 Expo
</Text>
<Link href="/sign-in" className="mb-4">
<Text className="text-center text-blue-600 underline text-lg">
Sign In
</Text>
</Link>
<Link href="/sign-in-oauth" className="mb-4">
<Text className="text-center text-blue-600 underline text-lg">
Sign In OAuth
</Text>
</Link>
<Link href="/sign-up">
<Text className="text-center text-blue-600 underline text-lg">
Sign Up
</Text>
</Link>
</View>
</SignedOut>
</View>
);
}

View File

@@ -0,0 +1,20 @@
import { Redirect, Stack } from "expo-router";
import { useAuth } from "../../src/auth-context";
export default function UnAuthenticatedLayout() {
const { isAuthenticated } = useAuth();
if (isAuthenticated) {
return <Redirect href={"/chat"} />;
}
return (
<Stack
screenOptions={{
headerShown: true,
headerBackVisible: true,
headerTitle: "",
}}
/>
);
}

View File

@@ -0,0 +1,65 @@
import React from "react";
import * as WebBrowser from "expo-web-browser";
import { Text, View, TouchableOpacity } from "react-native";
import { Link } from "expo-router";
import { useOAuth } from "@clerk/clerk-expo";
import * as Linking from "expo-linking";
export const useWarmUpBrowser = () => {
React.useEffect(() => {
// Warm up the android browser to improve UX
// https://docs.expo.dev/guides/authentication/#improving-user-experience
void WebBrowser.warmUpAsync();
return () => {
void WebBrowser.coolDownAsync();
};
}, []);
};
WebBrowser.maybeCompleteAuthSession();
const SignInWithOAuth = () => {
useWarmUpBrowser();
const { startOAuthFlow } = useOAuth({ strategy: "oauth_google" });
const onPress = React.useCallback(async () => {
try {
const { createdSessionId, signIn, signUp, setActive } =
await startOAuthFlow({
redirectUrl: Linking.createURL("/", {
scheme: "jazz-chat-rn-clerk",
}),
});
if (createdSessionId) {
setActive!({ session: createdSessionId });
} else {
// Use signIn or signUp for next steps such as MFA
}
} catch (err) {
console.error("OAuth error", err);
}
}, []);
return (
<View className="flex-1 justify-center items-center bg-gray-50 p-6">
<View className="bg-white w-11/12 max-w-md p-8 rounded-lg shadow-lg items-center">
<TouchableOpacity
onPress={onPress}
className="w-full bg-red-500 py-3 rounded-lg flex items-center justify-center"
>
<Text className="text-white text-lg font-semibold">
Sign in with Google
</Text>
</TouchableOpacity>
<Link href="/" className="mt-4">
<Text className="text-blue-600 text-lg font-semibold underline mb-6">
Back to Home
</Text>
</Link>
</View>
</View>
);
};
export default SignInWithOAuth;

View File

@@ -0,0 +1,91 @@
import { useSignIn } from "@clerk/clerk-expo";
import { Text, TextInput, View, TouchableOpacity } from "react-native";
import React from "react";
import { Link } from "expo-router";
export default function SignInPage() {
const { signIn, setActive, isLoaded } = useSignIn();
const [emailAddress, setEmailAddress] = React.useState("");
const [password, setPassword] = React.useState("");
const [errorMessage, setErrorMessage] = React.useState("");
const onSignInPress = React.useCallback(async () => {
if (!isLoaded) {
return;
}
setErrorMessage("");
try {
const signInAttempt = await signIn.create({
identifier: emailAddress,
password,
});
if (signInAttempt.status === "complete") {
await setActive({ session: signInAttempt.createdSessionId });
} else {
console.error(JSON.stringify(signInAttempt, null, 2));
setErrorMessage("Invalid credentials. Please try again.");
}
} catch (err: any) {
console.error(JSON.stringify(err, null, 2));
if (err.errors && err.errors[0]?.message) {
setErrorMessage(err.errors[0].message);
} else {
setErrorMessage(
"An unexpected error occurred. Please try again.",
);
}
}
}, [isLoaded, emailAddress, password]);
return (
<View className="flex-1 justify-center items-center bg-gray-50 p-6">
<View className="bg-white w-11/12 max-w-md p-8 rounded-lg shadow-md">
<Text className="text-3xl font-bold text-center text-gray-800 mb-6">
Sign In
</Text>
{errorMessage ? (
<Text className="text-red-500 text-center mb-4">
{errorMessage}
</Text>
) : null}
<TextInput
autoCapitalize="none"
value={emailAddress}
placeholder="Email..."
onChangeText={(emailAddress) =>
setEmailAddress(emailAddress)
}
className="w-full h-12 mb-4 px-4 bg-gray-100 border border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none"
/>
<TextInput
value={password}
placeholder="Password..."
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
className="w-full h-12 mb-6 px-4 bg-gray-100 border border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none"
/>
<TouchableOpacity
onPress={onSignInPress}
className="w-full h-12 bg-blue-600 rounded-lg flex items-center justify-center"
>
<Text className="text-white text-lg font-semibold">
Sign In
</Text>
</TouchableOpacity>
<View className="flex-row items-center justify-center mt-4">
<Text className="text-gray-600">
Don't have an account?
</Text>
<Link href="/sign-up">
<Text className="text-blue-500 ml-2 font-semibold">
Sign up
</Text>
</Link>
</View>
</View>
</View>
);
}

View File

@@ -0,0 +1,128 @@
import * as React from "react";
import { TextInput, View, Text, TouchableOpacity } from "react-native";
import { useSignUp } from "@clerk/clerk-expo";
import { useNavigation } from "@react-navigation/native";
export default function SignUpPage() {
const { isLoaded, signUp, setActive } = useSignUp();
const [emailAddress, setEmailAddress] = React.useState("");
const [password, setPassword] = React.useState("");
const [pendingVerification, setPendingVerification] = React.useState(false);
const [code, setCode] = React.useState("");
const [errorMessage, setErrorMessage] = React.useState("");
const navigation = useNavigation();
const onSignUpPress = async () => {
if (!isLoaded) return;
setErrorMessage("");
try {
await signUp.create({
emailAddress,
password,
});
await signUp.prepareEmailAddressVerification({
strategy: "email_code",
});
setPendingVerification(true);
} catch (err: any) {
console.error(JSON.stringify(err, null, 2));
if (err.errors && err.errors[0]?.message) {
setErrorMessage(err.errors[0].message);
} else {
setErrorMessage(
"An unexpected error occurred. Please try again.",
);
}
}
};
const onPressVerify = async () => {
if (!isLoaded) return;
setErrorMessage("");
try {
const completeSignUp = await signUp.attemptEmailAddressVerification(
{
code,
},
);
if (completeSignUp.status === "complete") {
await setActive({ session: completeSignUp.createdSessionId });
} else {
console.error(JSON.stringify(completeSignUp, null, 2));
setErrorMessage("Failed to verify. Please check your code.");
}
} catch (err: any) {
console.error(JSON.stringify(err, null, 2));
setErrorMessage("Invalid verification code. Please try again.");
}
};
return (
<View className="flex-1 justify-center items-center bg-gray-50 p-6">
<View className="bg-white w-11/12 max-w-md p-8 rounded-lg shadow-lg">
<Text className="text-3xl font-bold text-center text-gray-800 mb-6">
{pendingVerification ? "Verify Email" : "Sign Up"}
</Text>
{errorMessage ? (
<Text className="text-red-500 text-center mb-4">
{errorMessage}
</Text>
) : null}
{!pendingVerification && (
<>
<TextInput
autoCapitalize="none"
value={emailAddress}
placeholder="Email..."
onChangeText={(email) => setEmailAddress(email)}
className="w-full h-12 mb-4 px-4 bg-gray-100 border border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none"
/>
<TextInput
value={password}
placeholder="Password..."
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
className="w-full h-12 mb-6 px-4 bg-gray-100 border border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none"
/>
<TouchableOpacity
onPress={onSignUpPress}
className="w-full h-12 bg-blue-600 rounded-lg flex justify-center items-center mb-4"
>
<Text className="text-white text-lg font-semibold">
Sign Up
</Text>
</TouchableOpacity>
</>
)}
{pendingVerification && (
<>
<TextInput
value={code}
placeholder="Verification Code..."
onChangeText={(code) => setCode(code)}
className="w-full h-12 mb-4 px-4 bg-gray-100 border border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none"
/>
<TouchableOpacity
onPress={onPressVerify}
className="w-full h-12 bg-green-600 rounded-lg flex justify-center items-center mb-4"
>
<Text className="text-white text-lg font-semibold">
Verify Email
</Text>
</TouchableOpacity>
</>
)}
</View>
</View>
);
}

View File

@@ -0,0 +1,44 @@
import { ScrollViewStyleReset } from "expo-router/html";
import { type PropsWithChildren } from "react";
/**
* This file is web-only and used to configure the root HTML for every web page during static rendering.
* The contents of this function only run in Node.js environments and do not have access to the DOM or browser APIs.
*/
export default function Root({ children }: PropsWithChildren) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style
dangerouslySetInnerHTML={{ __html: responsiveBackground }}
/>
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}
const responsiveBackground = `
body {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}`;

View File

@@ -0,0 +1,29 @@
import { Link, Stack } from "expo-router";
import { StyleSheet, View, Text } from "react-native";
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: "Oops!" }} />
<View style={styles.container}>
<Text>This screen doesn't exist.</Text>
<Link href="/" style={styles.link}>
<Text>Go to home screen!</Text>
</Link>
</View>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
},
link: {
marginTop: 15,
paddingVertical: 15,
},
});

View File

@@ -0,0 +1,43 @@
import { useFonts } from "expo-font";
import { ClerkLoaded, ClerkProvider } from "@clerk/clerk-expo";
import * as SplashScreen from "expo-splash-screen";
import React, { useEffect } from "react";
import { Slot } from "expo-router";
import { tokenCache } from "../cache";
import { JazzAndAuth } from "../src/auth-context";
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
});
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);
if (!loaded) {
return null;
}
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
if (!publishableKey) {
throw new Error(
"Missing Publishable Key. Please set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env",
);
}
return (
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
<ClerkLoaded>
<JazzAndAuth>
<Slot />
</JazzAndAuth>
</ClerkLoaded>
</ClerkProvider>
);
}

View File

@@ -0,0 +1,173 @@
import React, {
SafeAreaView,
View,
Text,
Alert,
TouchableOpacity,
FlatList,
KeyboardAvoidingView,
TextInput,
Button,
} from "react-native";
import { useLocalSearchParams } from "expo-router";
import { useState, useEffect, useLayoutEffect } from "react";
import { useAccount, useCoState } from "@/src/jazz";
import { Chat, Message } from "@/src/schema";
import { Group, ID } from "jazz-tools";
import clsx from "clsx";
import { useNavigation, useFocusEffect } from "@react-navigation/native";
import * as Clipboard from "expo-clipboard";
export default function Conversation() {
const { chatId } = useLocalSearchParams();
const { me } = useAccount();
const [chat, setChat] = useState<Chat>();
const [message, setMessage] = useState("");
const loadedChat = useCoState(Chat, chat?.id, [{}]);
const navigation = useNavigation();
useEffect(() => {
if (chat) return;
if (chatId === "new") {
createChat();
} else {
loadChat(chatId as ID<Chat>);
}
}, [chat]);
// Effect to dynamically set header options
useLayoutEffect(() => {
navigation.setOptions({
headerTitle: "Chat",
headerRight: () =>
chat ? (
<Button
onPress={() => {
if (chat?.id) {
Clipboard.setStringAsync(
`https://chat.jazz.tools/#/chat/${chat.id}`,
);
Alert.alert(
"Copied to clipboard",
`Chat ID: ${chat.id}`,
);
}
}}
title="Share"
/>
) : null,
});
}, [navigation, chat]);
const createChat = () => {
const group = Group.create({ owner: me });
group.addMember("everyone", "writer");
const chat = Chat.create([], { owner: group });
setChat(chat);
};
const loadChat = async (chatId: ID<Chat>) => {
try {
const chat = await Chat.load(chatId, me, []);
setChat(chat);
} catch (error) {
console.log("Error loading chat", error);
Alert.alert("Error", `Error loading chat: ${error}`);
}
};
const sendMessage = () => {
if (!chat) return;
if (message.trim()) {
chat.push(
Message.create({ text: message }, { owner: chat._owner }),
);
setMessage("");
}
};
const renderMessageItem = ({ item }: { item: Message }) => {
const isMe = item._edits.text.by?.isMe;
return (
<View
className={clsx(
`rounded-xl px-3 py-2 max-w-[75%] my-1`,
isMe ? `bg-blue-500 self-end` : `bg-gray-200 self-start`,
)}
>
{!isMe ? (
<Text
className={clsx(
`text-xs text-gray-500 mb-1`,
isMe ? "text-right" : "text-left",
)}
>
{item._edits.text.by?.profile?.name}
</Text>
) : null}
<View
className={clsx(
"flex relative items-end justify-between",
isMe ? "flex-row" : "flex-row",
)}
>
<Text
className={clsx(
!isMe ? "text-black" : "text-gray-200",
`text-md max-w-[85%]`,
)}
>
{item.text}
</Text>
<Text
className={clsx(
"text-[10px] text-right ml-2",
!isMe ? "mt-2 text-gray-500" : "mt-1 text-gray-200",
)}
>
{item._edits.text.madeAt.getHours()}:
{item._edits.text.madeAt.getMinutes()}
</Text>
</View>
</View>
);
};
return (
<View className="flex-1 bg-gray-50">
<FlatList
contentContainerStyle={{
flexGrow: 1,
paddingVertical: 10,
paddingHorizontal: 8,
}}
className="flex"
data={loadedChat}
keyExtractor={(item) => item.id}
renderItem={renderMessageItem}
/>
<KeyboardAvoidingView
keyboardVerticalOffset={110}
behavior="padding"
className="p-3 bg-white border-t border-gray-300"
>
<SafeAreaView className="flex-row items-center gap-2">
<TextInput
className="flex-1 rounded-full h-10 px-4 bg-gray-100 border border-gray-300 focus:border-blue-500 focus:bg-white"
value={message}
onChangeText={setMessage}
placeholder="Type a message..."
textAlignVertical="center"
onSubmitEditing={sendMessage}
/>
<TouchableOpacity
onPress={sendMessage}
className="bg-blue-500 rounded-full h-10 w-10 items-center justify-center"
>
<Text className="text-white text-xl"></Text>
</TouchableOpacity>
</SafeAreaView>
</KeyboardAvoidingView>
</View>
);
}

View File

@@ -0,0 +1,14 @@
import React from "react";
import { Stack } from "expo-router";
export default function ChatLayout() {
return (
<Stack
screenOptions={{
headerShown: true,
headerBackVisible: true,
headerTitle: "",
}}
/>
);
}

View File

@@ -0,0 +1,85 @@
import { useLayoutEffect } from "react";
import React, {
Button,
Text,
TouchableOpacity,
View,
Alert,
} from "react-native";
import { ID } from "jazz-tools";
import { useRouter } from "expo-router";
import { useNavigation } from "@react-navigation/native";
import { Chat } from "../../src/schema";
import { useAccount } from "../../src/jazz";
import { useUser } from "@clerk/clerk-expo";
export default function ChatScreen() {
const { logOut } = useAccount();
const router = useRouter();
const navigation = useNavigation();
const { user } = useUser();
useLayoutEffect(() => {
navigation.setOptions({
headerTitle: "Chat",
headerRight: () => <Button onPress={logOut} title="Logout" />,
});
}, [navigation]);
const loadChat = async (chatId: ID<Chat> | "new") => {
router.navigate(`/chat/${chatId}`);
};
const joinChat = () => {
Alert.prompt(
"Join Chat",
"Enter the Chat ID (example: co_zBGEHYvRfGuT2YSBraY3njGjnde)",
[
{
text: "Cancel",
style: "cancel",
},
{
text: "Join",
onPress: (chatId) => {
if (chatId) {
loadChat(chatId as ID<Chat>);
} else {
Alert.alert("Error", "Chat ID cannot be empty.");
}
},
},
],
"plain-text",
);
};
return (
<View className="flex-1 bg-gray-50">
<View className="flex-1 justify-center items-center px-6">
<View className="w-full max-w-sm bg-white p-8 rounded-lg shadow-lg">
<Text className="text-xl font-semibold text-gray-800">
Welcome, {user?.emailAddresses[0].emailAddress}
</Text>
<TouchableOpacity
onPress={() => loadChat("new")}
className="w-full bg-blue-600 py-4 rounded-md mb-4 mt-4"
>
<Text className="text-white text-lg font-semibold text-center">
Start New Chat
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={joinChat}
className="w-full bg-green-500 py-4 rounded-md"
>
<Text className="text-white text-lg font-semibold text-center">
Join Chat
</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,10 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: [
"nativewind/babel",
"@babel/plugin-transform-class-static-block",
],
};
};

View File

@@ -0,0 +1,36 @@
import * as SecureStore from "expo-secure-store";
import { Platform } from "react-native";
export interface TokenCache {
getToken: (key: string) => Promise<string | undefined | null>;
saveToken: (key: string, token: string) => Promise<void>;
clearToken?: (key: string) => void;
}
const createTokenCache = (): TokenCache => {
return {
getToken: async (key: string) => {
try {
const item = await SecureStore.getItemAsync(key);
if (item) {
console.log(`${key} was used 🔐 \n`);
} else {
console.log("No values stored under key: " + key);
}
return item;
} catch (error) {
console.error("secure store get item error: ", error);
await SecureStore.deleteItemAsync(key);
return null;
}
},
saveToken: (key: string, token: string) => {
return SecureStore.setItemAsync(key, token);
},
};
};
// SecureStore is not supported on the web
// https://github.com/expo/expo/issues/7744#issuecomment-611093485
export const tokenCache =
Platform.OS !== "web" ? createTokenCache() : undefined;

View File

@@ -0,0 +1,27 @@
{
"cli": {
"version": ">= 12.5.1",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"ios-simulator": {
"extends": "development",
"ios": {
"simulator": true
}
},
"preview": {
"distribution": "internal"
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}

View File

@@ -0,0 +1,2 @@
import "./polyfills";
import "expo-router/entry";

View File

@@ -0,0 +1,31 @@
// Learn more https://docs.expo.dev/guides/monorepos
const { getDefaultConfig } = require("expo/metro-config");
const { FileStore } = require("metro-cache");
const path = require("path");
// eslint-disable-next-line no-undef
const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, "../..");
const config = getDefaultConfig(projectRoot);
// Since we are using pnpm, we have to setup the monorepo manually for Metro
// #1 - Watch all files in the monorepo
config.watchFolders = [workspaceRoot];
// #2 - Try resolving with project modules first, then workspace modules
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, "node_modules"),
path.resolve(workspaceRoot, "node_modules"),
];
config.resolver.sourceExts = ["mjs", "js", "json", "ts", "tsx"];
config.resolver.unstable_enablePackageExports = true;
config.resolver.requireCycleIgnorePatterns = [/(^|\/|\\)node_modules($|\/|\\)/];
// Use turborepo to restore the cache when possible
config.cacheStores = [
new FileStore({
root: path.join(projectRoot, "node_modules", ".cache", "metro"),
}),
];
module.exports = config;

View File

@@ -0,0 +1 @@
/// <reference types="nativewind/types" />

View File

@@ -0,0 +1,75 @@
{
"name": "chat-rn-clerk",
"main": "index.js",
"version": "1.0.0",
"scripts": {
"build": "expo export -p ios",
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"test": "jest --watchAll",
"lint": "expo lint"
},
"jest": {
"preset": "jest-expo"
},
"dependencies": {
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@clerk/clerk-expo": "^2.2.21",
"@expo/vector-icons": "^14.0.2",
"@react-native-community/netinfo": "^11.3.1",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0",
"base-64": "^1.0.0",
"clsx": "^2.0.0",
"expo": "~51.0.37",
"expo-build-properties": "~0.12.5",
"expo-clipboard": "~6.0.3",
"expo-constants": "~16.0.2",
"expo-crypto": "~13.0.2",
"expo-dev-client": "~4.0.28",
"expo-font": "~12.0.4",
"expo-linking": "~6.3.1",
"expo-router": "~3.5.23",
"expo-secure-store": "~13.0.2",
"expo-splash-screen": "~0.27.5",
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.7",
"expo-web-browser": "~13.0.3",
"jazz-react-auth-clerk": "workspace:*",
"jazz-react-native": "workspace:*",
"jazz-tools": "workspace:*",
"nativewind": "^2.0.11",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-native": "~0.74.5",
"react-native-fetch-api": "^3.0.0",
"react-native-gesture-handler": "~2.16.1",
"react-native-get-random-values": "^1.11.0",
"react-native-mmkv": "3.0.1",
"react-native-polyfill-globals": "^3.1.0",
"react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "4.10.5",
"react-native-screens": "3.31.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-web": "~0.19.10",
"text-encoding": "^0.7.0",
"web-streams-polyfill": "^3.2.1"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/plugin-transform-class-static-block": "^7.24.7",
"@types/jest": "^29.5.3",
"@types/react": "^18.2.19",
"@types/react-test-renderer": "^18.0.7",
"eslint": "^8.46.0",
"eslint-config-expo": "^7.1.2",
"jest": "^29.2.1",
"jest-expo": "~51.0.3",
"react-test-renderer": "18.2.0",
"tailwindcss": "3.3.2",
"typescript": "^5.3.3"
},
"private": true
}

View File

@@ -0,0 +1,6 @@
import "react-native-polyfill-globals/auto";
import "@azure/core-asynciterator-polyfill";
import { ReadableStream } from "web-streams-polyfill/ponyfill/es6";
import { polyfillGlobal } from "react-native/Libraries/Utilities/PolyfillFunctions";
polyfillGlobal("ReadableStream", () => ReadableStream);

View File

@@ -0,0 +1,62 @@
import React, {
useContext,
createContext,
useEffect,
useState,
PropsWithChildren,
} from "react";
import { useClerk, useUser } from "@clerk/clerk-expo";
import { useJazzClerkAuth } from "jazz-react-auth-clerk";
import { Jazz } from "./jazz";
import { Text, View } from "react-native";
const AuthContext = createContext<{
isAuthenticated: boolean;
isLoading: boolean;
}>({
isAuthenticated: false,
isLoading: true,
});
export function useAuth() {
return useContext(AuthContext);
}
export function JazzAndAuth({ children }: PropsWithChildren) {
const { isSignedIn, isLoaded: isClerkLoaded } = useUser();
const clerk = useClerk();
const [auth, state] = useJazzClerkAuth(clerk);
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
if (isSignedIn && isClerkLoaded && auth) {
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
}, [isSignedIn, isClerkLoaded, auth]);
return (
<AuthContext.Provider
value={{ isAuthenticated, isLoading: !isClerkLoaded || !auth }}
>
{state?.errors?.length > 0 &&
state.errors.map((error) => (
<View key={error}>
<Text style={{ color: "red" }}>{error}</Text>
</View>
))}
{auth ? (
<Jazz.Provider
auth={auth}
peer="wss://mesh.jazz.tools/?key=chat-rn-clerk-example-jazz@gcmp.io"
storage={undefined}
>
{children}
</Jazz.Provider>
) : (
children
)}
</AuthContext.Provider>
);
}

View File

@@ -0,0 +1,7 @@
import { createJazzRNApp } from "jazz-react-native";
import { MMKVStorage } from "./mmkv-storage";
const nativeStorage = new MMKVStorage();
export const Jazz = createJazzRNApp({ nativeStorage });
export const { useAccount, useCoState, useAcceptInvite } = Jazz;

View File

@@ -0,0 +1,25 @@
import { MMKV } from "react-native-mmkv";
import type { NativeStorage } from "jazz-react-native";
const storage = new MMKV();
export class MMKVStorage implements NativeStorage {
get(key: string): Promise<string | undefined> {
return Promise.resolve(storage.getString(key));
}
set(key: string, value: string): Promise<void> {
storage.set(key, value);
return Promise.resolve();
}
delete(key: string): Promise<void> {
storage.delete(key);
return Promise.resolve();
}
clearAll(): Promise<void> {
storage.clearAll();
return Promise.resolve();
}
}

View File

@@ -0,0 +1,7 @@
import { CoMap, CoList, co } from "jazz-tools";
export class Message extends CoMap {
text = co.string;
}
export class Chat extends CoList.Of(co.ref(Message)) {}

View File

@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}",
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -0,0 +1,16 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"moduleResolution": "bundler",
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"**/*.ts",
"**/*.tsx"
]
}

1
examples/chat-rn/.env Normal file
View File

@@ -0,0 +1 @@
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_ZXZpZGVudC1kYW5lLTg5LmNsZXJrLmFjY291bnRzLmRldiQ

View File

@@ -1,46 +1,46 @@
{
"expo": {
"name": "jazz-chat-rn",
"scheme": "jazz-chat-rn",
"slug": "jazz-chat-rn",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.jazz.chatrn"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.jazz.chatrn"
},
"plugins": [
[
"expo-build-properties",
{
"ios": {
"newArchEnabled": true
},
"android": {
"newArchEnabled": true
}
}
]
],
"extra": {
"eas": {
"projectId": "e0e61872-1906-4c84-b9d8-9be77355cad0"
}
},
"owner": "paxx"
}
"expo": {
"name": "jazz-chat-rn",
"scheme": "jazz-chat-rn",
"slug": "jazz-chat-rn",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.jazz.chatrn"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.jazz.chatrn"
},
"plugins": [
[
"expo-build-properties",
{
"ios": {
"newArchEnabled": true
},
"android": {
"newArchEnabled": true
}
}
]
],
"extra": {
"eas": {
"projectId": "e0e61872-1906-4c84-b9d8-9be77355cad0"
}
},
"owner": "paxx"
}
}

View File

@@ -1,5 +1,4 @@
import "./polyfills";
import { registerRootComponent } from "expo";
import App from "./src/App";
registerRootComponent(App);

View File

@@ -3,6 +3,7 @@ const { getDefaultConfig } = require("expo/metro-config");
const { FileStore } = require("metro-cache");
const path = require("path");
// eslint-disable-next-line no-undef
const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, "../..");
@@ -16,8 +17,9 @@ config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, "node_modules"),
path.resolve(workspaceRoot, "node_modules"),
];
config.resolver.sourceExts = ["js", "json", "ts", "tsx"];
config.resolver.sourceExts = ["mjs", "js", "json", "ts", "tsx"];
config.resolver.unstable_enablePackageExports = true;
config.resolver.requireCycleIgnorePatterns = [/(^|\/|\\)node_modules($|\/|\\)/];
// Use turborepo to restore the cache when possible
config.cacheStores = [

View File

@@ -3,6 +3,7 @@
"version": "1.0.2",
"main": "index.js",
"scripts": {
"build": "expo export -p ios",
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
@@ -15,14 +16,15 @@
"@react-navigation/native-stack": "^6.11.0",
"base-64": "^1.0.0",
"clsx": "^2.0.0",
"expo": "~51.0.36",
"expo": "~51.0.37",
"expo-build-properties": "~0.12.5",
"expo-clipboard": "~6.0.3",
"expo-constants": "~16.0.2",
"expo-crypto": "~13.0.2",
"expo-dev-client": "~4.0.27",
"expo-dev-client": "~4.0.28",
"expo-linking": "~6.3.1",
"expo-status-bar": "~1.12.1",
"expo-web-browser": "~13.0.3",
"jazz-react-native": "workspace:*",
"jazz-tools": "workspace:*",
"nativewind": "^2.0.11",

View File

@@ -59,6 +59,7 @@ function App() {
<Stack.Screen
options={{ title: "Jazz Chat" }}
name="ChatScreen"
// @ts-ignore
component={ChatScreen}
/>
<Stack.Screen

View File

@@ -22,7 +22,6 @@ export default function ChatScreen({ navigation }: { navigation: any }) {
const [chat, setChat] = useState<Chat>();
const [message, setMessage] = useState("");
const loadedChat = useCoState(Chat, chat?.id, [{}]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
navigation.setOptions({

View File

@@ -1,6 +1,5 @@
import { createJazzRNApp } from "jazz-react-native";
import { MMKVStorage } from "./mmkv-storage";
import { Account } from "jazz-tools";
const nativeStorage = new MMKVStorage();

View File

@@ -53,4 +53,5 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
)
Button.displayName = "Button"
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants }

View File

@@ -16,13 +16,15 @@ export function HeroHeader({
title,
slogan,
pt = true,
className = ''
}: {
title: ReactNode;
slogan: ReactNode;
pt?: boolean;
className?: string;
}) {
return (
<hgroup className={clsx(pt && "pt-12 md:pt-20", "mb-10")}>
<hgroup className={clsx(pt && "pt-12 md:pt-20", "mb-10", className)}>
<H1>{title}</H1>
<H1Sub>{slogan}</H1Sub>
</hgroup>

View File

@@ -22,7 +22,7 @@ type FooterProps = {
export function Footer({ logo, companyName, sections }: FooterProps) {
return (
<footer className="flex z-10 mt-10 min-h-[15rem] bg-stone-100 dark:bg-stone-900 text-stone-600 dark:text-stone-400 w-full justify-center">
<footer className="flex z-10 mt-10 min-h-[15rem] border-t bg-stone-100 dark:bg-stone-925 text-stone-600 dark:text-stone-400 w-full justify-center dark:border-stone-900">
<div className="p-8 container w-full grid grid-cols-3 md:grid-cols-4 lg:grid-cols-7 gap-8 max-sm:mb-12">
<div className="col-span-full md:col-span-1 sm:row-start-4 md:row-start-auto lg:col-span-2 md:row-span-2 md:flex-1 flex flex-row md:flex-col max-sm:mt-4 justify-between max-sm:items-start gap-2 text-sm min-w-[10rem]">
{logo}

View File

@@ -38,7 +38,7 @@ export function Nav({
className={[
clsx(
"hidden md:flex sticky left-0 right-0 top-0 max-sm:bottom-0 w-full justify-center",
"bg-white dark:bg-stone-950 border-b max-sm:border-t border-stone-50 dark:border-b-stone-950",
"bg-white dark:bg-stone-950 border-b max-sm:border-t border-stone-200 dark:border-stone-900",
"max-h-none overflow-hidden transition[max-height] duration-300 ease-in-out",
"z-50",
menuOpen ? "h-[100dvh]" : "h-16"

View File

@@ -100,7 +100,7 @@ export default function Home() {
</p>
</div>
<div className="bg-stone-100 dark:bg-stone-925 py-8 lg:py-16">
<div className="bg-stone-100 border-y dark:bg-stone-925 py-8 lg:py-16 dark:border-stone-900">
<div className="container grid gap-8 lg:gap-12">
<h2 className="font-display md:text-center text-stone-950 dark:text-white text-2xl md:text-3xl font-semibold tracking-tight">
Hard things are easy now.
@@ -568,20 +568,38 @@ export default function Home() {
</GridCard>
</GappedGrid>
<div>
<div className="flex flex-col justify-between gap-3">
<h3 className="font-display text-stone-950 dark:text-white text-xl font-semibold tracking-tight lg:text-2xl">
Get started
<div className="border border-stone-200 dark:border-stone-900 rounded-xl shadow-sm p-4 md:py-16">
<div className="flex flex-col lg:max-w-3xl md:text-center mx-auto justify-between gap-6">
<p className="uppercase text-blue tracking-wide text-sm font-medium dark:text-stone-400">
Become an early adopter
</p>
<h3 className="font-display md:text-center text-stone-950 dark:text-white text-2xl md:text-3xl font-semibold tracking-tight">
We&apos;ll help you build your next app with Jazz
</h3>
<div className="flex gap-3">
<Button href="/docs" variant="primary">
Read documentation
</Button>
<div className="space-y-2 md:text-balance leading-relaxed">
<p>
It&apos;s early days, but we work hard every day
to make Jazz a great tool for our users.
</p>
<p>
We want to hear about what you&apos;re building,
so we can help you every step of the way.
We&apos;ll prioritize features that you need to
succeed.
</p>
</div>
<div className="flex md:justify-center gap-3">
<Button
href="https://discord.gg/utDMjHYg42"
variant="secondary"
variant="primary"
>
Join Discord
Let&apos;s talk on Discord
</Button>
<Button href="/docs" variant="secondary">
Read <span className="sm:hidden">docs</span>{" "}
<span className="hidden sm:inline">
documentation
</span>
</Button>
</div>
</div>

View File

@@ -1,11 +1,5 @@
#### Completely DIY Mesh.
<p className="no-prose text-base">
Build your own network of sync and storage nodes. Handle
networking, security and backups yourself.
</p>
<div className="text-sm">
Costs:
- N × instance cost for your sync nodes.
- Very high self-hosted egress costs.
- High self-hosted storage costs.
</div>
devops,networking, security and backups yourself.
</p>

View File

@@ -2,11 +2,5 @@
<p className="no-prose text-base">
Connect your users to Jazz Mesh for all its benefits,
but also run and connect your own data backup node (just
in case.)
</p>
<div className="text-sm">
Extra costs:
- Instance costs for the backup node.
- Moderate self-hosted storage costs.
- Your backup node is counted as a continously connected device.
</div>
in case).
</p>

View File

@@ -3,11 +3,4 @@
Connect your users to Jazz Mesh, or your own nodes as a
lower-performance fallback. The two networks stay in
constant sync.
</p>
<div className="text-sm">
Extra costs:
- N × instance cost for your sync nodes.
- Typically moderate self-hosted egress costs.
- High self-hosted storage costs.
- Any of your sync nodes connected to Jazz Mesh is counted as a continously connected device.
</div>
</p>

View File

@@ -4,18 +4,19 @@ import { GappedGrid } from "gcmp-design-system/src/app/components/molecules/Gapp
import { GridCard } from "gcmp-design-system/src/app/components/atoms/GridCard";
import { ComingSoonBadge } from "gcmp-design-system/src/app/components/atoms/ComingSoonBadge";
import { P } from "gcmp-design-system/src/app/components/atoms/Paragraph";
import { H3, H4 } from "gcmp-design-system/src/app/components/atoms/Headings";
import {
H2,
H3,
H4,
} from "gcmp-design-system/src/app/components/atoms/Headings";
import { LI } from "gcmp-design-system/src/app/components/atoms/ListItem";
import { UL } from "gcmp-design-system/src/app/components/molecules/List";
import {
Prose,
SmallProse,
} from "gcmp-design-system/src/app/components/molecules/Prose";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import MeshPlusBackup from "./meshPlusBackup.mdx";
import MeshPlusDIY from "./meshPlusDIY.mdx";
import CompletelyDIY from "./completelyDIY.mdx";
import PricingFAQ from "./pricingFaq.mdx";
import { Button } from "@/components/Button";
import { Pricing } from "@/components/Pricing";
export const metadata = {
title: "jazz - Jazz Mesh",
@@ -24,16 +25,14 @@ export const metadata = {
export default function Mesh() {
return (
<div className="container space-y-16">
<div className="space-y-16">
<HeroHeader
className="container"
title="Jazz Mesh"
slogan="Real-time sync and storage infrastructure that scales up to millions of users."
/>
<div>
<P>
Pricing that scales down to zero.
</P>
<div className="container">
<P>Pricing that scales down to zero.</P>
<GappedGrid>
<GridCard>
<H3>Optimal mesh routing.</H3>
@@ -62,166 +61,194 @@ export default function Mesh() {
</GappedGrid>
</div>
<div>
<SectionHeader title="Pricing" slogan="" />
<GappedGrid>
<GridCard>
<H3>
Mesh Free{" "}
<div className="text-2xl float-right">$0</div>
</H3>
<div className="bg-stone-100 border-y dark:bg-stone-925 py-8 lg:py-16 dark:border-y-0 dark:bg-transparent dark:py-0">
<div className="container">
<H2 className="mb-5 sm:text-center md:text-left">
Pricing
</H2>
<Pricing />
</div>
</div>
<div className="container space-y-16">
<div>
<SectionHeader title="Pricing" slogan="" />
<GappedGrid>
<GridCard>
<H3>
Mesh Free{" "}
<div className="text-2xl float-right">$0</div>
</H3>
<UL>
<LI>Best-effort sync</LI>
<LI>3,000 sync-minutes/mo</LI>
<LI>1 GB storage</LI>
</UL>
</GridCard>
<GridCard>
<H3>
Mesh Starter <ComingSoonBadge />
<div className="float-right">
<span className="text-2xl">$9</span>/mo
</div>
</H3>
<UL>
<LI>Base-priority sync</LI>
<LI>6,000 sync-minutes/mo</LI>
<LI>100 GB storage</LI>
</UL>
<div className="text-xs">
<P>Extra usage:</P>
<LI>$9 per additional 6,000 sync-minutes</LI>
<LI>$9 per additional 1TB storage/mo</LI>
</div>
</GridCard>
<GridCard>
<H3>
Mesh Pro <ComingSoonBadge />
<div className="float-right">
<span className="text-2xl">$79</span>/mo
</div>
</H3>
<UL>
<LI>High-priority sync</LI>
<LI>30,000 sync-minutes/mo</LI>
<LI>1 TB storage</LI>
<LI>Offer sync.yourdomain.com</LI>
</UL>
<div className="text-xs">
<P>Extra usage:</P>
<UL>
<LI>$15 per additional 6,000 sync-minutes</LI>
<LI>$15 per additional 1TB storage/mo</LI>
<LI>Best-effort sync</LI>
<LI>Community support</LI>
<LI>
<s>20 Monthly Active Users</s>
</LI>
<LI>
<s>1 GB storage</s>
</LI>
</UL>
<div className="border-white bg-blue-50 border-4 rounded-lg px-4 -rotate-6 shadow">
<p className="text-center my-4 font-bold">
Public Alpha
</p>
<UL>
<LI>Use your email address as API key</LI>
<LI>Currently no enforced limits</LI>
</UL>
</div>
</GridCard>
<GridCard>
<H3>
Mesh Indie <ComingSoonBadge />
<div className="float-right">
<span className="text-2xl">$19</span>/mo
</div>
</H3>
<UL>
<LI>Base-priority sync</LI>
<LI>Community support</LI>
<LI>1000 Monthly Active Usersincluded</LI>
<LI>500 GB storageincluded</LI>
</UL>
<P>Extra usage:</P>
<LI>$9 per add. 1000 Monthly Active Users</LI>
<LI>$9 per add. 500 GB storage/mo</LI>
<p className="mt-4 text-sm">
For companies with &lt;$200k in annual revenue
or institutional funding.
</p>
</GridCard>
<GridCard>
<H3>
Mesh Pro
<div className="float-right">
from <span className="text-2xl">$500</span>
/mo
</div>
</H3>
<UL>
<LI>Highest-priority sync</LI>
<LI>Dedicated integration & dev support</LI>
<LI>Unlimited Monthly Active Users</LI>
<LI>Unlimited storage</LI>
<LI>SLAs, custom deployment, etc.</LI>
<LI>
Offer <code>sync.yourdomain.com</code>
</LI>
</UL>
<Button
href="https://cal.com/anselm-io/mesh-pro-intro"
size="lg"
className="block text-center"
>
Book intro call
</Button>
<p className="mt-4 text-sm">
Our team of devs & product experts will
get you going for free. Then we&apos;ll make a
deal just for you.
</p>
</GridCard>
</GappedGrid>
</div>
<div>
<SectionHeader
title="Global Footprint"
slogan="We're rapidly expanding our network of sync & storage nodes. This is our current best-effort coverage."
/>
<GappedGrid>
<div className="text-sm">
<H4>Under 50ms RTT</H4>
<UL>
<LI>Frankfurt</LI>
<LI>New York</LI>
<LI>Newark</LI>
<LI>North California</LI>
<LI>North Virginia</LI>
<LI>San Francisco</LI>
<LI>Singapore</LI>
<LI>Toronto</LI>
</UL>
</div>
</GridCard>
</GappedGrid>
<H3>FAQ</H3>
<SmallProse>
<PricingFAQ />
</SmallProse>
</div>
<div>
<SectionHeader
title="Global Footprint"
slogan="We're rapidly expanding our network of sync & storage nodes. This is our current best-effort coverage."
/>
<GappedGrid>
<div className="text-sm">
<H4>Under 50ms RTT</H4>
<UL>
<LI>Frankfurt</LI>
<LI>New York</LI>
<LI>Newark</LI>
<LI>North California</LI>
<LI>North Virginia</LI>
<LI>San Francisco</LI>
<LI>Singapore</LI>
<LI>Toronto</LI>
</UL>
</div>
<div className="text-sm">
<H4>Under 100ms RTT</H4>
<UL>
<LI>Amsterdam</LI>
<LI>Atlanta</LI>
<LI>London</LI>
<LI>Ohio</LI>
<LI>Paris</LI>
</UL>
</div>
<div className="text-sm">
<H4>Under 100ms RTT</H4>
<UL>
<LI>Amsterdam</LI>
<LI>Atlanta</LI>
<LI>London</LI>
<LI>Ohio</LI>
<LI>Paris</LI>
</UL>
</div>
<div className="text-sm">
<H4>Under 200ms RTT</H4>
<UL>
<LI>Bangalore</LI>
<LI>Dallas</LI>
<LI>Mumbai</LI>
<LI>Oregon</LI>
</UL>
<div className="text-sm">
<H4>Under 200ms RTT</H4>
<UL>
<LI>Bangalore</LI>
<LI>Dallas</LI>
<LI>Mumbai</LI>
<LI>Oregon</LI>
</UL>
<H4>Under 300ms RTT</H4>
<UL>
<LI> Seoul</LI>
<LI> Tokyo</LI>
</UL>
</div>
<H4>Under 300ms RTT</H4>
<UL>
<LI> Seoul</LI>
<LI> Tokyo</LI>
</UL>
</div>
<div className="text-sm">
<H4>Under 400ms RTT</H4>
<UL>
<LI>Sao Paulo</LI>
<LI>Sydney</LI>
</UL>
<div className="text-sm">
<H4>Under 400ms RTT</H4>
<UL>
<LI>Sao Paulo</LI>
<LI>Sydney</LI>
</UL>
<H4>Under 500ms RTT</H4>
<H4>Under 500ms RTT</H4>
<UL>
<LI>Cape Town</LI>
</UL>
</div>
</GappedGrid>
<UL>
<LI>Cape Town</LI>
</UL>
</div>
</GappedGrid>
<H3>Enterprise</H3>
<P>
Custom deployment in the cloud, your private cloud,
on-premises or hybrids?
<br />
SLAs and dedicated support? White-glove integration
services? Let&apos;s talk:{" "}
<a href="mailto:hello@gcmp.io">hello@gcmp.io</a>
</P>
</div>
</div>
<div>
<SectionHeader
title="Custom Deployment Scenarios"
slogan="You can rely on Jazz Mesh. But you don't have to."
/>
<P>
Because Jazz is open-source, you can optionally run your own
sync nodes &mdash; in a variety of setups.
</P>
<GappedGrid>
<GridCard>
<Prose>
<MeshPlusBackup />
</Prose>
</GridCard>
<GridCard>
<Prose>
<MeshPlusDIY />
</Prose>
</GridCard>
<GridCard>
<Prose>
<CompletelyDIY />
</Prose>
</GridCard>
</GappedGrid>
<div>
<SectionHeader
title="Custom Deployment Scenarios"
slogan="You can rely on Jazz Mesh. But you don't have to."
/>
<P>
Because Jazz is open-source, you can optionally run your
own sync nodes &mdash; in a variety of setups.
</P>
<GappedGrid>
<GridCard>
<Prose>
<MeshPlusBackup />
</Prose>
</GridCard>
<GridCard>
<Prose>
<MeshPlusDIY />
</Prose>
</GridCard>
<GridCard>
<Prose>
<CompletelyDIY />
</Prose>
</GridCard>
</GappedGrid>
</div>
</div>
</div>
);

View File

@@ -1,17 +0,0 @@
#### How are sync-minutes counted?
Sync-minutes are counted on a **per-connected-device, per-minute basis.**<br/>
A device is considered syncing **only when it's actively sending or receiving data.**
#### How can I estimate my usage?
The best way to estimate your usage is to **guess how many minutes per month each user will spend actively using your app.**
Storage is mostly determined by large binary blobs (like images or videos) that you store in Jazz.
#### What happens if I exceed my plan's limits?
All limits are initially soft limits, so don't worry if you suddenly get lots of users or traffic!
Sync beyond the limit is still served, but at a lower priority.
Data beyond the storage limit is still stored and backed up, but may be significantly slower to access.
If you exceed your plan's limits consistently, we'll reach out to discuss upgrading your plan.

View File

@@ -0,0 +1,44 @@
import { HeroHeader } from "gcmp-design-system/src/app/components/molecules/HeroHeader";
import Image from "next/image";
import { products } from "@/lib/showcase";
import Link from "next/link";
export const metadata = {
title: "jazz - Built with Jazz",
description: "Great apps by smart people.",
};
export default function Page() {
return (
<div className="container flex flex-col gap-6 pb-10 lg:pb-20">
<HeroHeader
title="Built with Jazz"
slogan="Great apps by smart people."
/>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-8">
{products.map((product) => (
<Link
href={product.url}
key={product.url}
className="group border bg-stone-50 shadow-sm p-3 flex flex-col gap-3 rounded-lg md:p-4 md:gap-4 dark:border-stone-900 dark:bg-stone-900"
>
<Image
className="rounded-md border dark:border-0"
src={product.imageUrl}
width="900"
height="675"
alt=""
/>
<div className="space-y-2">
<h2 className="font-medium text-stone-900 dark:text-white leading-none">
{product.name}
</h2>
<p className="text-sm">{product.description}</p>
</div>
</Link>
))}
</div>
</div>
);
}

View File

@@ -24,14 +24,15 @@ export function Button(props: ButtonProps) {
};
const variantClasses = {
primary: "bg-blue text-white font-medium bg-blue hover:bg-blue-800",
primary:
"bg-blue border-blue text-white font-medium bg-blue hover:bg-blue-800 hover:border-blue-800",
secondary:
"text-stone-900 font-medium bg-stone-100 hover:bg-stone-200 dark:bg-stone-900 dark:text-white",
"text-stone-900 border border-stone-200 font-medium hover:border-stone-300 dark:border-stone-900 dark:hover:border-stone-800 dark:text-white",
};
const classNames = clsx(
className,
"rounded-md transition-colors",
"rounded-lg text-center transition-colors",
sizeClasses[size],
variantClasses[variant],
);

View File

@@ -0,0 +1,145 @@
import { CircleCheckIcon } from "lucide-react";
import { Button } from "@/components/Button";
import { ComingSoonBadge } from "gcmp-design-system/src/app/components/atoms/ComingSoonBadge";
export function ListItem({
variant = "blue",
children,
}: {
variant?: "gray" | "blue";
children: React.ReactNode;
}) {
const iconSize = 16;
const iconVariants = {
gray: (
<CircleCheckIcon
size={iconSize}
className="text-stone-500 shrink-0"
/>
),
blue: (
<CircleCheckIcon
size={iconSize}
className="text-blue-500 dark:text-white shrink-0"
/>
),
};
return (
<li className="inline-flex items-center gap-2 text-stone-800 dark:text-stone-200 py-2">
{iconVariants[variant]}
{children}
</li>
);
}
export function Pricing() {
return (
<div className="flex flex-col sm:max-w-lg mx-auto md:max-w-none md:flex-row md:items-start gap-4">
<div className="md:flex-1 flex flex-col gap-3 overflow-hidden rounded-3xl p-6 shadow-lg shadow-gray-900/5 bg-white dark:bg-stone-925">
<h3 className="font-semibold text-stone-900 text-lg dark:text-white">
Starter
</h3>
<p className="text-3xl font-light text-stone-900 dark:text-white">
$0
<span className="text-sm text-stone-600 dark:text-stone-500">
/mo
</span>
</p>
<ul className="flex flex-col divide-y text-sm lg:text-base dark:divide-stone-900">
<ListItem>Best-effort sync</ListItem>
<ListItem>Community support</ListItem>
<ListItem variant="gray">
<s className="text-stone-500">
20 monthly active users
</s>
</ListItem>
<ListItem variant="gray">
<s className="text-stone-500">1 GB storage</s>
</ListItem>
</ul>
<div className="md:mt-5 space-y-3">
<p className="text-xs">
Currently no enforced limits for public alpha.
</p>
<p className="text-xs">
Use your email address as API key.
</p>
</div>
</div>
<div className="md:flex-1 flex flex-col gap-3 overflow-hidden rounded-3xl p-6 shadow-lg shadow-gray-900/5 bg-white dark:bg-stone-925">
<div>
<h3 className="font-semibold text-stone-900 text-lg inline mr-2 dark:text-white">
Indie
</h3>
<ComingSoonBadge />
</div>
<p className="text-3xl font-light text-stone-900 dark:text-white">
$19
<span className="text-sm text-stone-600 dark:text-stone-500">
/mo
</span>
</p>
<ul className="flex flex-col divide-y text-sm lg:text-base dark:divide-stone-900">
<ListItem>Base-priority sync</ListItem>
<ListItem>Community support</ListItem>
<ListItem>1000 monthly active users</ListItem>
<ListItem>500GB storage</ListItem>
</ul>
<p className="text-sm">Extra usage</p>
<ul className="flex flex-col divide-y text-sm lg:text-base dark:divide-stone-900">
<ListItem>$9 per additional 1000 MAUs</ListItem>
<ListItem>$9 per additional 500GB storage/mo</ListItem>
</ul>
<p className="text-xs">
For companies with &lt;$200k in annual revenue or
institutional funding.
</p>
</div>
<div className="md:flex-1 flex flex-col gap-3 overflow-hidden rounded-3xl p-6 shadow-lg shadow-gray-900/5 bg-white dark:bg-stone-925 pb-6">
<h3 className="font-semibold text-stone-900 text-lg dark:text-white">
Pro
</h3>
<p className="text-3xl font-light text-stone-900 dark:text-white">
<span className="text-lg font-medium">from {""}</span>
$1k
<span className="text-sm text-stone-600 dark:text-stone-500">
/mo
</span>
</p>
<ul className="flex flex-col divide-y text-sm lg:text-base dark:divide-stone-900">
<ListItem>High-priority sync</ListItem>
<ListItem>White-glove support</ListItem>
<ListItem>Unlimited monthly active users</ListItem>
<ListItem>Unlimited storage</ListItem>
<ListItem>SLAs, custom deployment, etc.</ListItem>
<ListItem>
Offer <code>sync.yourdomain.com</code>
</ListItem>
</ul>
<Button
href="https://cal.com/anselm-io/mesh-pro-intro"
size="lg"
className="md:mt-6"
>
Book intro call
</Button>
<p className="text-xs md:mt-3">
Our team of devs and product specialists will get you going
for free. Then we&apos;ll make a bespoke deal.
</p>
</div>
</div>
);
}

View File

@@ -14,6 +14,10 @@ export function JazzNav() {
title: "Docs",
href: "/docs",
},
{
title: "Built with Jazz",
href: "/showcase",
},
{
title: "Blog",
href: "https://gcmp.io/news",

View File

@@ -0,0 +1,16 @@
export const products = [
{
name: "Learn Anything",
imageUrl: "/learn-anything.png",
url: "https://learn-anything.xyz",
description:
"A free, comprehensive learning platform where people collect the best resources for every subject.",
},
{
name: "Invoice Radar",
imageUrl: "/invoice-radar.png",
url: "https://invoiceradar.com",
description:
"Automatically gather invoices from mail and cloud providers.",
},
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

View File

@@ -22,8 +22,9 @@
"dev": "turbo dev",
"build": "turbo build && cd homepage/homepage && pnpm run build",
"lint": "turbo lint && cd homepage/homepage && pnpm run lint",
"test": "vitest --coverage.enabled=false",
"test:coverage": "vitest --ui --coverage --watch",
"test": "vitest",
"test:ci": "vitest --coverage.enabled=true",
"test:coverage": "vitest --ui --coverage.enabled=true",
"format": "pnpm run -r format && cd homepage/homepage && pnpm run format",
"changeset": "changeset",
"changeset-version": "changeset version",
@@ -41,6 +42,10 @@
"expo-modules-*",
"typescript"
]
},
"overrides": {
"react": "18.3.1",
"react-dom": "18.3.1"
}
}
}

View File

@@ -0,0 +1,149 @@
import {
CoValueCore,
type CoValueUniqueness,
MAX_RECOMMENDED_TX_SIZE,
idforHeader,
} from "./coValueCore.js";
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
import { LocalNode } from "./localNode.js";
import { type RawCoValue } from "./coValue.js";
import { RawCoMap } from "./coValues/coMap.js";
import { RawCoList } from "./coValues/coList.js";
import { RawCoStream, RawBinaryCoStream } from "./coValues/coStream.js";
import {
secretSeedLength,
shortHashLength,
StreamingHash,
CryptoProvider,
} from "./crypto/crypto.js";
import { connectedPeers, Channel } from "./streamUtils.js";
import { ControlledAgent, RawControlledAccount } from "./coValues/account.js";
import type { Role } from "./permissions.js";
import { rawCoIDtoBytes, rawCoIDfromBytes, isRawCoID } from "./ids.js";
import { RawGroup, EVERYONE } from "./coValues/group.js";
import type { Everyone } from "./coValues/group.js";
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
import { parseJSON } from "./jsonStringify.js";
import {
RawAccount,
RawProfile,
accountHeaderForInitialAgentSecret,
} from "./coValues/account.js";
import { expectGroup } from "./typeUtils/expectGroup.js";
import { isAccountID } from "./typeUtils/isAccountID.js";
import type { SessionID, AgentID } from "./ids.js";
import type { CoID, AnyRawCoValue } from "./coValue.js";
import type {
BinaryStreamInfo,
BinaryCoStreamMeta,
} from "./coValues/coStream.js";
import type { JsonValue } from "./jsonValue.js";
import type {
SyncMessage,
Peer,
IncomingSyncStream,
OutgoingSyncQueue,
} from "./sync.js";
import { DisconnectedError, PingTimeoutError } from "./sync.js";
import type { AgentSecret } from "./crypto/crypto.js";
import type {
RawAccountID,
AccountMeta,
RawAccountMigration,
} from "./coValues/account.js";
import type { InviteSecret } from "./coValues/group.js";
import type * as Media from "./media.js";
type Value = JsonValue | AnyRawCoValue;
import { LSMStorage, BlockFilename, WalFilename } from "./storage/index.js";
import { FileSystem } from "./storage/FileSystem.js";
import { getPriorityFromHeader } from "./priority.js";
/** @hidden */
export const cojsonInternals = {
connectedPeers,
rawCoIDtoBytes,
rawCoIDfromBytes,
secretSeedLength,
shortHashLength,
expectGroup,
base64URLtoBytes,
bytesToBase64url,
parseJSON,
accountOrAgentIDfromSessionID,
isAccountID,
accountHeaderForInitialAgentSecret,
idforHeader,
StreamingHash,
Channel,
getPriorityFromHeader,
};
export {
LocalNode,
RawGroup,
Role,
EVERYONE,
Everyone,
RawCoMap,
RawCoList,
RawCoStream,
RawBinaryCoStream,
RawCoValue,
CoID,
AnyRawCoValue,
RawAccount,
RawAccountID,
AccountMeta,
RawAccountMigration,
RawProfile as Profile,
SessionID,
Media,
CoValueCore,
ControlledAgent,
RawControlledAccount,
MAX_RECOMMENDED_TX_SIZE,
JsonValue,
Peer,
BinaryStreamInfo,
BinaryCoStreamMeta,
AgentID,
AgentSecret,
InviteSecret,
CryptoProvider,
SyncMessage,
isRawCoID,
LSMStorage,
};
export type {
Value,
FileSystem,
BlockFilename,
WalFilename,
IncomingSyncStream,
OutgoingSyncQueue,
DisconnectedError,
PingTimeoutError,
CoValueUniqueness,
};
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace CojsonInternalTypes {
export type CoValueKnownState = import("./sync.js").CoValueKnownState;
export type DoneMessage = import("./sync.js").DoneMessage;
export type KnownStateMessage = import("./sync.js").KnownStateMessage;
export type LoadMessage = import("./sync.js").LoadMessage;
export type NewContentMessage = import("./sync.js").NewContentMessage;
export type CoValueHeader = import("./coValueCore.js").CoValueHeader;
export type Transaction = import("./coValueCore.js").Transaction;
export type TransactionID = import("./ids.js").TransactionID;
export type Signature = import("./crypto/crypto.js").Signature;
export type RawCoID = import("./ids.js").RawCoID;
export type ProfileShape = import("./coValues/account.js").ProfileShape;
export type SealerSecret = import("./crypto/crypto.js").SealerSecret;
export type SignerSecret = import("./crypto/crypto.js").SignerSecret;
export type JsonObject = import("./jsonValue.js").JsonObject;
}

View File

@@ -1,152 +1,2 @@
import { PureJSCrypto } from "./crypto/PureJSCrypto.js";
import {
CoValueCore,
type CoValueUniqueness,
MAX_RECOMMENDED_TX_SIZE,
idforHeader,
} from "./coValueCore.js";
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
import { LocalNode } from "./localNode.js";
import { type RawCoValue } from "./coValue.js";
import { RawCoMap } from "./coValues/coMap.js";
import { RawCoList } from "./coValues/coList.js";
import { RawCoStream, RawBinaryCoStream } from "./coValues/coStream.js";
import {
secretSeedLength,
shortHashLength,
StreamingHash,
CryptoProvider,
} from "./crypto/crypto.js";
import { connectedPeers, Channel } from "./streamUtils.js";
import { ControlledAgent, RawControlledAccount } from "./coValues/account.js";
import type { Role } from "./permissions.js";
import { rawCoIDtoBytes, rawCoIDfromBytes, isRawCoID } from "./ids.js";
import { RawGroup, EVERYONE } from "./coValues/group.js";
import type { Everyone } from "./coValues/group.js";
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
import { parseJSON } from "./jsonStringify.js";
import {
RawAccount,
RawProfile,
accountHeaderForInitialAgentSecret,
} from "./coValues/account.js";
import { expectGroup } from "./typeUtils/expectGroup.js";
import { isAccountID } from "./typeUtils/isAccountID.js";
import type { SessionID, AgentID } from "./ids.js";
import type { CoID, AnyRawCoValue } from "./coValue.js";
import type {
BinaryStreamInfo,
BinaryCoStreamMeta,
} from "./coValues/coStream.js";
import type { JsonValue } from "./jsonValue.js";
import type {
SyncMessage,
Peer,
IncomingSyncStream,
OutgoingSyncQueue,
} from "./sync.js";
import { DisconnectedError, PingTimeoutError } from "./sync.js";
import type { AgentSecret } from "./crypto/crypto.js";
import type {
RawAccountID,
AccountMeta,
RawAccountMigration,
} from "./coValues/account.js";
import type { InviteSecret } from "./coValues/group.js";
import type * as Media from "./media.js";
type Value = JsonValue | AnyRawCoValue;
import { LSMStorage, BlockFilename, WalFilename } from "./storage/index.js";
import { FileSystem } from "./storage/FileSystem.js";
import { getPriorityFromHeader } from "./priority.js";
/** @hidden */
export const cojsonInternals = {
connectedPeers,
rawCoIDtoBytes,
rawCoIDfromBytes,
secretSeedLength,
shortHashLength,
expectGroup,
base64URLtoBytes,
bytesToBase64url,
parseJSON,
accountOrAgentIDfromSessionID,
isAccountID,
accountHeaderForInitialAgentSecret,
idforHeader,
StreamingHash,
Channel,
getPriorityFromHeader,
};
export {
LocalNode,
RawGroup,
Role,
EVERYONE,
Everyone,
RawCoMap,
RawCoList,
RawCoStream,
RawBinaryCoStream,
RawCoValue,
CoID,
AnyRawCoValue,
RawAccount,
RawAccountID,
AccountMeta,
RawAccountMigration,
RawProfile as Profile,
SessionID,
Media,
CoValueCore,
ControlledAgent,
RawControlledAccount,
MAX_RECOMMENDED_TX_SIZE,
JsonValue,
Peer,
BinaryStreamInfo,
BinaryCoStreamMeta,
AgentID,
AgentSecret,
InviteSecret,
CryptoProvider,
SyncMessage,
isRawCoID,
LSMStorage,
PureJSCrypto,
};
export type {
Value,
FileSystem,
BlockFilename,
WalFilename,
IncomingSyncStream,
OutgoingSyncQueue,
DisconnectedError,
PingTimeoutError,
CoValueUniqueness,
};
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace CojsonInternalTypes {
export type CoValueKnownState = import("./sync.js").CoValueKnownState;
export type DoneMessage = import("./sync.js").DoneMessage;
export type KnownStateMessage = import("./sync.js").KnownStateMessage;
export type LoadMessage = import("./sync.js").LoadMessage;
export type NewContentMessage = import("./sync.js").NewContentMessage;
export type CoValueHeader = import("./coValueCore.js").CoValueHeader;
export type Transaction = import("./coValueCore.js").Transaction;
export type TransactionID = import("./ids.js").TransactionID;
export type Signature = import("./crypto/crypto.js").Signature;
export type RawCoID = import("./ids.js").RawCoID;
export type ProfileShape = import("./coValues/account.js").ProfileShape;
export type SealerSecret = import("./crypto/crypto.js").SealerSecret;
export type SignerSecret = import("./crypto/crypto.js").SignerSecret;
export type JsonObject = import("./jsonValue.js").JsonObject;
}
export * from "./exports.js";
export { PureJSCrypto } from "./crypto/PureJSCrypto.js";

View File

@@ -1,152 +1,2 @@
import { WasmCrypto } from "./crypto/WasmCrypto.js";
import {
CoValueCore,
type CoValueUniqueness,
MAX_RECOMMENDED_TX_SIZE,
idforHeader,
} from "./coValueCore.js";
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
import { LocalNode } from "./localNode.js";
import { type RawCoValue } from "./coValue.js";
import { RawCoMap } from "./coValues/coMap.js";
import { RawCoList } from "./coValues/coList.js";
import { RawCoStream, RawBinaryCoStream } from "./coValues/coStream.js";
import {
secretSeedLength,
shortHashLength,
StreamingHash,
CryptoProvider,
} from "./crypto/crypto.js";
import { connectedPeers, Channel } from "./streamUtils.js";
import { ControlledAgent, RawControlledAccount } from "./coValues/account.js";
import type { Role } from "./permissions.js";
import { rawCoIDtoBytes, rawCoIDfromBytes, isRawCoID } from "./ids.js";
import { RawGroup, EVERYONE } from "./coValues/group.js";
import type { Everyone } from "./coValues/group.js";
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
import { parseJSON } from "./jsonStringify.js";
import {
RawAccount,
RawProfile,
accountHeaderForInitialAgentSecret,
} from "./coValues/account.js";
import { expectGroup } from "./typeUtils/expectGroup.js";
import { isAccountID } from "./typeUtils/isAccountID.js";
import type { SessionID, AgentID } from "./ids.js";
import type { CoID, AnyRawCoValue } from "./coValue.js";
import type {
BinaryStreamInfo,
BinaryCoStreamMeta,
} from "./coValues/coStream.js";
import type { JsonValue } from "./jsonValue.js";
import type {
SyncMessage,
Peer,
IncomingSyncStream,
OutgoingSyncQueue,
} from "./sync.js";
import { DisconnectedError, PingTimeoutError } from "./sync.js";
import type { AgentSecret } from "./crypto/crypto.js";
import type {
RawAccountID,
AccountMeta,
RawAccountMigration,
} from "./coValues/account.js";
import type { InviteSecret } from "./coValues/group.js";
import type * as Media from "./media.js";
type Value = JsonValue | AnyRawCoValue;
import { LSMStorage, BlockFilename, WalFilename } from "./storage/index.js";
import { FileSystem } from "./storage/FileSystem.js";
import { getPriorityFromHeader } from "./priority.js";
/** @hidden */
export const cojsonInternals = {
connectedPeers,
rawCoIDtoBytes,
rawCoIDfromBytes,
secretSeedLength,
shortHashLength,
expectGroup,
base64URLtoBytes,
bytesToBase64url,
parseJSON,
accountOrAgentIDfromSessionID,
isAccountID,
accountHeaderForInitialAgentSecret,
idforHeader,
StreamingHash,
Channel,
getPriorityFromHeader,
};
export {
LocalNode,
RawGroup,
Role,
EVERYONE,
Everyone,
RawCoMap,
RawCoList,
RawCoStream,
RawBinaryCoStream,
RawCoValue,
CoID,
AnyRawCoValue,
RawAccount,
RawAccountID,
AccountMeta,
RawAccountMigration,
RawProfile as Profile,
SessionID,
Media,
CoValueCore,
ControlledAgent,
RawControlledAccount,
MAX_RECOMMENDED_TX_SIZE,
JsonValue,
Peer,
BinaryStreamInfo,
BinaryCoStreamMeta,
AgentID,
AgentSecret,
InviteSecret,
CryptoProvider,
SyncMessage,
isRawCoID,
LSMStorage,
WasmCrypto,
};
export type {
Value,
FileSystem,
BlockFilename,
WalFilename,
IncomingSyncStream,
OutgoingSyncQueue,
DisconnectedError,
PingTimeoutError,
CoValueUniqueness,
};
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace CojsonInternalTypes {
export type CoValueKnownState = import("./sync.js").CoValueKnownState;
export type DoneMessage = import("./sync.js").DoneMessage;
export type KnownStateMessage = import("./sync.js").KnownStateMessage;
export type LoadMessage = import("./sync.js").LoadMessage;
export type NewContentMessage = import("./sync.js").NewContentMessage;
export type CoValueHeader = import("./coValueCore.js").CoValueHeader;
export type Transaction = import("./coValueCore.js").Transaction;
export type TransactionID = import("./ids.js").TransactionID;
export type Signature = import("./crypto/crypto.js").Signature;
export type RawCoID = import("./ids.js").RawCoID;
export type ProfileShape = import("./coValues/account.js").ProfileShape;
export type SealerSecret = import("./crypto/crypto.js").SealerSecret;
export type SignerSecret = import("./crypto/crypto.js").SignerSecret;
export type JsonObject = import("./jsonValue.js").JsonObject;
}
export * from "./exports.js";
export { WasmCrypto } from "./crypto/WasmCrypto.js";

View File

@@ -0,0 +1,35 @@
export type {
InviteSecret,
Peer,
SessionID,
AgentID,
SyncMessage,
CryptoProvider,
CoValueUniqueness,
} from "cojson";
export type { ID, CoValue } from "./internal.js";
export { Encoders, co } from "./internal.js";
export { CoMap, type CoMapInit } from "./internal.js";
export { CoList } from "./internal.js";
export { CoStream, BinaryCoStream } from "./internal.js";
export { Group, Profile } from "./internal.js";
export { Account, isControlledAccount, type AccountClass } from "./internal.js";
export { ImageDefinition } from "./internal.js";
export { CoValueBase, type CoValueClass } from "./internal.js";
export type { DepthsIn, DeeplyLoaded } from "./internal.js";
export { loadCoValue, subscribeToCoValue } from "./internal.js";
export {
type AuthMethod,
type AuthResult,
createJazzContext,
fixedCredentialsAuth,
ephemeralCredentialsAuth,
AnonymousJazzAgent,
createAnonymousJazzContext,
randomSessionProvider,
} from "./internal.js";

View File

@@ -1,40 +1,7 @@
export * from "./exports.js";
export {
cojsonInternals,
MAX_RECOMMENDED_TX_SIZE,
PureJSCrypto,
} from "cojson/native";
export type {
InviteSecret,
Peer,
SessionID,
AgentID,
SyncMessage,
CryptoProvider,
CoValueUniqueness,
} from "cojson";
export type { ID, CoValue } from "./internal.js";
export { Encoders, co } from "./internal.js";
export { CoMap, type CoMapInit } from "./internal.js";
export { CoList } from "./internal.js";
export { CoStream, BinaryCoStream } from "./internal.js";
export { Group, Profile } from "./internal.js";
export { Account, isControlledAccount, type AccountClass } from "./internal.js";
export { ImageDefinition } from "./internal.js";
export { CoValueBase, type CoValueClass } from "./internal.js";
export type { DepthsIn, DeeplyLoaded } from "./internal.js";
export { loadCoValue, subscribeToCoValue } from "./internal.js";
export {
type AuthMethod,
type AuthResult,
createJazzContext,
fixedCredentialsAuth,
ephemeralCredentialsAuth,
AnonymousJazzAgent,
createAnonymousJazzContext,
randomSessionProvider,
} from "./internal.js";

View File

@@ -1,36 +1,3 @@
export * from "./exports.js";
export { cojsonInternals, MAX_RECOMMENDED_TX_SIZE, WasmCrypto } from "cojson";
export type {
InviteSecret,
Peer,
SessionID,
AgentID,
SyncMessage,
CryptoProvider,
CoValueUniqueness,
} from "cojson";
export type { ID, CoValue } from "./internal.js";
export { Encoders, co } from "./internal.js";
export { CoMap, type CoMapInit } from "./internal.js";
export { CoList } from "./internal.js";
export { CoStream, BinaryCoStream } from "./internal.js";
export { Group, Profile } from "./internal.js";
export { Account, isControlledAccount, type AccountClass } from "./internal.js";
export { ImageDefinition } from "./internal.js";
export { CoValueBase, type CoValueClass } from "./internal.js";
export type { DepthsIn, DeeplyLoaded } from "./internal.js";
export { loadCoValue, subscribeToCoValue } from "./internal.js";
export {
type AuthMethod,
type AuthResult,
createJazzContext,
fixedCredentialsAuth,
ephemeralCredentialsAuth,
AnonymousJazzAgent,
createAnonymousJazzContext,
randomSessionProvider,
} from "./internal.js";

4347
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ export default defineConfig({
root: "./",
test: {
coverage: {
enabled: true,
enabled: false,
provider: "istanbul",
include: ["packages/*/src/**/*.ts"],
exclude: ["packages/*/src/tests"],
@@ -20,7 +20,6 @@ export default defineConfig({
},
},
include: ["packages/*/tests/**/*.test.ts"],
watch: false,
watchExclude: ["**/node_modules/**", "**/dist/**"],
maxConcurrency: 5,
},