Compare commits

...

15 Commits

Author SHA1 Message Date
Guido D'Orsi
d18d09e002 Merge pull request #2574 from garden-co/changeset-release/main
Version Packages
2025-06-24 15:33:26 +02:00
github-actions[bot]
d983f27bbe Version Packages 2025-06-24 13:32:21 +00:00
Guido D'Orsi
fcf83b0da4 Merge pull request #2570 from joeinnes/post-0.15-docs-fixes
Post 0.15 docs fixes
2025-06-24 15:26:22 +02:00
Guido D'Orsi
9231e2c22f Merge pull request #2568 from garden-co/feat/cotext-display
feat(inspector): improve CoPlainText view
2025-06-24 15:24:54 +02:00
Guido D'Orsi
33157ee0ad Merge pull request #2573 from garden-co/fix/debug-transactions
fix: add debug code on parseJSON errors during the transactions parsing
2025-06-24 15:24:18 +02:00
Guido D'Orsi
4b964edcaf fix: add debug code on parseJSON errors during the transactions parsing 2025-06-24 15:16:10 +02:00
Guido D'Orsi
a8e1726797 Merge pull request #2569 from garden-co/docs/type-aliases
docs: add type aliases to the docs examples
2025-06-23 21:25:43 +02:00
Joe Innes
a6eeada331 docs: ⚠️ update Node.js requirement to v20.0.0 or later
* Updated the minimum Node.js version requirement in the documentation to ensure compatibility with the latest features and improvements.
2025-06-23 20:56:59 +02:00
Joe Innes
3b38a8241c docs: ✏️ add Node.js v20 requirement alerts in documentation
* Added `<Alert>` components to inform users about the requirement of Node.js v20 across various setup documentation files.
* Updated installation instructions for clarity and improved user guidance.
2025-06-23 20:56:40 +02:00
Joe Innes
c49330c308 docs: ⚠️ add warning about custom AccountSchema requirement
* Added an `<Alert>` component to inform users that they need to pass their custom `AccountSchema` to the provider.
* This change aims to reduce confusion for new adopters regarding schema registration.
2025-06-23 20:20:12 +02:00
Guido D'Orsi
0c4e27c18d docs: add type aliases to the docs examples 2025-06-23 19:54:42 +02:00
Joe Innes
d8d273821e docs: ✏️ remove 'note' on docs inaccuracies
* Removed outdated note about Jazz 0.14.0 release.
* We've got links on the right now so docs issues can be reported, no need for an extra chore to bump the version number in this note every release.
2025-06-23 18:04:04 +02:00
Guido D'Orsi
def0ca81b4 Merge pull request #2567 from garden-co/fix/clerk-starter
fix: fix clerk-expo starter and make it more minimal
2025-06-23 18:00:08 +02:00
Trisha Lim
0e7e53238b feat(inspector): improve CoPlainText view 2025-06-23 15:17:47 +01:00
Guido D'Orsi
fcc18e5212 fix: fix clerk-expo starter and make it more minimal 2025-06-23 15:45:43 +02:00
113 changed files with 1274 additions and 3596 deletions

View File

@@ -1,23 +0,0 @@
node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
# macOS
.DS_Store
ios
android
# Env
.env
.env.*
!.env.example
!.env.test

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +0,0 @@
# 🎷 Jazz + Expo + `expo-router` + Clerk Auth
## 🚀 How to Run
### 1. Inside the Workspace Root
First, install dependencies and build the project:
```bash
pnpm i
mv .env.example .env
pnpm run build
```
Don't forget to update `VITE_CLERK_PUBLISHABLE_KEY` in `.env` with your [Publishable Key](https://clerk.com/docs/deployments/clerk-environment-variables#clerk-publishable-and-secret-keys) from Clerk.
### 2. Inside the `examples/chat-rn-expo-clerk` Directory
Next, navigate to the specific example project and run the following commands:
```bash
pnpm expo prebuild
pnpx pod-install
pnpm expo run:ios
```
This will set up and launch the app on iOS. For Android, you can replace the last command with `pnpm expo run:android`.

View File

@@ -1,45 +0,0 @@
{
"expo": {
"name": "jazz-chat-rn-expo-clerk",
"scheme": "jazz-chat-rn-expo-clerk",
"slug": "jazz-chat-rn-expo-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"
},
"newArchEnabled": true,
"plugins": [
"expo-secure-store",
"expo-font",
"expo-router",
"expo-sqlite",
[
"expo-image-picker",
{
"photosPermission": "The app accesses your photos to let you share them with your friends."
}
]
],
"extra": {
"eas": {
"projectId": "ca3d46e5-a10a-47ec-9d77-3b841e1c62d4"
}
}
}
}

View File

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

View File

@@ -1,33 +0,0 @@
import { SignedOut } from "@clerk/clerk-expo";
import { Link } from "expo-router";
import React from "react";
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

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

View File

@@ -1,65 +0,0 @@
import { useOAuth } from "@clerk/clerk-expo";
import * as Linking from "expo-linking";
import { Link } from "expo-router";
import * as WebBrowser from "expo-web-browser";
import React from "react";
import { Text, TouchableOpacity, View } from "react-native";
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-expo-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

@@ -1,79 +0,0 @@
import { useSignIn } from "@clerk/clerk-expo";
import { Link } from "expo-router";
import React from "react";
import { Text, TextInput, TouchableOpacity, View } from "react-native";
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

@@ -1,120 +0,0 @@
import { useSignUp } from "@clerk/clerk-expo";
import { useNavigation } from "@react-navigation/native";
import * as React from "react";
import { Text, TextInput, TouchableOpacity, View } from "react-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

@@ -1,42 +0,0 @@
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

@@ -1,29 +0,0 @@
import { Link, Stack } from "expo-router";
import { StyleSheet, Text, View } 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

@@ -1,71 +0,0 @@
import "../global.css";
import { ClerkLoaded, ClerkProvider } from "@clerk/clerk-expo";
import { secureStore } from "@clerk/clerk-expo/secure-store";
import { useFonts } from "expo-font";
import { Slot, useRouter, useSegments } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { useIsAuthenticated, useJazzContext } from "jazz-tools/expo";
import React, { useEffect } from "react";
import { tokenCache } from "../cache";
import { JazzAndAuth } from "../src/auth-context";
SplashScreen.preventAutoHideAsync();
function InitialLayout() {
const isAuthenticated = useIsAuthenticated();
const segments = useSegments();
const router = useRouter();
useEffect(() => {
const inAuthGroup = segments[0] === "(auth)";
if (isAuthenticated && inAuthGroup) {
router.replace("/chat");
} else if (!isAuthenticated && !inAuthGroup) {
router.replace("/");
}
SplashScreen.hideAsync();
}, [isAuthenticated, segments, router]);
return <Slot />;
}
export default function RootLayout() {
const [fontsLoaded] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
});
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",
);
}
useEffect(() => {
if (fontsLoaded) {
} else {
SplashScreen.preventAutoHideAsync();
}
}, [fontsLoaded]);
if (!fontsLoaded) {
return null;
}
return (
<ClerkProvider
tokenCache={tokenCache}
publishableKey={publishableKey}
__experimental_resourceCache={secureStore}
>
<ClerkLoaded>
<JazzAndAuth>
<InitialLayout />
</JazzAndAuth>
</ClerkLoaded>
</ClerkProvider>
);
}

View File

@@ -1,236 +0,0 @@
import { Chat, Message } from "@/src/schema";
import { useNavigation } from "@react-navigation/native";
import clsx from "clsx";
import * as Clipboard from "expo-clipboard";
import * as ImagePicker from "expo-image-picker";
import { useLocalSearchParams } from "expo-router";
import { CoPlainText, Group, Loaded } from "jazz-tools";
import { useAccount, useCoState } from "jazz-tools/expo";
import { ProgressiveImgNative } from "jazz-tools/expo";
import { createImageNative } from "jazz-tools/react-native-media-images";
import { useEffect, useLayoutEffect, useState } from "react";
import React, {
SafeAreaView,
View,
Text,
Alert,
TouchableOpacity,
FlatList,
KeyboardAvoidingView,
TextInput,
Button,
Image,
ActivityIndicator,
} from "react-native";
export default function Conversation() {
const { chatId } = useLocalSearchParams();
const { me } = useAccount();
const [chat, setChat] = useState<Loaded<typeof Chat>>();
const [message, setMessage] = useState("");
const loadedChat = useCoState(Chat, chat?.id, { resolve: { $each: true } });
const navigation = useNavigation();
const [isUploading, setIsUploading] = useState(false);
useEffect(() => {
if (chat) return;
if (chatId === "new") {
createChat();
} else {
loadChat(chatId as string);
}
}, [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: string) => {
try {
const chat = await Chat.load(chatId);
if (chat) 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: CoPlainText.create(message, chat._owner) },
chat._owner,
),
);
setMessage("");
}
};
const handleImageUpload = async () => {
try {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
base64: true,
quality: 0.7,
});
if (!result.canceled && result.assets[0].base64 && chat) {
setIsUploading(true);
const base64Uri = `data:image/jpeg;base64,${result.assets[0].base64}`;
const image = await createImageNative(base64Uri, {
owner: chat._owner,
maxSize: 2048,
});
chat.push(
Message.create(
{ text: CoPlainText.create("", chat._owner), image },
chat._owner,
),
);
}
} catch (error) {
Alert.alert("Error", "Failed to upload image");
} finally {
setIsUploading(false);
}
};
const renderMessageItem = ({ item }: { item: Loaded<typeof 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",
)}
>
{item.image && (
<ProgressiveImgNative
image={item.image}
maxWidth={1024}
children={({ src }) => (
<Image
source={{ uri: src }}
className="w-48 h-48 rounded-lg mb-2"
resizeMode="cover"
/>
)}
/>
)}
{item.text && (
<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().toString().padStart(2, "0")}:
{item._edits.text?.madeAt?.getMinutes().toString().padStart(2, "0")}
</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">
<TouchableOpacity
onPress={handleImageUpload}
disabled={isUploading}
className="h-10 w-10 items-center justify-center"
>
{isUploading ? (
<ActivityIndicator size="small" color="#0000ff" />
) : (
<Text className="text-2xl">🖼</Text>
)}
</TouchableOpacity>
<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

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

View File

@@ -1,90 +0,0 @@
import { useNavigation } from "@react-navigation/native";
import { useRouter } from "expo-router";
import { ID } from "jazz-tools";
import { useLayoutEffect } from "react";
import React, {
Button,
Text,
TouchableOpacity,
View,
Alert,
} from "react-native";
import { useUser } from "@clerk/clerk-expo";
import { useAccount } from "jazz-tools/expo";
import { Chat } from "../../src/schema";
export default function ChatScreen() {
const { logOut } = useAccount();
const router = useRouter();
const navigation = useNavigation();
const { user } = useUser();
function handleLogOut() {
logOut();
router.navigate("/");
}
useLayoutEffect(() => {
navigation.setOptions({
headerTitle: "Chat",
headerRight: () => <Button onPress={handleLogOut} title="Logout" />,
});
}, [navigation]);
const loadChat = async (chatId: string | "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);
} 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.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -1,9 +0,0 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
};
};

View File

@@ -1,39 +0,0 @@
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);
},
clearToken: (key: string) => {
return SecureStore.deleteItemAsync(key);
},
};
};
// 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

@@ -1,27 +0,0 @@
{
"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

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

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

View File

@@ -1,35 +0,0 @@
// Learn more https://docs.expo.dev/guides/monorepos
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");
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.requireCycleIgnorePatterns = [
/(^|\/|\\)node_modules($|\/|\\)/,
/(^|\/|\\)packages($|\/|\\)/,
];
// Use turborepo to restore the cache when possible
config.cacheStores = [
new FileStore({
root: path.join(projectRoot, "node_modules", ".cache", "metro"),
}),
];
// module.exports = config;
module.exports = withNativeWind(config, { input: "./global.css" });

View File

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

View File

@@ -1,61 +0,0 @@
{
"name": "chat-rn-expo-clerk",
"main": "index.js",
"scripts": {
"build": "expo export -p ios",
"start": "expo start",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write",
"android": "expo run:android",
"ios": "expo prebuild && pnpx pod-install && expo run:ios",
"web": "expo start --web",
"run:ios": "pnpm expo prebuild && npx pod-install && pnpm expo run:ios"
},
"dependencies": {
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@bacons/text-decoder": "0.0.0",
"@bam.tech/react-native-image-resizer": "^3.0.11",
"@clerk/clerk-expo": "^2.2.21",
"@expo/vector-icons": "^14.1.0",
"@react-native-community/netinfo": "11.4.1",
"@react-navigation/native": "7.0.19",
"@react-navigation/native-stack": "7.2.1",
"clsx": "^2.0.0",
"expo": "^53.0.8",
"expo-build-properties": "~0.14.6",
"expo-clipboard": "~7.1.4",
"expo-constants": "~17.1.6",
"expo-crypto": "~14.1.4",
"expo-dev-client": "~5.1.8",
"expo-file-system": "^18.1.9",
"expo-font": "~13.3.1",
"expo-image-picker": "~16.1.4",
"expo-linking": "~7.1.4",
"expo-router": "~5.0.6",
"expo-secure-store": "~14.2.3",
"expo-splash-screen": "~0.30.8",
"expo-sqlite": "15.2.9",
"expo-status-bar": "~2.2.3",
"expo-web-browser": "~14.1.6",
"jazz-tools": "workspace:*",
"nativewind": "^4.1.21",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.2",
"react-native-gesture-handler": "~2.24.0",
"react-native-get-random-values": "^1.11.0",
"react-native-reanimated": "~3.17.5",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "4.10.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-web": "~0.20.0",
"readable-stream": "4.7.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/react": "~19.0.14",
"tailwindcss": "^3.4.17",
"typescript": "5.8.3"
},
"private": true
}

View File

@@ -1 +0,0 @@
export const apiKey = "chat-rn-expo-clerk-example-jazz@garden.co";

View File

@@ -1,19 +0,0 @@
import { useClerk } from "@clerk/clerk-expo";
import { JazzExpoProviderWithClerk } from "jazz-tools/expo";
import React, { PropsWithChildren } from "react";
import { apiKey } from "./apiKey";
export function JazzAndAuth({ children }: PropsWithChildren) {
const clerk = useClerk();
return (
<JazzExpoProviderWithClerk
clerk={clerk}
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
}}
>
{children}
</JazzExpoProviderWithClerk>
);
}

View File

@@ -1,8 +0,0 @@
import { CoList, co, coField, z } from "jazz-tools";
export const Message = co.map({
text: co.plainText(),
image: z.optional(co.image()),
});
export const Chat = co.list(Message);

View File

@@ -1,14 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
// NOTE: Update this to include the paths to all of your component files.
content: [
"./app/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}",
"./src/**/*.{js,jsx,ts,tsx}",
],
presets: [require("nativewind/preset")],
theme: {
extend: {},
},
plugins: [],
};

View File

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

View File

@@ -17,7 +17,6 @@
"expo-clipboard": "^7.1.4",
"expo-secure-store": "~14.2.3",
"expo-sqlite": "~15.2.10",
"expo-status-bar": "~2.2.3",
"jazz-tools": "workspace:*",
"react": "19.0.0",
"react-native": "0.79.2",

View File

@@ -1,5 +1,12 @@
# passkey-svelte
## 0.0.91
### Patch Changes
- Updated dependencies [0e7e532]
- jazz-tools@0.15.2
## 0.0.90
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-svelte",
"version": "0.0.90",
"version": "0.0.91",
"type": "module",
"private": true,
"scripts": {

View File

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

44
examples/clerk-expo/.gitignore vendored Normal file
View File

@@ -0,0 +1,44 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
android/
ios/
# env files
.env
.env.*
!.env.example
!.env.test

View File

@@ -0,0 +1,32 @@
{
"expo": {
"name": "rnexpoclerk",
"slug": "rnexpoclerk",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "tools.jazz.rnexpoclerk"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"edgeToEdgeEnabled": true,
"package": "tools.jazz.chatrnexpo"
},
"web": {
"favicon": "./assets/favicon.png"
},
"plugins": ["expo-secure-store", "expo-sqlite", "expo-web-browser"]
}
}

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,9 @@
import { registerRootComponent } from "expo";
import "./polyfills";
import App from "./src/App";
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);

View File

@@ -0,0 +1,35 @@
{
"name": "clerk-expo",
"main": "index.ts",
"scripts": {
"build": "expo prebuild",
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web"
},
"dependencies": {
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@bacons/text-decoder": "^0.0.0",
"@bam.tech/react-native-image-resizer": "^3.0.11",
"@clerk/clerk-expo": "^2.13.1",
"@react-native-community/netinfo": "11.4.1",
"expo": "~53.0.9",
"expo-crypto": "~14.1.5",
"expo-linking": "~7.1.5",
"expo-secure-store": "~14.2.3",
"expo-sqlite": "~15.2.10",
"expo-web-browser": "~14.2.0",
"jazz-tools": "workspace:*",
"react": "19.0.0",
"react-native": "0.79.2",
"react-native-get-random-values": "^1.11.0",
"readable-stream": "^4.7.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/react": "~19.0.10",
"typescript": "~5.8.3"
},
"private": true
}

View File

@@ -0,0 +1,48 @@
import { ClerkLoaded, ClerkProvider, useClerk } from "@clerk/clerk-expo";
import { resourceCache } from "@clerk/clerk-expo/resource-cache";
import { tokenCache } from "@clerk/clerk-expo/token-cache";
import { JazzExpoProviderWithClerk } from "jazz-tools/expo";
import { MainScreen } from "./MainScreen";
import { apiKey } from "./apiKey";
import { AuthScreen } from "./auth/AuthScreen";
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const clerk = useClerk();
return (
<JazzExpoProviderWithClerk
clerk={clerk}
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
}}
>
{children}
</JazzExpoProviderWithClerk>
);
}
export default function RootLayout() {
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}
__experimental_resourceCache={resourceCache}
publishableKey={publishableKey}
>
<ClerkLoaded>
<JazzAndAuth>
<AuthScreen>
<MainScreen />
</AuthScreen>
</JazzAndAuth>
</ClerkLoaded>
</ClerkProvider>
);
}

View File

@@ -0,0 +1,44 @@
import { useClerk } from "@clerk/clerk-expo";
import { useAccount } from "jazz-tools/expo";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
export function MainScreen() {
const { me } = useAccount();
const { signOut } = useClerk();
const handleSignOut = async () => {
await signOut();
};
return (
<View style={styles.container}>
<Text style={styles.title}>You're logged in</Text>
<Text style={styles.subtitle}>Welcome back, {me?.profile?.name}</Text>
<TouchableOpacity onPress={handleSignOut}>
<Text>Sign out</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
backgroundColor: "#fff",
},
title: {
fontSize: 24,
fontWeight: "bold",
marginBottom: 16,
textAlign: "center",
},
subtitle: {
fontSize: 16,
marginBottom: 24,
textAlign: "center",
color: "#666",
},
});

View File

@@ -0,0 +1 @@
export const apiKey = "chat-rn-expo-example-jazz@garden.co";

View File

@@ -0,0 +1,19 @@
import { useIsAuthenticated } from "jazz-tools/expo";
import { type ReactNode, useState } from "react";
import { SignInScreen } from "./SignInScreen";
import { SignUpScreen } from "./SignUpScreen";
export function AuthScreen({ children }: { children: ReactNode }) {
const isAuthenticated = useIsAuthenticated();
const [page, setPage] = useState<"sign-in" | "sign-up">("sign-in");
if (isAuthenticated) {
return children;
}
if (page === "sign-in") {
return <SignInScreen setPage={setPage} />;
}
return <SignUpScreen setPage={setPage} />;
}

View File

@@ -0,0 +1,127 @@
import { useSignIn } from "@clerk/clerk-expo";
import { useState } from "react";
import {
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
export function SignInScreen({
setPage,
}: { setPage: (page: "sign-in" | "sign-up") => void }) {
const { signIn, setActive, isLoaded } = useSignIn();
const [emailAddress, setEmailAddress] = useState("");
const [password, setPassword] = useState("");
// Handle the submission of the sign-in form
const onSignInPress = async () => {
if (!isLoaded) return;
// Start the sign-in process using the email and password provided
try {
const signInAttempt = await signIn.create({
identifier: emailAddress,
password,
});
// If sign-in process is complete, set the created session as active
// and redirect the user
if (signInAttempt.status === "complete") {
await setActive({ session: signInAttempt.createdSessionId });
} else {
// If the status isn't complete, check why. User might need to
// complete further steps.
console.error(JSON.stringify(signInAttempt, null, 2));
}
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2));
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Sign in</Text>
<TextInput
style={styles.input}
autoCapitalize="none"
value={emailAddress}
placeholder="Enter email"
onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
/>
<TextInput
style={styles.input}
value={password}
placeholder="Enter password"
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
/>
<TouchableOpacity style={styles.button} onPress={onSignInPress}>
<Text style={styles.buttonText}>Continue</Text>
</TouchableOpacity>
<View style={styles.footer}>
<Text style={styles.footerText}>Don't have an account?</Text>
<TouchableOpacity onPress={() => setPage("sign-up")}>
<Text style={styles.linkText}>Sign up</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: "center",
backgroundColor: "#f5f5f5",
},
title: {
fontSize: 24,
fontWeight: "bold",
textAlign: "center",
marginBottom: 30,
color: "#333",
},
input: {
backgroundColor: "white",
borderWidth: 1,
borderColor: "#ddd",
borderRadius: 8,
padding: 15,
marginBottom: 15,
fontSize: 16,
},
button: {
backgroundColor: "#007AFF",
borderRadius: 8,
padding: 15,
alignItems: "center",
marginTop: 10,
},
buttonText: {
color: "white",
fontSize: 16,
fontWeight: "600",
},
footer: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
marginTop: 20,
gap: 5,
},
footerText: {
color: "#666",
fontSize: 14,
},
linkText: {
color: "#007AFF",
fontSize: 14,
fontWeight: "600",
},
});

View File

@@ -0,0 +1,169 @@
import { useSignUp } from "@clerk/clerk-expo";
import * as React from "react";
import {
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
export function SignUpScreen({
setPage,
}: { setPage: (page: "sign-in" | "sign-up") => void }) {
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("");
// Handle submission of sign-up form
const onSignUpPress = async () => {
if (!isLoaded) return;
console.log(emailAddress, password);
// Start sign-up process using email and password provided
try {
await signUp.create({
emailAddress,
password,
});
// Send user an email with verification code
await signUp.prepareEmailAddressVerification({ strategy: "email_code" });
// Set 'pendingVerification' to true to display second form
// and capture OTP code
setPendingVerification(true);
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2));
}
};
// Handle submission of verification form
const onVerifyPress = async () => {
if (!isLoaded) return;
try {
// Use the code the user provided to attempt verification
const signUpAttempt = await signUp.attemptEmailAddressVerification({
code,
});
// If verification was completed, set the session to active
// and redirect the user
if (signUpAttempt.status === "complete") {
await setActive({ session: signUpAttempt.createdSessionId });
} else {
// If the status is not complete, check why. User may need to
// complete further steps.
console.error(JSON.stringify(signUpAttempt, null, 2));
}
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2));
}
};
if (pendingVerification) {
return (
<View style={styles.container}>
<Text style={styles.title}>Verify your email</Text>
<TextInput
style={styles.input}
value={code}
placeholder="Enter your verification code"
onChangeText={(code) => setCode(code)}
/>
<TouchableOpacity style={styles.button} onPress={onVerifyPress}>
<Text style={styles.buttonText}>Verify</Text>
</TouchableOpacity>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Sign up</Text>
<TextInput
style={styles.input}
autoCapitalize="none"
value={emailAddress}
placeholder="Enter email"
onChangeText={(email) => setEmailAddress(email)}
/>
<TextInput
style={styles.input}
value={password}
placeholder="Enter password"
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
/>
<TouchableOpacity style={styles.button} onPress={onSignUpPress}>
<Text style={styles.buttonText}>Continue</Text>
</TouchableOpacity>
<View style={styles.linkContainer}>
<Text style={styles.linkText}>Already have an account? </Text>
<TouchableOpacity onPress={() => setPage("sign-in")}>
<Text style={styles.link}>Sign in</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
padding: 20,
backgroundColor: "#f5f5f5",
},
title: {
fontSize: 24,
fontWeight: "bold",
textAlign: "center",
marginBottom: 30,
color: "#333",
},
input: {
backgroundColor: "white",
borderWidth: 1,
borderColor: "#ddd",
borderRadius: 8,
padding: 15,
marginBottom: 15,
fontSize: 16,
},
button: {
backgroundColor: "#007AFF",
borderRadius: 8,
padding: 15,
alignItems: "center",
marginBottom: 20,
},
buttonText: {
color: "white",
fontSize: 16,
fontWeight: "600",
},
linkContainer: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
},
linkText: {
color: "#666",
fontSize: 14,
},
link: {
color: "#007AFF",
fontSize: 14,
fontWeight: "600",
},
});

View File

@@ -0,0 +1,9 @@
import { co, z } from "jazz-tools";
export const Message = co.map({
text: z.string(),
});
export type Message = co.loaded<typeof Message>;
export const Chat = co.list(Message);
export type Chat = co.loaded<typeof Chat>;

View File

@@ -0,0 +1,6 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
}
}

View File

@@ -509,10 +509,10 @@ const rnExamples: Example[] = [
},
{
name: "Chat",
slug: "chat-rn-expo-clerk",
name: "Clerk",
slug: "clerk-expo",
description:
"Exactly like the React Native Expo chat app, with Clerk for auth.",
"An example Expo app that uses Clerk for authentication.",
tech: [tech.reactNative, tech.expo],
features: [features.clerk],
illustration: <ClerkIllustration />,

View File

@@ -76,7 +76,6 @@ Use `<JazzExpoProviderWithClerk />` to wrap your app.
<CodeGroup>
```tsx twoslash
import * as React from "react";
import { Slot } from "expo-router";
const apiKey = "you@example.com";
const tokenCache = {
getToken: async (key: string) => {
@@ -85,9 +84,12 @@ const tokenCache = {
saveToken: async (key: string, token: string) => {},
clearToken: async (key: string) => {},
};
function MainScreen() {
return null;
}
// ---cut---
import { useClerk, ClerkProvider, ClerkLoaded } from '@clerk/clerk-expo';
import { secureStore } from "@clerk/clerk-expo/secure-store";
import { resourceCache } from '@clerk/clerk-expo/resource-cache';
import { JazzExpoProviderWithClerk } from "jazz-tools/expo";
function JazzAndAuth({ children }: { children: React.ReactNode }) {
@@ -118,11 +120,11 @@ export default function RootLayout() {
<ClerkProvider
tokenCache={tokenCache}
publishableKey={publishableKey}
__experimental_resourceCache={secureStore}
__experimental_resourceCache={resourceCache}
>
<ClerkLoaded>
<JazzAndAuth>
<Slot />
<MainScreen />
</JazzAndAuth>
</ClerkLoaded>
</ClerkProvider>
@@ -132,8 +134,6 @@ export default function RootLayout() {
</CodeGroup>
</ContentByFramework>
Once set up, you can use Clerk's auth methods for login and signup:
<ContentByFramework framework="react">
<CodeGroup>
```tsx twoslash

View File

@@ -1,4 +1,5 @@
import { CodeGroup, ContentByFramework, JazzLogo } from '@/components/forMdx'
import { CodeGroup, ContentByFramework, JazzLogo } from "@/components/forMdx";
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
export const metadata = {
title: "Documentation",
@@ -12,20 +13,21 @@ Instead of wrestling with databases, APIs, and server infrastructure, you work w
---
**Note:** We just released [Jazz 0.14.0](/docs/upgrade/0-14-0) with a bunch of breaking changes and are still cleaning the docs up - see the [upgrade guide](/docs/upgrade/0-14-0) for details.
## Quickstart
You can use [`create-jazz-app`](/docs/tools/create-jazz-app) to create a new Jazz project from one of our starter templates or example apps:
<CodeGroup>
```sh
npx create-jazz-app@latest --api-key you@example.com
```
```sh
npx create-jazz-app@latest --api-key you@example.com
```
</CodeGroup>
<Alert variant="info" className="mt-4 flex gap-2 items-center">Requires at least Node.js v20.</Alert>
{/* <ContentByFramework framework="react">
Or you can follow this [React step-by-step guide](/docs/react/guide) where we walk you through building an issue tracker app.
Or you can follow this [React step-by-step guide](/docs/react/guide) where we walk you through building an issue tracker app.
</ContentByFramework> */}
## Why Jazz is different
@@ -33,7 +35,7 @@ npx create-jazz-app@latest --api-key you@example.com
Most apps rebuild the same thing: shared state that syncs between users and devices. Jazz starts from that shared state, giving you:
- **No backend required** — Focus on building features, not infrastructure
- **Real-time sync** — Changes appear everywhere immediately
- **Real-time sync** — Changes appear everywhere immediately
- **Multiplayer by default** — Collaboration just works
- **Local-first** — Your app works offline and feels instant

View File

@@ -3,6 +3,7 @@ export const metadata = {
};
import { CodeGroup } from "@/components/forMdx";
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
# React Native (Expo) Installation and Setup
@@ -49,7 +50,10 @@ npm i -S jazz-tools
```
</CodeGroup>
**Note**: Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include: `text-encoding`, `base-64`, and you can drop `@bacons/text-decoder`.
<Alert variant="info" className="mt-4" title="Note">
- Requires at least Node.js v20.
- Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include `text-encoding` and `base-64`, and you can drop `@bacons/text-decoder`.
</Alert>
#### Fix incompatible dependencies
@@ -180,7 +184,7 @@ Lastly, ensure that the `"main"` field in your `package.json` points to `index.j
## Authentication
Jazz provides authentication to help users access their data across multiple devices. For details on implementing authentication with Expo, check our [Authentication Overview](/docs/authentication/overview) guide and see the [Expo Chat Demo](https://github.com/garden-co/jazz/tree/main/examples/chat-rn-expo-clerk) for a complete example.
Jazz provides authentication to help users access their data across multiple devices. For details on implementing authentication with Expo, check our [Authentication Overview](/docs/authentication/overview) guide and see the [Expo Clerk Demo](https://github.com/garden-co/jazz/tree/main/examples/clerk-expo) for a complete example.
## Next Steps

View File

@@ -3,6 +3,7 @@ export const metadata = {
};
import { CodeGroup } from "@/components/forMdx";
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
# React Native Installation and Setup
@@ -49,7 +50,10 @@ npm i -S jazz-tools
```
</CodeGroup>
**Note**: Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include `text-encoding` and `base-64`, and you can drop `@bacons/text-decoder`.
<Alert variant="info" className="mt-4" title="Note">
- Requires at least Node.js v20.
- Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include `text-encoding` and `base-64`, and you can drop `@bacons/text-decoder`.
</Alert>
### Configure Metro

View File

@@ -3,6 +3,7 @@ export const metadata = {
};
import { CodeGroup } from "@/components/forMdx";
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
# React Installation and Setup
@@ -26,6 +27,8 @@ pnpm install jazz-tools
```
</CodeGroup>
<Alert variant="info" className="mt-4 flex gap-2 items-center">Requires at least Node.js v20.</Alert>
## Write your schema
Define your data schema using [CoValues](/docs/schemas/covalues) from `jazz-tools`.

View File

@@ -3,6 +3,7 @@ export const metadata = {
};
import { CodeGroup } from "@/components/forMdx";
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
# Node.JS / server workers
@@ -12,6 +13,8 @@ This lets you share CoValues with Server Workers, having precise access control
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/jazz-paper-scissors)
<Alert variant="info" className="mt-4 flex gap-2 items-center">Requires at least Node.js v20.</Alert>
## Generating credentials
Server Workers typically have static credentials, consisting of a public Account ID and a private Account Secret.

View File

@@ -3,6 +3,7 @@ export const metadata = {
};
import { CodeGroup } from "@/components/forMdx";
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
# Svelte Installation and Setup
@@ -10,7 +11,7 @@ Jazz can be used with Svelte or in a SvelteKit app.
To add some Jazz to your Svelte app, you can use the following steps:
1. Install Jazz dependencies
## Install Jazz dependencies
<CodeGroup>
```sh
@@ -18,7 +19,9 @@ pnpm install jazz-tools
```
</CodeGroup>
2. Write your schema
<Alert variant="info" className="mt-4 flex gap-2 items-center">Requires at least Node.js v20.</Alert>
## Write your schema
See the [schema docs](/docs/schemas/covalues) for more information.
@@ -40,7 +43,7 @@ export class MyAccount extends Account {
```
</CodeGroup>
3. Set up the Provider in your root layout
## Set up the Provider in your root layout
<CodeGroup>
```svelte
@@ -60,7 +63,7 @@ export class MyAccount extends Account {
```
</CodeGroup>
4. Use Jazz hooks in your components
## Use Jazz hooks in your components
<CodeGroup>
```svelte

View File

@@ -89,5 +89,5 @@ When you run `create-jazz-app`, we'll:
## Requirements
- Node.js 14.0.0 or later
- Node.js 20.0.0 or later
- Your preferred package manager (npm, yarn, pnpm, bun, or deno)

View File

@@ -1,4 +1,5 @@
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
# Jazz 0.14.0 Introducing Zod-based schemas
@@ -221,6 +222,10 @@ We have removed the Typescript AccountSchema registration.
It was causing some deal of confusion to new adopters so we have decided to replace the magic inference with a more explicit approach.
<Alert variant="warning" className="flex gap-2 items-center my-4">
You still need to pass your custom AccountSchema to your provider!
</Alert>
<ContentByFramework framework={["react", "react-native", "vue", "vanilla", "react-native-expo"]}>
<CodeGroup>
```tsx

View File

@@ -28,6 +28,7 @@ const Activity = co.map({
action: z.literal(["watering", "planting", "harvesting", "maintenance"]),
notes: z.optional(z.string()),
});
export type Activity = co.loaded<typeof Activity>;
// Define a feed of garden activities
const ActivityFeed = co.feed(Activity);

View File

@@ -23,8 +23,10 @@ const Task = co.map({
import { co, z } from "jazz-tools";
const ListOfResources = co.list(z.string());
export type ListOfResources = co.loaded<typeof ListOfResources>;
const ListOfTasks = co.list(Task);
export type ListOfTasks = co.loaded<typeof ListOfTasks>;
```
</CodeGroup>

View File

@@ -26,6 +26,7 @@ const Project = co.map({
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
});
export type Project = co.loaded<typeof Project>;
```
</CodeGroup>
@@ -180,6 +181,53 @@ if (project.coordinator) {
```
</CodeGroup>
### Recursive references
CoMaps can reference themselves recursively:
<CodeGroup>
```ts twoslash
const Member = co.map({
name: z.string(),
});
// ---cut---
import { co, z } from "jazz-tools";
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
get subProject() {
return Project.optional();
}
});
export type Project = co.loaded<typeof Project>;
```
</CodeGroup>
When the recursive references involve more complex types, it is required to specify the getter return type:
<CodeGroup>
```ts twoslash
const Member = co.map({
name: z.string(),
});
// ---cut---
import { co, z, CoListSchema } from "jazz-tools";
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
get subProjects(): z.ZodOptional<CoListSchema<typeof Project>> {
return z.optional(co.list(Project));
}
});
export type Project = co.loaded<typeof Project>;
```
</CodeGroup>
### Working with Record CoMaps
For record-type CoMaps, you can access values using bracket notation:
@@ -401,6 +449,7 @@ export const Task = z.discriminatedUnion("version", [
TaskV1,
TaskV2,
]);
export type Task = co.loaded<typeof Task>;
```
</CodeGroup>

View File

@@ -16,6 +16,7 @@ export const Location = co.map({
city: z.string(),
country: z.string(),
});
export type Location = co.loaded<typeof Location>;
// co.ref can be used within CoMap fields to point to other CoValues
const Actor = co.map({
@@ -23,6 +24,7 @@ const Actor = co.map({
imageURL: z.string,
birthplace: Location // Links directly to the Location CoMap above.
})
export type Actor = co.loaded<typeof Actor>;
// actual actor data is stored in the separate Actor CoValue
const Movie = co.map({
@@ -30,12 +32,14 @@ const Movie = co.map({
director: z.string,
cast: co.list(Actor), // ordered, mutable
})
export type Movie = co.loaded<typeof Movie>;
// A User CoMap can maintain a CoFeed of co.ref(Movie) to track their favorite movies
const User = co.map({
username: z.string,
favoriteMovies: co.feed(Movie), // append-only
})
export type User = co.loaded<typeof User>;
```
</CodeGroup>
@@ -46,6 +50,3 @@ const User = co.map({
This direct linking approach offers a single source of truth. When you update a referenced CoValue, all other CoValues that point to it are automatically updated, ensuring data consistency across your application.
By connecting CoValues through these direct references, you can build robust and collaborative applications where data is consistent, efficient to manage, and relationships are clearly defined. The ability to link different CoValue types to the same underlying data is fundamental to building complex applications with Jazz.

View File

@@ -18,6 +18,7 @@ const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
});
export type Task = co.loaded<typeof Task>;
```
</CodeGroup>

View File

@@ -17,8 +17,8 @@ Jazz offers several tools to work with images in React Native:
For examples of use, see our example apps:
- [React Native Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn) (Framework-less implementation)
- [React Native Expo Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-expo) (Expo implementation)
- [React Native Expo Clerk Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-expo-clerk) (Expo implementation with Clerk)
- [Expo Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-expo) (Expo implementation)
- [Expo Clerk](https://github.com/gardencmp/jazz/tree/main/examples/clerk-expo) (Expo with Clerk-based authentication)
## Creating Images

View File

@@ -17,8 +17,8 @@ Jazz offers several tools to work with images in React Native:
For examples of use, see our example apps:
- [React Native Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn) (Framework-less implementation)
- [React Native Expo Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-expo) (Expo implementation)
- [React Native Expo Clerk Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-expo-clerk) (Expo implementation with Clerk)
- [Expo Chat](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-expo) (Expo implementation)
- [Expo Clerk](https://github.com/gardencmp/jazz/tree/main/examples/clerk-expo) (Expo with Clerk-based authentication)
## Creating Images

View File

@@ -57,9 +57,6 @@
"react-dom": "19.0.0",
"vite": "6.3.5",
"esbuild": "0.24.0"
},
"patchedDependencies": {
"expo-router": "patches/expo-router.patch"
}
}
}

View File

@@ -1,5 +1,13 @@
# cojson-storage-indexeddb
## 0.15.2
### Patch Changes
- Updated dependencies [4b964ed]
- cojson@0.15.2
- cojson-storage@0.15.2
## 0.15.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.15.1",
"version": "0.15.2",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",

View File

@@ -1,5 +1,13 @@
# cojson-storage-sqlite
## 0.15.2
### Patch Changes
- Updated dependencies [4b964ed]
- cojson@0.15.2
- cojson-storage@0.15.2
## 0.15.1
### Patch Changes

View File

@@ -1,13 +1,13 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.15.1",
"version": "0.15.2",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^11.7.0",
"cojson": "workspace:0.15.1",
"cojson": "workspace:0.15.2",
"cojson-storage": "workspace:*"
},
"devDependencies": {

View File

@@ -1,5 +1,12 @@
# cojson-storage
## 0.15.2
### Patch Changes
- Updated dependencies [4b964ed]
- cojson@0.15.2
## 0.15.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "cojson-storage",
"version": "0.15.1",
"version": "0.15.2",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",

View File

@@ -1,5 +1,12 @@
# cojson-transport-nodejs-ws
## 0.15.2
### Patch Changes
- Updated dependencies [4b964ed]
- cojson@0.15.2
## 0.15.1
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "cojson-transport-ws",
"type": "module",
"version": "0.15.1",
"version": "0.15.2",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",

View File

@@ -1,5 +1,11 @@
# cojson
## 0.15.2
### Patch Changes
- 4b964ed: Add debug code on transactions parsing
## 0.15.1
### Patch Changes

View File

@@ -25,7 +25,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.15.1",
"version": "0.15.2",
"devDependencies": {
"@opentelemetry/sdk-metrics": "^2.0.0",
"typescript": "catalog:"

View File

@@ -621,11 +621,19 @@ export class CoValueCore {
}
if (tx.privacy === "trusting") {
allTransactions.push({
txID,
madeAt: tx.madeAt,
changes: parseJSON(tx.changes),
});
try {
allTransactions.push({
txID,
madeAt: tx.madeAt,
changes: parseJSON(tx.changes),
});
} catch (e) {
logger.error("Failed to parse trusting transaction on " + this.id, {
err: e,
txID,
changes: tx.changes.slice(0, 50),
});
}
continue;
}
@@ -650,7 +658,17 @@ export class CoValueCore {
tx: txID,
},
);
decryptedChanges = decryptedString && parseJSON(decryptedString);
try {
decryptedChanges = decryptedString && parseJSON(decryptedString);
} catch (e) {
logger.error("Failed to parse private transaction on " + this.id, {
err: e,
txID,
changes: decryptedString?.slice(0, 50),
});
continue;
}
this._decryptionCache[tx.encryptedChanges] = decryptedChanges;
}

View File

@@ -1,4 +1,6 @@
import { encrypt } from "jazz-crypto-rs";
import { assert, afterEach, beforeEach, expect, test, vi } from "vitest";
import { bytesToBase64url } from "../base64url.js";
import { CoValueCore } from "../coValueCore/coValueCore.js";
import { Transaction } from "../coValueCore/verifiedState.js";
import { MapOpPayload } from "../coValues/coMap.js";
@@ -348,3 +350,165 @@ test("listeners are notified even if the previous listener threw an error", asyn
errorLog.mockRestore();
});
test("getValidTransactions should skip trusting transactions with invalid JSON", () => {
const [agent, sessionID] = randomAgentAndSessionID();
const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
const coValue = node.createCoValue({
type: "costream",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...Crypto.createdNowUnique(),
});
// Create a valid transaction first
const validTransaction: Transaction = {
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([{ hello: "world" }]),
};
const { expectedNewHash: expectedNewHash1 } =
coValue.verified.expectedNewHashAfter(node.currentSessionID, [
validTransaction,
]);
coValue
.tryAddTransactions(
node.currentSessionID,
[validTransaction],
expectedNewHash1,
Crypto.sign(agent.currentSignerSecret(), expectedNewHash1),
"immediate",
)
._unsafeUnwrap();
// Create an invalid transaction with malformed JSON
const invalidTransaction: Transaction = {
privacy: "trusting",
madeAt: Date.now() + 1,
changes: '{"invalid": json}' as any, // Invalid JSON string
};
const { expectedNewHash: expectedNewHash2 } =
coValue.verified.expectedNewHashAfter(node.currentSessionID, [
invalidTransaction,
]);
coValue
.tryAddTransactions(
node.currentSessionID,
[invalidTransaction],
expectedNewHash2,
Crypto.sign(agent.currentSignerSecret(), expectedNewHash2),
"immediate",
)
._unsafeUnwrap();
// Get valid transactions - should only include the valid one
const validTransactions = coValue.getValidTransactions();
expect(validTransactions).toHaveLength(1);
expect(validTransactions[0]?.changes).toEqual([{ hello: "world" }]);
});
test("getValidTransactions should skip private transactions with invalid JSON", () => {
const [agent, sessionID] = randomAgentAndSessionID();
const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
const group = node.createGroup();
group.addMember("everyone", "writer");
const coValue = node.createCoValue({
type: "costream",
ruleset: { type: "ownedByGroup", group: group.id },
meta: null,
...Crypto.createdNowUnique(),
});
const { secret: keySecret, id: keyID } = coValue.getCurrentReadKey();
assert(keySecret);
const encrypted = Crypto.encryptForTransaction(
[{ hello: "world" }],
keySecret,
{
in: coValue.id,
tx: coValue.nextTransactionID(),
},
);
// Create a valid private transaction first
const validTransaction: Transaction = {
privacy: "private",
madeAt: Date.now(),
keyUsed: keyID,
encryptedChanges: encrypted as any,
};
const { expectedNewHash: expectedNewHash1 } =
coValue.verified.expectedNewHashAfter(node.currentSessionID, [
validTransaction,
]);
coValue
.tryAddTransactions(
node.currentSessionID,
[validTransaction],
expectedNewHash1,
Crypto.sign(agent.currentSignerSecret(), expectedNewHash1),
"immediate",
)
._unsafeUnwrap();
const textEncoder = new TextEncoder();
const brokenChange = `encrypted_U${bytesToBase64url(
encrypt(
textEncoder.encode('{"invalid": json}'),
keySecret,
textEncoder.encode(
stableStringify({
in: coValue.id,
tx: coValue.nextTransactionID(),
}),
),
),
)}`;
// Create an invalid private transaction with malformed JSON after decryption
const invalidTransaction: Transaction = {
privacy: "private",
madeAt: Date.now() + 1,
keyUsed: keyID,
encryptedChanges: brokenChange as any,
};
const { expectedNewHash: expectedNewHash2 } =
coValue.verified.expectedNewHashAfter(node.currentSessionID, [
invalidTransaction,
]);
coValue
.tryAddTransactions(
node.currentSessionID,
[invalidTransaction],
expectedNewHash2,
Crypto.sign(agent.currentSignerSecret(), expectedNewHash2),
"immediate",
)
._unsafeUnwrap();
// Get valid transactions - should skip the invalid one
const validTransactions = coValue.getValidTransactions({
ignorePrivateTransactions: false,
});
// Since we can't easily create valid private transactions in this test setup,
// we just verify that the method doesn't crash and handles the invalid JSON gracefully
expect(validTransactions).toBeDefined();
expect(Array.isArray(validTransactions)).toBe(true);
expect(validTransactions.length).toBe(1);
expect(validTransactions[0]?.changes).toEqual([{ hello: "world" }]);
});

View File

@@ -1,5 +1,15 @@
# jazz-auth-betterauth
## 0.15.2
### Patch Changes
- Updated dependencies [4b964ed]
- Updated dependencies [0e7e532]
- cojson@0.15.2
- jazz-tools@0.15.2
- jazz-betterauth-client-plugin@0.15.2
## 0.15.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-auth-betterauth",
"version": "0.15.1",
"version": "0.15.2",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,5 +1,11 @@
# jazz-betterauth-client-plugin
## 0.15.2
### Patch Changes
- jazz-betterauth-server-plugin@0.15.2
## 0.15.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-betterauth-client-plugin",
"version": "0.15.1",
"version": "0.15.2",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,5 +1,14 @@
# jazz-betterauth-server-plugin
## 0.15.2
### Patch Changes
- Updated dependencies [4b964ed]
- Updated dependencies [0e7e532]
- cojson@0.15.2
- jazz-tools@0.15.2
## 0.15.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-betterauth-server-plugin",
"version": "0.15.1",
"version": "0.15.2",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,5 +1,16 @@
# jazz-react-auth-betterauth
## 0.15.2
### Patch Changes
- Updated dependencies [4b964ed]
- Updated dependencies [0e7e532]
- cojson@0.15.2
- jazz-tools@0.15.2
- jazz-auth-betterauth@0.15.2
- jazz-betterauth-client-plugin@0.15.2
## 0.15.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-react-auth-betterauth",
"version": "0.15.1",
"version": "0.15.2",
"type": "module",
"main": "dist/index.js",
"types": "src/index.tsx",

View File

@@ -1,5 +1,16 @@
# jazz-run
## 0.15.2
### Patch Changes
- Updated dependencies [4b964ed]
- Updated dependencies [0e7e532]
- cojson@0.15.2
- jazz-tools@0.15.2
- cojson-storage-sqlite@0.15.2
- cojson-transport-ws@0.15.2
## 0.15.1
### Patch Changes

View File

@@ -3,7 +3,7 @@
"bin": "./dist/index.js",
"type": "module",
"license": "MIT",
"version": "0.15.1",
"version": "0.15.2",
"exports": {
"./startSyncServer": {
"import": "./dist/startSyncServer.js",
@@ -28,11 +28,11 @@
"@effect/printer-ansi": "^0.34.5",
"@effect/schema": "^0.71.1",
"@effect/typeclass": "^0.25.5",
"cojson": "workspace:0.15.1",
"cojson-storage-sqlite": "workspace:0.15.1",
"cojson-transport-ws": "workspace:0.15.1",
"cojson": "workspace:0.15.2",
"cojson-storage-sqlite": "workspace:0.15.2",
"cojson-transport-ws": "workspace:0.15.2",
"effect": "^3.6.5",
"jazz-tools": "workspace:0.15.1",
"jazz-tools": "workspace:0.15.2",
"ws": "^8.14.2"
},
"devDependencies": {

Some files were not shown because too many files have changed in this diff Show More