diff --git a/registries/marketplace/react/registry.json b/registries/marketplace/react/registry.json index 3219a96f..5b48a668 100644 --- a/registries/marketplace/react/registry.json +++ b/registries/marketplace/react/registry.json @@ -1,23 +1,33 @@ { "$schema": "https://ui.shadcn.com/schema/registry.json", - "name": "sitecore marketplace", + "name": "Sitecore Marketplace", "homepage": "https://blok.sitecore.com", "items": [ { "name": "quickstart", "type": "registry:block", - "title": "Quickstart for React SPA Sitecore marketplace apps with marketplace sdk", - "description": "A quickstart for next.js Sitecore marketplace apps with marketplace sdk", + "title": "Quickstart for React/Vite Sitecore Marketplace apps", + "description": "A quickstart for React/Vite Sitecore Marketplace apps with the Marketplace SDK", "dependencies": ["@sitecore-marketplace-sdk/client"], + "registryDependencies": [ + "https://blok.sitecore.com/r/badge.json", + "https://blok.sitecore.com/r/card.json", + "https://blok.sitecore.com/r/collapsible.json" + ], "files": [ { "path": "src/marketplace/react/components/providers/marketplace.tsx", - "type": "registry:file", - "target": "app/providers/marketplace.tsx" + "type": "registry:component", + "target": "components/providers/marketplace.tsx" }, { - "path": "src/marketplace/react/components/examples/example.tsx", - "type": "registry:file", + "path": "src/marketplace/react/components/examples/built-in-auth/application-context.tsx", + "type": "registry:component", + "target": "components/examples/built-in-auth/application-context.tsx" + }, + { + "path": "src/marketplace/react/components/examples/built-in-auth/example.tsx", + "type": "registry:page", "target": "app/page.tsx" } ] @@ -25,29 +35,76 @@ { "name": "quickstart-with-xmc", "type": "registry:block", - "title": "Examples for React SPA Sitecore marketplace apps with marketplace sdk", - "description": "Examples for next.js Sitecore marketplace apps with marketplace sdk", + "title": "Quickstart for React/Vite with XMC API examples", + "description": "A quickstart for React/Vite Sitecore Marketplace apps with XMC client-side API examples", "dependencies": ["@sitecore-marketplace-sdk/xmc"], "registryDependencies": [ - "https://blok.sitecore.com/r/marketplace/react/quickstart.json" + "https://blok.sitecore.com/r/marketplace/react/quickstart.json", + "https://blok.sitecore.com/r/button.json", + "https://blok.sitecore.com/r/alert.json", + "https://blok.sitecore.com/r/skeleton.json", + "https://blok.sitecore.com/r/separator.json" ], "files": [ { "path": "src/marketplace/react/components/providers/marketplace-w-xmc.tsx", - "type": "registry:file", + "type": "registry:component", "target": "components/providers/marketplace.tsx" }, { - "path": "src/marketplace/react/components/examples/with-xmc/list-languages.tsx", - "type": "registry:file", - "target": "components/examples/with-xmc/list-languages.tsx" + "path": "src/marketplace/react/components/examples/built-in-auth/with-xmc/list-languages.tsx", + "type": "registry:component", + "target": "components/examples/built-in-auth/with-xmc/list-languages.tsx" }, { - "path": "src/marketplace/react/components/examples/with-xmc/example.tsx", - "type": "registry:file", + "path": "src/marketplace/react/components/examples/built-in-auth/with-xmc/example.tsx", + "type": "registry:page", "target": "app/page.tsx" } ] + }, + { + "name": "quickstart-with-custom-auth", + "type": "registry:block", + "title": "Quickstart for React/Vite with Auth0 SPA", + "description": "Auth0 client-side authentication for Sitecore Marketplace apps on Vite. This is SPA auth only—no Next.js middleware, route handlers, or server sessions.", + "dependencies": ["@auth0/auth0-react"], + "registryDependencies": [ + "https://blok.sitecore.com/r/marketplace/react/quickstart.json" + ], + "files": [ + { + "path": "src/marketplace/react/vite-env.d.ts", + "type": "registry:file", + "target": "vite-env.d.ts" + }, + { + "path": "src/marketplace/react/lib/auth/env.ts", + "type": "registry:file", + "target": "lib/auth/env.ts" + }, + { + "path": "src/marketplace/react/components/providers/auth.tsx", + "type": "registry:component", + "target": "components/providers/auth.tsx" + }, + { + "path": "src/marketplace/react/components/providers/with-custom-auth.tsx", + "type": "registry:component", + "target": "components/providers/with-custom-auth.tsx" + } + ], + "envVars": { + "VITE_AUTH0_DOMAIN": "https://auth.sitecorecloud.io", + "VITE_AUTH0_AUDIENCE": "https://api-webapp.sitecorecloud.io", + "VITE_AUTH0_SCOPE": "openid profile email offline_access", + "VITE_AUTH0_CLIENT_ID": "your-marketplace-client-id", + "VITE_SITECORE_APP_ID": "your-app-id", + "VITE_APP_BASE_URL": "https://localhost:5173", + "VITE_SITECORE_ORGANIZATION_ID": "your-organization-id", + "VITE_SITECORE_TENANT_ID": "marketplace-app-tenant-id-after-installation" + }, + "docs": "Use HTTPS in development (e.g. vite-plugin-mkcert or a reverse proxy). Wrap your root with MarketplaceAppProvidersWithCustomAuth from components/providers/with-custom-auth instead of MarketplaceProvider alone. Set all VITE_* variables in .env. XMC examples in this registry use the Marketplace SDK from the browser only (quickstart-with-xmc); there is no server-side or backend-proxy block." } ] } diff --git a/src/marketplace/react/components/examples/built-in-auth/application-context.tsx b/src/marketplace/react/components/examples/built-in-auth/application-context.tsx new file mode 100644 index 00000000..034ad197 --- /dev/null +++ b/src/marketplace/react/components/examples/built-in-auth/application-context.tsx @@ -0,0 +1,44 @@ +import { useAppContext } from "@/components/providers/marketplace"; +import { Badge } from "@/components/ui/badge"; +import { CardTitle } from "@/components/ui/card"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { ChevronDown, ChevronRight } from "lucide-react"; +import { useState } from "react"; + +export const ApplicationContext = () => { + const appContext = useAppContext(); + const [isExpanded, setIsExpanded] = useState(false); + return ( + + +
+
+ + Application Context + + Client-side + SDK Built-in Auth +
+ {isExpanded ? ( + + ) : ( + + )} +
+
+ +
+          {JSON.stringify(appContext, null, 2)}
+        
+
+
+ ); +}; diff --git a/src/marketplace/react/components/examples/built-in-auth/example.tsx b/src/marketplace/react/components/examples/built-in-auth/example.tsx new file mode 100644 index 00000000..392d3ffd --- /dev/null +++ b/src/marketplace/react/components/examples/built-in-auth/example.tsx @@ -0,0 +1,20 @@ +import { ApplicationContext } from "@/components/examples/built-in-auth/application-context"; + +function Examples() { + return ( +
+
+

+ Marketplace SDK Demo +

+

+ Marketplace SDK with built-in authentication +

+
+ + +
+ ); +} + +export default Examples; diff --git a/src/marketplace/react/components/examples/built-in-auth/with-xmc/example.tsx b/src/marketplace/react/components/examples/built-in-auth/with-xmc/example.tsx new file mode 100644 index 00000000..a499550d --- /dev/null +++ b/src/marketplace/react/components/examples/built-in-auth/with-xmc/example.tsx @@ -0,0 +1,32 @@ +import { ApplicationContext } from "@/components/examples/built-in-auth/application-context"; +import { ListLanguagesFromClientSdk } from "@/components/examples/built-in-auth/with-xmc/list-languages"; +import { Separator } from "@/components/ui/separator"; + +function Examples() { + return ( +
+
+

+ Marketplace SDK Demo +

+

+ Marketplace SDK with built-in authentication and XMC client-side + examples +

+
+ + + + + +
+

Built-in Auth Examples

+
+ +
+
+
+ ); +} + +export default Examples; diff --git a/src/marketplace/react/components/examples/built-in-auth/with-xmc/list-languages.tsx b/src/marketplace/react/components/examples/built-in-auth/with-xmc/list-languages.tsx new file mode 100644 index 00000000..3d691d08 --- /dev/null +++ b/src/marketplace/react/components/examples/built-in-auth/with-xmc/list-languages.tsx @@ -0,0 +1,109 @@ +import { + useAppContext, + useMarketplaceClient, +} from "@/components/providers/marketplace"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import type { Xmapp } from "@sitecore-marketplace-sdk/xmc"; +import { useState } from "react"; + +export const ListLanguagesFromClientSdk = () => { + const client = useMarketplaceClient(); + const appContext = useAppContext(); + const [languages, setLanguages] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchLanguages = async () => { + setLoading(true); + setError(null); + + const contextId = appContext?.resourceAccess?.[0]?.context + ?.preview as string; + if (!contextId) { + setError("No context ID available"); + setLoading(false); + return; + } + + const data = { + query: { + sitecoreContextId: contextId, + }, + }; + + try { + const languagesResponse = await client.query("xmc.sites.listLanguages", { + params: data, + }); + console.log("languages from client", languagesResponse); + setLanguages(languagesResponse.data?.data ?? []); + } catch (err) { + console.log("error from client", err); + setError(err instanceof Error ? err.message : "An error occurred"); + } finally { + setLoading(false); + } + }; + + return ( + + + + Client SDK Example + Client-side + SDK Built-in Auth + + + Fetch languages using Sitecore Marketplace SDK from client component + + + + + + {error && ( + + {error} + + )} + +
+
+ Languages found: + {loading ? ( + + ) : ( + {languages?.length || 0} + )} +
+ + {languages.length > 0 && ( +
+

Language names:

+
+ {languages.map((language, index) => ( + {language.name} + ))} +
+
+ )} +
+
+
+ ); +}; diff --git a/src/marketplace/react/components/examples/example.tsx b/src/marketplace/react/components/examples/example.tsx index fb3b9be6..4d0e9e01 100644 --- a/src/marketplace/react/components/examples/example.tsx +++ b/src/marketplace/react/components/examples/example.tsx @@ -1,13 +1 @@ -import { useAppContext } from "@/components/providers/marketplace"; - -function ClientSideExamples() { - const appContext = useAppContext(); - return ( -
-
Application Context
-
{JSON.stringify(appContext)}
-
- ); -} - -export default ClientSideExamples; +export { default } from "@/components/examples/built-in-auth/example"; diff --git a/src/marketplace/react/components/examples/with-xmc/example.tsx b/src/marketplace/react/components/examples/with-xmc/example.tsx index cade93fa..b7a6b502 100644 --- a/src/marketplace/react/components/examples/with-xmc/example.tsx +++ b/src/marketplace/react/components/examples/with-xmc/example.tsx @@ -1,17 +1 @@ -import { ListLanguagesFromClientSdk } from "@/components/examples/with-xmc/list-languages"; -import { useAppContext } from "@/components/providers/marketplace"; - -function ClientSideExamples() { - const appContext = useAppContext(); - return ( -
-
Application Context
-
{JSON.stringify(appContext)}
-
-
Different ways to call XMC APIs from the browser
- -
- ); -} - -export default ClientSideExamples; +export { default } from "@/components/examples/built-in-auth/with-xmc/example"; diff --git a/src/marketplace/react/components/examples/with-xmc/list-languages.tsx b/src/marketplace/react/components/examples/with-xmc/list-languages.tsx index a4e35435..0cf82a8f 100644 --- a/src/marketplace/react/components/examples/with-xmc/list-languages.tsx +++ b/src/marketplace/react/components/examples/with-xmc/list-languages.tsx @@ -1,45 +1 @@ -import { - useAppContext, - useMarketplaceClient, -} from "@/components/providers/marketplace"; -import type { Xmapp } from "@sitecore-marketplace-sdk/xmc"; -import { useState } from "react"; - -export const ListLanguagesFromClientSdk = () => { - const client = useMarketplaceClient(); - const appContext = useAppContext(); - const [languages, setLanguages] = useState([]); - - const fetchLanguages = async () => { - const contextId = appContext?.resourceAccess?.[0]?.context - ?.preview as string; - if (!contextId) { - return; - } - - const data = { - query: { - sitecoreContextId: contextId, - }, - }; - - try { - const languagesResponse = await client.query("xmc.sites.listLanguages", { - params: data, - }); - console.log("languages from client", languagesResponse); - setLanguages(languagesResponse.data?.data ?? []); - } catch (err) { - console.log("error from client", err); - } - }; - - return ( -
-

Client API Request

- -
Languages: {languages.length}
-
{languages.map((language) => language.name)}
-
- ); -}; +export { ListLanguagesFromClientSdk } from "@/components/examples/built-in-auth/with-xmc/list-languages"; diff --git a/src/marketplace/react/components/providers/auth.tsx b/src/marketplace/react/components/providers/auth.tsx new file mode 100644 index 00000000..f7600889 --- /dev/null +++ b/src/marketplace/react/components/providers/auth.tsx @@ -0,0 +1,55 @@ +import { + type Auth0ContextInterface, + Auth0Provider, + type GetTokenSilentlyOptions, + useAuth0, + withAuthenticationRequired, +} from "@auth0/auth0-react"; +import { + getAuth0AuthorizationParams, + getAuth0ClientEnv, + getSitecoreTokenParams, +} from "@/lib/auth/env"; +import type React from "react"; + +export const WithAuth = withAuthenticationRequired( + ({ children }: { children: React.ReactNode }) => children, +); + +export const AuthProvider = ({ children }: { children: React.ReactNode }) => { + const { domain, clientId } = getAuth0ClientEnv(); + const authParams = getAuth0AuthorizationParams(); + + return ( + + {children} + + ); +}; + +export const useAuth = (): Auth0ContextInterface => { + const { getAccessTokenSilently, ...rest } = useAuth0(); + const sitecoreParams = getSitecoreTokenParams(); + + const customGetAccessTokenSilently = (options?: GetTokenSilentlyOptions) => { + return getAccessTokenSilently({ + ...options, + authorizationParams: { + ...options?.authorizationParams, + ...sitecoreParams, + }, + }); + }; + + return { + ...rest, + getAccessTokenSilently: + customGetAccessTokenSilently as Auth0ContextInterface["getAccessTokenSilently"], + }; +}; diff --git a/src/marketplace/react/components/providers/with-custom-auth.tsx b/src/marketplace/react/components/providers/with-custom-auth.tsx new file mode 100644 index 00000000..3815a71f --- /dev/null +++ b/src/marketplace/react/components/providers/with-custom-auth.tsx @@ -0,0 +1,19 @@ +import { AuthProvider } from "@/components/providers/auth"; +import { MarketplaceProvider } from "@/components/providers/marketplace"; +import type { ReactNode } from "react"; + +/** + * Wrap your Vite app root with this instead of only {@link MarketplaceProvider} + * when using the custom Auth0 SPA flow from the registry quickstart-with-custom-auth block. + */ +export function MarketplaceAppProvidersWithCustomAuth({ + children, +}: { + children: ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/src/marketplace/react/lib/auth/env.ts b/src/marketplace/react/lib/auth/env.ts new file mode 100644 index 00000000..0d2feb74 --- /dev/null +++ b/src/marketplace/react/lib/auth/env.ts @@ -0,0 +1,30 @@ +export function getAuth0ClientEnv(): { domain: string; clientId: string } { + const domain = import.meta.env.VITE_AUTH0_DOMAIN; + const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID; + if (!domain || !clientId) { + throw new Error( + "Auth0 domain and client ID are required (VITE_AUTH0_DOMAIN, VITE_AUTH0_CLIENT_ID)", + ); + } + return { domain, clientId }; +} + +export function getAuth0AuthorizationParams(): Record { + const appId = import.meta.env.VITE_SITECORE_APP_ID; + return { + organization_id: import.meta.env.VITE_SITECORE_ORGANIZATION_ID, + tenant_id: import.meta.env.VITE_SITECORE_TENANT_ID, + product_codes: appId ? `mkp_${appId}` : undefined, + audience: import.meta.env.VITE_AUTH0_AUDIENCE, + redirect_uri: import.meta.env.VITE_APP_BASE_URL, + scope: import.meta.env.VITE_AUTH0_SCOPE, + }; +} + +/** Passed through to silent token calls (organization / tenant). */ +export function getSitecoreTokenParams(): Record { + return { + organization_id: import.meta.env.VITE_SITECORE_ORGANIZATION_ID, + tenant_id: import.meta.env.VITE_SITECORE_TENANT_ID, + }; +} diff --git a/src/marketplace/react/vite-env.d.ts b/src/marketplace/react/vite-env.d.ts new file mode 100644 index 00000000..70453f91 --- /dev/null +++ b/src/marketplace/react/vite-env.d.ts @@ -0,0 +1,16 @@ +/// + +interface ImportMetaEnv { + readonly VITE_AUTH0_DOMAIN?: string; + readonly VITE_AUTH0_CLIENT_ID?: string; + readonly VITE_AUTH0_AUDIENCE?: string; + readonly VITE_AUTH0_SCOPE?: string; + readonly VITE_SITECORE_APP_ID?: string; + readonly VITE_APP_BASE_URL?: string; + readonly VITE_SITECORE_ORGANIZATION_ID?: string; + readonly VITE_SITECORE_TENANT_ID?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +}