community-jazz-vue 0.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +2201 -0
- package/LICENSE.txt +19 -0
- package/README.md +26 -0
- package/dist/Image.vue.d.ts +26 -0
- package/dist/auth/JazzVueProviderWithClerk.d.ts +83 -0
- package/dist/auth/PasskeyAuthBasicUI.vue.d.ts +21 -0
- package/dist/auth/useClerkAuth.d.ts +2 -0
- package/dist/auth/useIsAuthenticated.d.ts +1 -0
- package/dist/auth/usePasskeyAuth.d.ts +18 -0
- package/dist/auth/usePassphraseAuth.d.ts +20 -0
- package/dist/composables.d.ts +22 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +667 -0
- package/dist/index.js.map +1 -0
- package/dist/provider-CCZVJj45.js +143 -0
- package/dist/provider-CCZVJj45.js.map +1 -0
- package/dist/provider.d.ts +172 -0
- package/dist/testing.d.ts +30 -0
- package/dist/testing.js +41 -0
- package/dist/testing.js.map +1 -0
- package/dist/tests/fixtures.d.ts +1 -0
- package/dist/tests/proxyBehavior.test.d.ts +1 -0
- package/dist/tests/testUtils.d.ts +9 -0
- package/dist/tests/useAcceptInvite.test.d.ts +1 -0
- package/dist/tests/useAccount.test.d.ts +1 -0
- package/dist/tests/useCoState.test.d.ts +1 -0
- package/dist/tests/useInboxSender.test.d.ts +1 -0
- package/dist/tests/useIsAuthenticated.test.d.ts +1 -0
- package/dist/tests/usePassphraseAuth.test.d.ts +1 -0
- package/dist/utils/contextManager.d.ts +3 -0
- package/package.json +53 -0
- package/src/Image.vue +151 -0
- package/src/auth/JazzVueProviderWithClerk.ts +123 -0
- package/src/auth/PasskeyAuthBasicUI.vue +153 -0
- package/src/auth/useClerkAuth.ts +35 -0
- package/src/auth/useIsAuthenticated.ts +21 -0
- package/src/auth/usePasskeyAuth.ts +48 -0
- package/src/auth/usePassphraseAuth.ts +57 -0
- package/src/composables.ts +323 -0
- package/src/index.ts +14 -0
- package/src/provider.ts +192 -0
- package/src/testing.ts +45 -0
- package/src/tests/fixtures.ts +2050 -0
- package/src/tests/proxyBehavior.test.ts +267 -0
- package/src/tests/testUtils.ts +75 -0
- package/src/tests/useAcceptInvite.test.ts +55 -0
- package/src/tests/useAccount.test.ts +59 -0
- package/src/tests/useCoState.test.ts +175 -0
- package/src/tests/useInboxSender.test.ts +58 -0
- package/src/tests/useIsAuthenticated.test.ts +35 -0
- package/src/tests/usePassphraseAuth.test.ts +95 -0
- package/src/utils/contextManager.ts +31 -0
- package/src/vite-env.d.ts +7 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +31 -0
package/src/Image.vue
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ImageDefinition } from "jazz-tools";
|
|
3
|
+
import { highestResAvailable } from "jazz-tools/media";
|
|
4
|
+
import { onUnmounted, ref, watch, computed } from "vue";
|
|
5
|
+
import { useCoState } from "./composables.js";
|
|
6
|
+
|
|
7
|
+
export interface ImageProps {
|
|
8
|
+
/** The ID of the ImageDefinition to display */
|
|
9
|
+
imageId: string;
|
|
10
|
+
/**
|
|
11
|
+
* The desired width of the image. Can be a number in pixels or "original" to use the image's original width.
|
|
12
|
+
* When set to a number, the component will select the best available resolution and maintain aspect ratio.
|
|
13
|
+
*/
|
|
14
|
+
width?: number | "original";
|
|
15
|
+
/**
|
|
16
|
+
* The desired height of the image. Can be a number in pixels or "original" to use the image's original height.
|
|
17
|
+
* When set to a number, the component will select the best available resolution and maintain aspect ratio.
|
|
18
|
+
*/
|
|
19
|
+
height?: number | "original";
|
|
20
|
+
/** Alt text for the image */
|
|
21
|
+
alt?: string;
|
|
22
|
+
/** CSS classes to apply to the image */
|
|
23
|
+
classNames?: string;
|
|
24
|
+
/** CSS styles to apply to the image */
|
|
25
|
+
style?: string | Record<string, string>;
|
|
26
|
+
/** Loading strategy for the image */
|
|
27
|
+
loading?: "lazy" | "eager";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const props = withDefaults(defineProps<ImageProps>(), {
|
|
31
|
+
loading: "eager",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const image = useCoState(ImageDefinition, props.imageId, {});
|
|
35
|
+
let lastBestImage: [string, string] | null = null;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* For lazy loading, we use the browser's strategy for images with loading="lazy".
|
|
39
|
+
* We use an empty image, and when the browser triggers the load event, we load the best available image.
|
|
40
|
+
*/
|
|
41
|
+
const waitingLazyLoading = ref(props.loading === "lazy");
|
|
42
|
+
const lazyPlaceholder = computed(() =>
|
|
43
|
+
waitingLazyLoading.value ? URL.createObjectURL(emptyPixelBlob) : undefined,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const dimensions = computed(() => {
|
|
47
|
+
const originalWidth = image.value?.originalSize?.[0];
|
|
48
|
+
const originalHeight = image.value?.originalSize?.[1];
|
|
49
|
+
|
|
50
|
+
// Both width and height are "original"
|
|
51
|
+
if (props.width === "original" && props.height === "original") {
|
|
52
|
+
return { width: originalWidth, height: originalHeight };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Width is "original", height is a number
|
|
56
|
+
if (props.width === "original" && typeof props.height === "number") {
|
|
57
|
+
if (originalWidth && originalHeight) {
|
|
58
|
+
return {
|
|
59
|
+
width: Math.round((props.height * originalWidth) / originalHeight),
|
|
60
|
+
height: props.height,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { width: undefined, height: props.height };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Height is "original", width is a number
|
|
67
|
+
if (props.height === "original" && typeof props.width === "number") {
|
|
68
|
+
if (originalWidth && originalHeight) {
|
|
69
|
+
return {
|
|
70
|
+
width: props.width,
|
|
71
|
+
height: Math.round((props.width * originalHeight) / originalWidth),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return { width: props.width, height: undefined };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// In all other cases, use the property value:
|
|
78
|
+
return {
|
|
79
|
+
width: props.width === "original" ? originalWidth : props.width,
|
|
80
|
+
height: props.height === "original" ? originalHeight : props.height,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const src = computed(() => {
|
|
85
|
+
if (waitingLazyLoading.value) {
|
|
86
|
+
return lazyPlaceholder.value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!image.value) return undefined;
|
|
90
|
+
|
|
91
|
+
const bestImage = highestResAvailable(
|
|
92
|
+
image.value,
|
|
93
|
+
dimensions.value.width || dimensions.value.height || 9999,
|
|
94
|
+
dimensions.value.height || dimensions.value.width || 9999,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!bestImage) return image.value.placeholderDataURL;
|
|
98
|
+
if (lastBestImage?.[0] === bestImage.image.id) return lastBestImage?.[1];
|
|
99
|
+
|
|
100
|
+
const blob = bestImage.image.toBlob();
|
|
101
|
+
|
|
102
|
+
if (blob) {
|
|
103
|
+
const url = URL.createObjectURL(blob);
|
|
104
|
+
revokeObjectURL(lastBestImage?.[1]);
|
|
105
|
+
lastBestImage = [bestImage.image.id, url];
|
|
106
|
+
return url;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return image.value.placeholderDataURL;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const onThresholdReached = () => {
|
|
113
|
+
waitingLazyLoading.value = false;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Cleanup object URL on component destroy
|
|
117
|
+
onUnmounted(() => {
|
|
118
|
+
revokeObjectURL(lastBestImage?.[1]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
function revokeObjectURL(url: string | undefined) {
|
|
122
|
+
if (url && url.startsWith("blob:")) {
|
|
123
|
+
URL.revokeObjectURL(url);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const emptyPixelBlob = new Blob(
|
|
128
|
+
[
|
|
129
|
+
Uint8Array.from(
|
|
130
|
+
atob(
|
|
131
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
|
132
|
+
),
|
|
133
|
+
(c) => c.charCodeAt(0),
|
|
134
|
+
),
|
|
135
|
+
],
|
|
136
|
+
{ type: "image/png" },
|
|
137
|
+
);
|
|
138
|
+
</script>
|
|
139
|
+
|
|
140
|
+
<template>
|
|
141
|
+
<img
|
|
142
|
+
:src="src"
|
|
143
|
+
:width="dimensions.width"
|
|
144
|
+
:height="dimensions.height"
|
|
145
|
+
:alt="alt"
|
|
146
|
+
:class="classNames"
|
|
147
|
+
:style="style"
|
|
148
|
+
:loading="loading"
|
|
149
|
+
@load="waitingLazyLoading ? onThresholdReached() : undefined"
|
|
150
|
+
/>
|
|
151
|
+
</template>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Account,
|
|
3
|
+
AccountClass,
|
|
4
|
+
AnyAccountSchema,
|
|
5
|
+
CoValueFromRaw,
|
|
6
|
+
SyncConfig,
|
|
7
|
+
} from "jazz-tools";
|
|
8
|
+
import {
|
|
9
|
+
InMemoryKVStore,
|
|
10
|
+
JazzClerkAuth,
|
|
11
|
+
KvStoreContext,
|
|
12
|
+
type MinimalClerkClient,
|
|
13
|
+
} from "jazz-tools";
|
|
14
|
+
import { LocalStorageKVStore } from "jazz-tools/browser";
|
|
15
|
+
import { type PropType, defineComponent, h, onMounted, ref } from "vue";
|
|
16
|
+
import { JazzVueProvider } from "../provider.js";
|
|
17
|
+
import { useClerkAuth } from "./useClerkAuth.js";
|
|
18
|
+
|
|
19
|
+
function setupKvStore() {
|
|
20
|
+
KvStoreContext.getInstance().initialize(
|
|
21
|
+
typeof window === "undefined"
|
|
22
|
+
? new InMemoryKVStore()
|
|
23
|
+
: new LocalStorageKVStore(),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const RegisterClerkAuth = defineComponent({
|
|
28
|
+
name: "RegisterClerkAuth",
|
|
29
|
+
props: {
|
|
30
|
+
clerk: {
|
|
31
|
+
type: Object as PropType<MinimalClerkClient>,
|
|
32
|
+
required: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
setup(props, { slots }) {
|
|
36
|
+
useClerkAuth(props.clerk);
|
|
37
|
+
return () => slots.default?.();
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const JazzVueProviderWithClerk = defineComponent({
|
|
42
|
+
name: "JazzVueProviderWithClerk",
|
|
43
|
+
props: {
|
|
44
|
+
clerk: {
|
|
45
|
+
type: Object as PropType<MinimalClerkClient>,
|
|
46
|
+
required: true,
|
|
47
|
+
},
|
|
48
|
+
AccountSchema: {
|
|
49
|
+
type: Function as unknown as PropType<
|
|
50
|
+
(AccountClass<Account> & CoValueFromRaw<Account>) | AnyAccountSchema
|
|
51
|
+
>,
|
|
52
|
+
required: false,
|
|
53
|
+
},
|
|
54
|
+
guestMode: {
|
|
55
|
+
type: Boolean,
|
|
56
|
+
default: false,
|
|
57
|
+
},
|
|
58
|
+
sync: {
|
|
59
|
+
type: Object as PropType<SyncConfig>,
|
|
60
|
+
required: true,
|
|
61
|
+
},
|
|
62
|
+
storage: {
|
|
63
|
+
type: String as PropType<"indexedDB">,
|
|
64
|
+
default: undefined,
|
|
65
|
+
},
|
|
66
|
+
defaultProfileName: {
|
|
67
|
+
type: String,
|
|
68
|
+
required: false,
|
|
69
|
+
},
|
|
70
|
+
onLogOut: {
|
|
71
|
+
type: Function as PropType<() => void>,
|
|
72
|
+
required: false,
|
|
73
|
+
},
|
|
74
|
+
onAnonymousAccountDiscarded: {
|
|
75
|
+
type: Function as PropType<(anonymousAccount: any) => Promise<void>>,
|
|
76
|
+
required: false,
|
|
77
|
+
},
|
|
78
|
+
enableSSR: {
|
|
79
|
+
type: Boolean,
|
|
80
|
+
default: false,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
setup(props, { slots }) {
|
|
84
|
+
const isLoaded = ref(false);
|
|
85
|
+
|
|
86
|
+
onMounted(async () => {
|
|
87
|
+
try {
|
|
88
|
+
setupKvStore();
|
|
89
|
+
await JazzClerkAuth.initializeAuth(props.clerk);
|
|
90
|
+
isLoaded.value = true;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error("Jazz + Clerk initialization error:", error);
|
|
93
|
+
// Still render even if auth init fails
|
|
94
|
+
isLoaded.value = true;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return () => {
|
|
99
|
+
if (!isLoaded.value) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Destructure props to exclude 'clerk' which JazzVueProvider doesn't accept
|
|
104
|
+
const { clerk, ...jazzProviderProps } = props;
|
|
105
|
+
|
|
106
|
+
return h(
|
|
107
|
+
JazzVueProvider,
|
|
108
|
+
{
|
|
109
|
+
...jazzProviderProps,
|
|
110
|
+
logOutReplacement: clerk.signOut,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
default: () =>
|
|
114
|
+
h(
|
|
115
|
+
RegisterClerkAuth,
|
|
116
|
+
{ clerk },
|
|
117
|
+
{ default: () => slots.default?.() },
|
|
118
|
+
),
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="auth.state === 'signedIn'">
|
|
3
|
+
<slot />
|
|
4
|
+
</div>
|
|
5
|
+
<div v-else :style="containerStyle">
|
|
6
|
+
<div :style="cardStyle">
|
|
7
|
+
<h1 :style="headingStyle">{{ appName }}</h1>
|
|
8
|
+
|
|
9
|
+
<div v-if="error" :style="errorStyle">
|
|
10
|
+
{{ error }}
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<form @submit.prevent="handleSignUp" :style="formStyle">
|
|
14
|
+
<input
|
|
15
|
+
v-model="username"
|
|
16
|
+
type="text"
|
|
17
|
+
placeholder="Display name"
|
|
18
|
+
autocomplete="name"
|
|
19
|
+
:style="inputStyle"
|
|
20
|
+
/>
|
|
21
|
+
<button type="submit" :style="primaryButtonStyle">
|
|
22
|
+
Sign up
|
|
23
|
+
</button>
|
|
24
|
+
</form>
|
|
25
|
+
|
|
26
|
+
<button @click="handleLogIn" :style="secondaryButtonStyle">
|
|
27
|
+
Log in with existing account
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { ref } from "vue";
|
|
35
|
+
import { usePasskeyAuth } from "./usePasskeyAuth.js";
|
|
36
|
+
|
|
37
|
+
interface Props {
|
|
38
|
+
appName: string;
|
|
39
|
+
appHostname?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const props = defineProps<Props>();
|
|
43
|
+
|
|
44
|
+
const username = ref("");
|
|
45
|
+
const error = ref<string | null>(null);
|
|
46
|
+
|
|
47
|
+
const auth = usePasskeyAuth({
|
|
48
|
+
appName: props.appName,
|
|
49
|
+
appHostname: props.appHostname,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function handleError(err: Error) {
|
|
53
|
+
if (err.cause instanceof Error) {
|
|
54
|
+
error.value = err.cause.message;
|
|
55
|
+
} else {
|
|
56
|
+
error.value = err.message;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handleSignUp = async () => {
|
|
61
|
+
if (!username.value.trim()) {
|
|
62
|
+
error.value = "Name is required";
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
error.value = null;
|
|
67
|
+
try {
|
|
68
|
+
await auth.value.signUp(username.value.trim());
|
|
69
|
+
} catch (err) {
|
|
70
|
+
handleError(err as Error);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleLogIn = async () => {
|
|
75
|
+
error.value = null;
|
|
76
|
+
try {
|
|
77
|
+
await auth.value.logIn();
|
|
78
|
+
} catch (err) {
|
|
79
|
+
handleError(err as Error);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Styles (matching React version)
|
|
84
|
+
const containerStyle = {
|
|
85
|
+
width: "100vw",
|
|
86
|
+
height: "100vh",
|
|
87
|
+
display: "flex",
|
|
88
|
+
alignItems: "center",
|
|
89
|
+
justifyContent: "center",
|
|
90
|
+
backgroundColor: "#f3f4f6",
|
|
91
|
+
padding: "1rem",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const cardStyle = {
|
|
95
|
+
backgroundColor: "white",
|
|
96
|
+
padding: "2rem",
|
|
97
|
+
borderRadius: "0.5rem",
|
|
98
|
+
boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
|
|
99
|
+
width: "100%",
|
|
100
|
+
maxWidth: "18rem",
|
|
101
|
+
display: "flex",
|
|
102
|
+
flexDirection: "column" as const,
|
|
103
|
+
gap: "2rem",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const headingStyle = {
|
|
107
|
+
fontSize: "1.5rem",
|
|
108
|
+
fontWeight: "bold",
|
|
109
|
+
textAlign: "center" as const,
|
|
110
|
+
margin: "0",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const errorStyle = {
|
|
114
|
+
color: "red",
|
|
115
|
+
fontSize: "0.875rem",
|
|
116
|
+
textAlign: "center" as const,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const formStyle = {
|
|
120
|
+
display: "flex",
|
|
121
|
+
flexDirection: "column" as const,
|
|
122
|
+
gap: "0.5rem",
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const inputStyle = {
|
|
126
|
+
padding: "0.75rem",
|
|
127
|
+
border: "1px solid #d1d5db",
|
|
128
|
+
borderRadius: "0.375rem",
|
|
129
|
+
fontSize: "1rem",
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const primaryButtonStyle = {
|
|
133
|
+
backgroundColor: "#3b82f6",
|
|
134
|
+
color: "white",
|
|
135
|
+
padding: "0.75rem 1rem",
|
|
136
|
+
border: "none",
|
|
137
|
+
borderRadius: "0.375rem",
|
|
138
|
+
cursor: "pointer",
|
|
139
|
+
fontSize: "1rem",
|
|
140
|
+
fontWeight: "500",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const secondaryButtonStyle = {
|
|
144
|
+
backgroundColor: "#e5e7eb",
|
|
145
|
+
color: "#374151",
|
|
146
|
+
padding: "0.75rem 1rem",
|
|
147
|
+
border: "none",
|
|
148
|
+
borderRadius: "0.375rem",
|
|
149
|
+
cursor: "pointer",
|
|
150
|
+
fontSize: "1rem",
|
|
151
|
+
fontWeight: "500",
|
|
152
|
+
};
|
|
153
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { JazzClerkAuth, type MinimalClerkClient } from "jazz-tools";
|
|
2
|
+
import { computed, markRaw, onMounted, onUnmounted } from "vue";
|
|
3
|
+
import { useAuthSecretStorage, useJazzContext } from "../composables.js";
|
|
4
|
+
|
|
5
|
+
export function useClerkAuth(clerk: MinimalClerkClient) {
|
|
6
|
+
const context = useJazzContext();
|
|
7
|
+
const authSecretStorage = useAuthSecretStorage();
|
|
8
|
+
|
|
9
|
+
if ("guest" in context.value) {
|
|
10
|
+
throw new Error("Clerk auth is not supported in guest mode");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Create auth method similar to React's useMemo pattern
|
|
14
|
+
const authMethod = computed(() => {
|
|
15
|
+
return markRaw(
|
|
16
|
+
new JazzClerkAuth(context.value.authenticate, authSecretStorage),
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
onMounted(() => {
|
|
21
|
+
const cleanup = authMethod.value.registerListener(clerk) as
|
|
22
|
+
| (() => void)
|
|
23
|
+
| void;
|
|
24
|
+
|
|
25
|
+
onUnmounted(() => {
|
|
26
|
+
// Clerk's addListener returns a cleanup function, but the type says void
|
|
27
|
+
// Handle both cases for type safety
|
|
28
|
+
if (typeof cleanup === "function") {
|
|
29
|
+
cleanup();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return authMethod.value;
|
|
35
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { onUnmounted, ref } from "vue";
|
|
2
|
+
import { useAuthSecretStorage } from "../composables.js";
|
|
3
|
+
|
|
4
|
+
export function useIsAuthenticated() {
|
|
5
|
+
const authSecretStorage = useAuthSecretStorage();
|
|
6
|
+
const isAuthenticated = ref(authSecretStorage.isAuthenticated);
|
|
7
|
+
|
|
8
|
+
const handleUpdate = (newIsAuthenticated: boolean) => {
|
|
9
|
+
isAuthenticated.value = newIsAuthenticated;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Set up the listener immediately, not waiting for onMounted
|
|
13
|
+
// This ensures we catch auth state changes that happen before mounting
|
|
14
|
+
const cleanup = authSecretStorage.onUpdate(handleUpdate);
|
|
15
|
+
|
|
16
|
+
onUnmounted(() => {
|
|
17
|
+
cleanup();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return isAuthenticated;
|
|
21
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BrowserPasskeyAuth } from "jazz-tools/browser";
|
|
2
|
+
import { computed, markRaw } from "vue";
|
|
3
|
+
import { useAuthSecretStorage, useJazzContext } from "../composables.js";
|
|
4
|
+
import { useIsAuthenticated } from "./useIsAuthenticated.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `usePasskeyAuth` composable provides a `JazzAuth` object for passkey authentication.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const auth = usePasskeyAuth({ appName, appHostname });
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* @category Auth Providers
|
|
15
|
+
*/
|
|
16
|
+
export function usePasskeyAuth({
|
|
17
|
+
appName,
|
|
18
|
+
appHostname,
|
|
19
|
+
}: {
|
|
20
|
+
appName: string;
|
|
21
|
+
appHostname?: string;
|
|
22
|
+
}) {
|
|
23
|
+
const context = useJazzContext();
|
|
24
|
+
const authSecretStorage = useAuthSecretStorage();
|
|
25
|
+
const isAuthenticated = useIsAuthenticated();
|
|
26
|
+
|
|
27
|
+
if ("guest" in context.value) {
|
|
28
|
+
throw new Error("Passkey auth is not supported in guest mode");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const authMethod = computed(() => {
|
|
32
|
+
return markRaw(
|
|
33
|
+
new BrowserPasskeyAuth(
|
|
34
|
+
context.value.node.crypto,
|
|
35
|
+
context.value.authenticate,
|
|
36
|
+
authSecretStorage,
|
|
37
|
+
appName,
|
|
38
|
+
appHostname,
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return computed(() => ({
|
|
44
|
+
state: isAuthenticated.value ? "signedIn" : "anonymous",
|
|
45
|
+
logIn: authMethod.value.logIn,
|
|
46
|
+
signUp: authMethod.value.signUp,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { PassphraseAuth } from "jazz-tools";
|
|
2
|
+
import { computed, markRaw, ref, watchEffect } from "vue";
|
|
3
|
+
import { useAuthSecretStorage, useJazzContext } from "../composables.js";
|
|
4
|
+
import { useIsAuthenticated } from "./useIsAuthenticated.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `usePassphraseAuth` composable provides a `JazzAuth` object for passphrase authentication.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const auth = usePassphraseAuth({ wordlist });
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* @category Auth Providers
|
|
15
|
+
*/
|
|
16
|
+
export function usePassphraseAuth({ wordlist }: { wordlist: string[] }) {
|
|
17
|
+
const context = useJazzContext();
|
|
18
|
+
const authSecretStorage = useAuthSecretStorage();
|
|
19
|
+
const isAuthenticated = useIsAuthenticated();
|
|
20
|
+
|
|
21
|
+
if ("guest" in context.value) {
|
|
22
|
+
throw new Error("Passphrase auth is not supported in guest mode");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const authMethod = computed(() => {
|
|
26
|
+
return markRaw(
|
|
27
|
+
new PassphraseAuth(
|
|
28
|
+
context.value.node.crypto,
|
|
29
|
+
context.value.authenticate,
|
|
30
|
+
context.value.register,
|
|
31
|
+
authSecretStorage,
|
|
32
|
+
wordlist,
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const passphrase = ref(authMethod.value.passphrase);
|
|
38
|
+
|
|
39
|
+
watchEffect((onCleanup) => {
|
|
40
|
+
authMethod.value.loadCurrentAccountPassphrase();
|
|
41
|
+
|
|
42
|
+
const unsubscribe = authMethod.value.subscribe(() => {
|
|
43
|
+
passphrase.value = authMethod.value.passphrase;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
onCleanup(unsubscribe);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return computed(() => ({
|
|
50
|
+
state: isAuthenticated.value ? "signedIn" : "anonymous",
|
|
51
|
+
logIn: authMethod.value.logIn,
|
|
52
|
+
signUp: authMethod.value.signUp,
|
|
53
|
+
registerNewAccount: authMethod.value.registerNewAccount,
|
|
54
|
+
generateRandomPassphrase: authMethod.value.generateRandomPassphrase,
|
|
55
|
+
passphrase: passphrase.value,
|
|
56
|
+
}));
|
|
57
|
+
}
|