Compare commits
15 Commits
cojson-sto
...
cojson-sto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d18d09e002 | ||
|
|
d983f27bbe | ||
|
|
fcf83b0da4 | ||
|
|
9231e2c22f | ||
|
|
33157ee0ad | ||
|
|
4b964edcaf | ||
|
|
a8e1726797 | ||
|
|
a6eeada331 | ||
|
|
3b38a8241c | ||
|
|
c49330c308 | ||
|
|
0c4e27c18d | ||
|
|
d8d273821e | ||
|
|
def0ca81b4 | ||
|
|
0e7e53238b | ||
|
|
fcc18e5212 |
23
examples/chat-rn-expo-clerk/.gitignore
vendored
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }} />
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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: "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}`;
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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: "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 46 KiB |
@@ -1,9 +0,0 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: [
|
||||
["babel-preset-expo", { jsxImportSource: "nativewind" }],
|
||||
"nativewind/babel",
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,2 +0,0 @@
|
||||
import "./polyfills";
|
||||
import "expo-router/entry";
|
||||
@@ -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" });
|
||||
@@ -1 +0,0 @@
|
||||
/// <reference types="nativewind/types" />
|
||||
@@ -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
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "chat-rn-expo-clerk-example-jazz@garden.co";
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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: [],
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "nativewind-env.d.ts"]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# passkey-svelte
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [0e7e532]
|
||||
- jazz-tools@0.15.2
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-svelte",
|
||||
"version": "0.0.90",
|
||||
"version": "0.0.91",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
1
examples/clerk-expo/.env.test
Normal file
@@ -0,0 +1 @@
|
||||
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_ZXZpZGVudC1kYW5lLTg5LmNsZXJrLmFjY291bnRzLmRldiQ
|
||||
44
examples/clerk-expo/.gitignore
vendored
Normal 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
|
||||
32
examples/clerk-expo/app.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
BIN
examples/clerk-expo/assets/splash-icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
9
examples/clerk-expo/index.ts
Normal 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);
|
||||
35
examples/clerk-expo/package.json
Normal 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
|
||||
}
|
||||
48
examples/clerk-expo/src/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
44
examples/clerk-expo/src/MainScreen.tsx
Normal 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",
|
||||
},
|
||||
});
|
||||
1
examples/clerk-expo/src/apiKey.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const apiKey = "chat-rn-expo-example-jazz@garden.co";
|
||||
19
examples/clerk-expo/src/auth/AuthScreen.tsx
Normal 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} />;
|
||||
}
|
||||
127
examples/clerk-expo/src/auth/SignInScreen.tsx
Normal 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",
|
||||
},
|
||||
});
|
||||
169
examples/clerk-expo/src/auth/SignUpScreen.tsx
Normal 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",
|
||||
},
|
||||
});
|
||||
9
examples/clerk-expo/src/schema.ts
Normal 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>;
|
||||
6
examples/clerk-expo/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
@@ -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 />,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -57,9 +57,6 @@
|
||||
"react-dom": "19.0.0",
|
||||
"vite": "6.3.5",
|
||||
"esbuild": "0.24.0"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"expo-router": "patches/expo-router.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# cojson-storage
|
||||
|
||||
## 0.15.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [4b964ed]
|
||||
- cojson@0.15.2
|
||||
|
||||
## 0.15.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# cojson
|
||||
|
||||
## 0.15.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4b964ed: Add debug code on transactions parsing
|
||||
|
||||
## 0.15.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -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:"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }]);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||