From 9f872c9a382f744f15085d532d602cf39992e014 Mon Sep 17 00:00:00 2001 From: Michael Murray Date: Wed, 27 Nov 2024 13:55:44 -0500 Subject: [PATCH 1/2] fix: trigger onAuthenticated on first mount --- .../basic-nextjs/src/app/authors/page.tsx | 2 + examples/basic/package-lock.json | 103 +++++++++++++++++- examples/basic/package.json | 3 +- examples/basic/src/App.tsx | 2 + examples/basic/src/main.tsx | 22 +++- examples/remix/app/entry.server.tsx | 2 +- examples/remix/app/root.tsx | 2 +- examples/remix/app/routes/_index.tsx | 2 +- examples/remix/app/routes/books/index.tsx | 2 +- examples/remix/app/routes/profile/index.tsx | 2 +- src/context/RowndContext.tsx | 34 +++++- src/context/types.ts | 2 +- src/next/client/useRownd.tsx | 14 ++- src/remix/client/useRownd.tsx | 14 ++- 14 files changed, 190 insertions(+), 16 deletions(-) diff --git a/examples/basic-nextjs/src/app/authors/page.tsx b/examples/basic-nextjs/src/app/authors/page.tsx index 75c80c9..003b351 100644 --- a/examples/basic-nextjs/src/app/authors/page.tsx +++ b/examples/basic-nextjs/src/app/authors/page.tsx @@ -4,6 +4,7 @@ import Fallback from '@/components/Fallback'; import { getRowndUser } from '../../../../../src/next/server'; import { withRowndRequireSignIn } from '../../../../../src/next'; import { cookies } from 'next/headers'; +import Link from 'next/link'; async function Authors() { const data = await fetch('https://jsonplaceholder.typicode.com/posts'); @@ -14,6 +15,7 @@ async function Authors() {

Authors

User ID: {JSON.stringify(user, null, 2)}

+ Home
    {posts diff --git a/examples/basic/package-lock.json b/examples/basic/package-lock.json index 245ccdf..79857d9 100644 --- a/examples/basic/package-lock.json +++ b/examples/basic/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "@rownd/react": "file:../../packages/react/rownd-react-2.2.2.tgz", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^7.0.1" }, "devDependencies": { "@types/react": "^18.2.15", @@ -747,6 +748,11 @@ "integrity": "sha512-z/G02d+59gyyUb7KYhKi9jOhicek6QD2oMaotUyG+lUkybpXoV49dY9bj7Ah5Q+y7knK2jU67UTX9FyfGzaxQg==", "dev": true }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -1145,6 +1151,14 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2111,6 +2125,44 @@ "react": "^18.2.0" } }, + "node_modules/react-router": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.0.1.tgz", + "integrity": "sha512-WVAhv9oWCNsja5AkK6KLpXJDSJCQizOIyOd4vvB/+eHGbYx5vkhcmcmwWjQ9yqkRClogi+xjEg9fNEOd5EX/tw==", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.0.1.tgz", + "integrity": "sha512-duBzwAAiIabhFPZfDjcYpJ+f08TMbPMETgq254GWne2NW1ZwRHhZLj7tpSp8KGb7JvZzlLcjGUnqLxpZQVEPng==", + "dependencies": { + "react-router": "7.0.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2207,6 +2259,11 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2312,6 +2369,11 @@ "typescript": ">=4.2.0" } }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2801,6 +2863,11 @@ "integrity": "sha512-z/G02d+59gyyUb7KYhKi9jOhicek6QD2oMaotUyG+lUkybpXoV49dY9bj7Ah5Q+y7knK2jU67UTX9FyfGzaxQg==", "dev": true }, + "@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -3068,6 +3135,11 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==" + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3766,6 +3838,25 @@ "scheduler": "^0.23.0" } }, + "react-router": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.0.1.tgz", + "integrity": "sha512-WVAhv9oWCNsja5AkK6KLpXJDSJCQizOIyOd4vvB/+eHGbYx5vkhcmcmwWjQ9yqkRClogi+xjEg9fNEOd5EX/tw==", + "requires": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + } + }, + "react-router-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.0.1.tgz", + "integrity": "sha512-duBzwAAiIabhFPZfDjcYpJ+f08TMbPMETgq254GWne2NW1ZwRHhZLj7tpSp8KGb7JvZzlLcjGUnqLxpZQVEPng==", + "requires": { + "react-router": "7.0.1" + } + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3822,6 +3913,11 @@ "lru-cache": "^6.0.0" } }, + "set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3895,6 +3991,11 @@ "dev": true, "requires": {} }, + "turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/examples/basic/package.json b/examples/basic/package.json index 8e4233b..7cea34f 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -12,7 +12,8 @@ "dependencies": { "@rownd/react": "file:../../packages/react/rownd-react-2.2.2.tgz", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^7.0.1" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/examples/basic/src/App.tsx b/examples/basic/src/App.tsx index 131608a..de7383c 100644 --- a/examples/basic/src/App.tsx +++ b/examples/basic/src/App.tsx @@ -4,6 +4,7 @@ import { useRownd, } from '../../../src/context/index'; import './App.css'; +import { Link } from 'react-router-dom'; const Initializing = () =>

    Initializing...

    ; @@ -23,6 +24,7 @@ function App() { return ( }>

    Rownd sample app

    + Go to profile
    ); diff --git a/examples/basic/src/main.tsx b/examples/basic/src/main.tsx index 5cc3bc7..5350c5a 100644 --- a/examples/basic/src/main.tsx +++ b/examples/basic/src/main.tsx @@ -3,11 +3,31 @@ import ReactDOM from 'react-dom/client'; import App from './App.tsx'; import './index.css'; import { RowndProvider } from '../../../src/context/index'; +import { + createBrowserRouter, + Link, + RouterProvider, +} from "react-router-dom"; + +const Profile: React.FC = () => { + return Go back; +}; + +const router = createBrowserRouter([ + { + path: "/", + element: , + }, + { + path: "/profile", + element: , + } +]); ReactDOM.createRoot(document.getElementById('root')!).render( - + ); diff --git a/examples/remix/app/entry.server.tsx b/examples/remix/app/entry.server.tsx index 3f41f89..e2181d5 100644 --- a/examples/remix/app/entry.server.tsx +++ b/examples/remix/app/entry.server.tsx @@ -11,7 +11,7 @@ import { createReadableStreamFromReadable } from '@remix-run/node'; import { RemixServer } from '@remix-run/react'; import { isbot } from 'isbot'; import { renderToPipeableStream } from 'react-dom/server'; -import { withRowndHandleRequest } from '../../../src/remix'; +import { withRowndHandleRequest } from '../../../src/remix/index'; const ABORT_DELAY = 5_000; diff --git a/examples/remix/app/root.tsx b/examples/remix/app/root.tsx index 099435a..beec748 100644 --- a/examples/remix/app/root.tsx +++ b/examples/remix/app/root.tsx @@ -9,7 +9,7 @@ import { import type { LinksFunction } from '@remix-run/node'; import './tailwind.css'; -import { RemixRowndProvider } from '../../../src/remix'; +import { RemixRowndProvider } from '../../../src/remix/index'; export const loader = async (): Promise<{ env: { diff --git a/examples/remix/app/routes/_index.tsx b/examples/remix/app/routes/_index.tsx index 9c80dd0..2ff3332 100644 --- a/examples/remix/app/routes/_index.tsx +++ b/examples/remix/app/routes/_index.tsx @@ -1,6 +1,6 @@ import type { MetaFunction } from '@remix-run/node'; import { useNavigate } from '@remix-run/react'; -import { useRownd } from '../../../../src/remix'; +import { useRownd } from '../../../../src/remix/index'; import { useEffect } from 'react'; export const meta: MetaFunction = () => { diff --git a/examples/remix/app/routes/books/index.tsx b/examples/remix/app/routes/books/index.tsx index 31a14a6..eb522d6 100644 --- a/examples/remix/app/routes/books/index.tsx +++ b/examples/remix/app/routes/books/index.tsx @@ -4,7 +4,7 @@ import { useRownd, withRowndLoader, withRowndRequireSignIn, -} from '../../../../../src/remix'; +} from '../../../../../src/remix/index'; import { isAuthenticated, getRowndUser, getRowndUserId } from '../../../../../src/remix/server'; diff --git a/examples/remix/app/routes/profile/index.tsx b/examples/remix/app/routes/profile/index.tsx index 8043d81..c517699 100644 --- a/examples/remix/app/routes/profile/index.tsx +++ b/examples/remix/app/routes/profile/index.tsx @@ -4,7 +4,7 @@ import { useRownd, withRowndRequireSignIn, withRowndLoader, -} from '../../../../../src/remix'; +} from '../../../../../src/remix/index'; type LoaderResponse = { user_id: string; diff --git a/src/context/RowndContext.tsx b/src/context/RowndContext.tsx index 0eab78d..c48504f 100644 --- a/src/context/RowndContext.tsx +++ b/src/context/RowndContext.tsx @@ -1,5 +1,10 @@ -import React, { useContext, createContext } from 'react'; -import { TRowndContext } from './types'; +import React, { + useContext, + createContext, + useRef, + useCallback, +} from 'react'; +import { TRowndContext, Unsubscribe, UserDataContext } from './types'; export const RowndContext = createContext(undefined); @@ -25,7 +30,30 @@ function useRownd(): TRowndContext { throw new Error('useRownd must be used within a RowndProvider'); } - return context; + // If the user is authenticated on the first mount, we want to call the callback immediately + const isFirstMount = useRef(true); + const onAuthenticated = useCallback( + (callback: (userData: UserDataContext) => void): Unsubscribe => { + if ( + context.is_authenticated && + !context.is_initializing && + context.user.data.user_id && + isFirstMount.current + ) { + callback(context.user.data); + isFirstMount.current = false; + return () => {}; + } + isFirstMount.current = false; + return context.onAuthenticated(callback); + }, + [context.is_authenticated, context.is_initializing, context.user?.data?.user_id] + ); + + return { + ...context, + onAuthenticated, + }; } export { useRownd }; diff --git a/src/context/types.ts b/src/context/types.ts index 6adf060..b7d78e4 100644 --- a/src/context/types.ts +++ b/src/context/types.ts @@ -1,6 +1,6 @@ type AuthLevel = 'instant' | 'guest' | 'unverified' | 'verified'; -type Unsubscribe = () => void; +export type Unsubscribe = () => void; export type TRowndContext = { requestSignIn: (e?: SignInProps) => void; diff --git a/src/next/client/useRownd.tsx b/src/next/client/useRownd.tsx index 49ace80..4c92cc3 100644 --- a/src/next/client/useRownd.tsx +++ b/src/next/client/useRownd.tsx @@ -1,17 +1,27 @@ +'use client'; + import { useStore } from './store/useStore'; import { store } from './store'; import { TRowndContext, UserDataContext } from '../../context/types'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useRef } from 'react'; import { setCookie } from '../../ssr/server/cookie'; import { addOnAuthenticatedListener, unsubscribeOnAuthenticatedListener } from '../../utils/listeners'; export const useRownd = (): TRowndContext => { const state = useStore(store, (x) => x); + const isFirstMount = useRef(true); const onAuthenticated: ( callback: (userData: UserDataContext) => void ) => () => void = useCallback( (callback: (userData: UserDataContext) => void) => { + if (state.is_authenticated && !state.is_initializing && Boolean(state.user.data.user_id) && isFirstMount.current) { + callback(state.user.data); + isFirstMount.current = false; + return () => {}; + } + + isFirstMount.current = false; const id = addOnAuthenticatedListener(callback); const unsubscribe = () => { @@ -20,7 +30,7 @@ export const useRownd = (): TRowndContext => { return unsubscribe; }, - [] + [state.is_authenticated, state.is_initializing, state.user?.data?.user_id] ); const memoized = useMemo(() => { diff --git a/src/remix/client/useRownd.tsx b/src/remix/client/useRownd.tsx index 490d825..1656d8d 100644 --- a/src/remix/client/useRownd.tsx +++ b/src/remix/client/useRownd.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useRef } from 'react'; import { TRowndContext, UserDataContext } from '../../context/types'; import { useRownd as useRowndDefault } from '../../index'; import { setCookie } from '../../ssr/server/cookie'; @@ -7,10 +7,20 @@ import { addOnAuthenticatedListener, unsubscribeOnAuthenticatedListener } from ' const useRownd = (): TRowndContext => { const rowndDefault = useRowndDefault(); + const isFirstMount = useRef(true); const onAuthenticated: ( callback: (userData: UserDataContext) => void ) => () => void = useCallback( (callback: (userData: UserDataContext) => void) => { + + // If the user is authenticated on the first mount, we want to call the callback immediately + if (rowndDefault.is_authenticated && !rowndDefault.is_initializing && Boolean(rowndDefault.user.data.user_id) && isFirstMount.current) { + callback(rowndDefault.user.data); + isFirstMount.current = false; + return () => {}; + } + isFirstMount.current = false; + const id = addOnAuthenticatedListener(callback); const unsubscribe = () => { @@ -19,7 +29,7 @@ const useRownd = (): TRowndContext => { return unsubscribe; }, - [] + [rowndDefault.is_authenticated, rowndDefault.is_initializing, rowndDefault.user.data.user_id] ); const memoized = useMemo( From cc525d0e2c60b903f21cdddeda9252f47d41c409 Mon Sep 17 00:00:00 2001 From: Michael Murray Date: Wed, 27 Nov 2024 13:56:31 -0500 Subject: [PATCH 2/2] add comment --- src/next/client/useRownd.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/next/client/useRownd.tsx b/src/next/client/useRownd.tsx index 4c92cc3..9855240 100644 --- a/src/next/client/useRownd.tsx +++ b/src/next/client/useRownd.tsx @@ -15,6 +15,8 @@ export const useRownd = (): TRowndContext => { callback: (userData: UserDataContext) => void ) => () => void = useCallback( (callback: (userData: UserDataContext) => void) => { + + // If the user is authenticated on the first mount, we want to call the callback immediately if (state.is_authenticated && !state.is_initializing && Boolean(state.user.data.user_id) && isFirstMount.current) { callback(state.user.data); isFirstMount.current = false;