Shipnoted

Docs

Remix

Works the same in React Router 7. The widget in one tag, the changelog from a loader, and verified customers from your session.

  1. Create a project

    Sign in, create a project, and note its slug.

  2. Add the widget

    npx shipnoted init

    It edits app/root.tsx. By hand, put the tag next to <Scripts />:

    app/root.tsx

    export function Layout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <head><Meta /><Links /></head>
          <body>
            {children}
            <ScrollRestoration />
            <script src="https://shipnoted.com/widget.js" data-project="your-slug" async />
            <Scripts />
          </body>
        </html>
      )
    }

A changelog route#

Load the first page in the loader, so the page arrives rendered, then let the hook handle “older releases”.

npm i @shipnoted/react

app/routes/changelog.tsx

import { useLoaderData } from "@remix-run/react"
import { createPublicClient } from "@shipnoted/react/core"
import { ShipnotedProvider, useChangelog } from "@shipnoted/react"

export async function loader() {
  const client = createPublicClient({ project: "your-slug", baseUrl: "https://shipnoted.com" })
  return { releases: (await client.releases({ limit: 10 })).releases }
}

function List({ initialData }) {
  const { releases, hasMore, loadMore } = useChangelog({ initialData })
  return (
    <>
      {releases.map((r) => <article key={r.id}><h2>{r.title}</h2><p>{r.summary}</p></article>)}
      {hasMore && <button onClick={loadMore}>Older</button>}
    </>
  )
}

export default function Changelog() {
  const { releases } = useLoaderData<typeof loader>()
  return <ShipnotedProvider project="your-slug" baseUrl="https://shipnoted.com"><List initialData={releases} /></ShipnotedProvider>
}

Verified customers#

Sign a token from your session in a resource route, with the identify secret from Install & connect:

app/routes/shipnoted-token.ts

import { SignJWT } from "jose"
import { getUser } from "~/session.server"

export async function loader({ request }: { request: Request }) {
  const user = await getUser(request)
  if (!user) return Response.json({ token: null })
  const token = await new SignJWT({ email: user.email, name: user.name, plan: user.plan, mrr: user.mrr, company: user.company })
    .setProtectedHeader({ alg: "HS256" })
    .setSubject(user.id)
    .setExpirationTime("1h")
    .sign(new TextEncoder().encode(process.env.SHIPNOTED_IDENTIFY_SECRET!))
  return Response.json({ token })
}
// in a useEffect on the client
fetch("/shipnoted-token").then((r) => r.json()).then(({ token }) => token && window.Shipnoted?.identify(token))