@outseta/react 0.3.7 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outseta/react",
3
- "version": "0.3.7",
3
+ "version": "0.4.1",
4
4
  "description": "React hooks and components for the Outseta API",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -18,15 +18,21 @@
18
18
  }
19
19
  },
20
20
  "dependencies": {
21
- "@tanstack/react-query": "^5.0.0",
22
- "@outseta/api-client": "0.3.6"
21
+ "@outseta/api-client": "0.3.7"
23
22
  },
24
23
  "peerDependencies": {
25
24
  "react": "^18.0.0 || ^19.0.0"
26
25
  },
27
26
  "devDependencies": {
27
+ "@testing-library/dom": "^10.4.0",
28
+ "@testing-library/react": "^16.1.0",
28
29
  "@types/react": "^19.0.0",
29
- "typescript": "^5.7.0"
30
+ "@types/react-dom": "^19.0.0",
31
+ "jsdom": "^26.0.0",
32
+ "react": "19.2.7",
33
+ "react-dom": "19.2.7",
34
+ "typescript": "^5.7.0",
35
+ "vitest": "^3.2.0"
30
36
  },
31
37
  "sideEffects": false,
32
38
  "files": [
@@ -40,6 +46,8 @@
40
46
  "scripts": {
41
47
  "build": "rimraf dist && bun build src/index.ts --outdir dist --target browser --format esm --packages external",
42
48
  "typecheck": "tsc --noEmit",
43
- "lint": "eslint src --ignore-pattern generated"
49
+ "lint": "eslint src --ignore-pattern generated",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest"
44
52
  }
45
53
  }
@@ -0,0 +1,33 @@
1
+ import type { ButtonHTMLAttributes } from "react";
2
+ import { useOutseta } from "./OutsetaProvider.js";
3
+ import type { AuthWidgetOptions } from "./types.js";
4
+ import { composeButtonClick } from "./utils.js";
5
+
6
+ export interface AuthButtonProps
7
+ extends ButtonHTMLAttributes<HTMLButtonElement> {
8
+ /** Options passed to `Outseta.auth.open`. */
9
+ options?: AuthWidgetOptions;
10
+ }
11
+
12
+ /**
13
+ * Unstyled button that opens the Outseta auth widget.
14
+ */
15
+ export function AuthButton({
16
+ children = "Continue",
17
+ options,
18
+ onClick,
19
+ type = "button",
20
+ ...props
21
+ }: AuthButtonProps) {
22
+ const { openAuth } = useOutseta();
23
+
24
+ return (
25
+ <button
26
+ type={type}
27
+ {...props}
28
+ onClick={composeButtonClick(onClick, () => openAuth(options))}
29
+ >
30
+ {children}
31
+ </button>
32
+ );
33
+ }
@@ -5,6 +5,8 @@ export interface AuthEmbedProps extends HTMLAttributes<HTMLDivElement> {
5
5
  widgetMode?: "login" | "register" | "login|register";
6
6
  /** Registration defaults as a JSON string or object */
7
7
  registrationDefaults?: string | Record<string, unknown>;
8
+ /** Props applied to the element Outseta mounts into. */
9
+ widgetProps?: HTMLAttributes<HTMLDivElement>;
8
10
  }
9
11
 
10
12
  /**
@@ -16,6 +18,7 @@ export interface AuthEmbedProps extends HTMLAttributes<HTMLDivElement> {
16
18
  export function AuthEmbed({
17
19
  widgetMode = "login|register",
18
20
  registrationDefaults,
21
+ widgetProps,
19
22
  ...props
20
23
  }: AuthEmbedProps) {
21
24
  const defaults =
@@ -28,6 +31,7 @@ export function AuthEmbed({
28
31
  return (
29
32
  <div key={widgetMode} {...props}>
30
33
  <div
34
+ {...widgetProps}
31
35
  data-o-auth="1"
32
36
  data-mode="embed"
33
37
  data-widget-mode={widgetMode}
@@ -0,0 +1,38 @@
1
+ import type { ButtonHTMLAttributes } from "react";
2
+ import { useOutseta } from "./OutsetaProvider.js";
3
+ import type { EmailListWidgetOptions } from "./types.js";
4
+ import { composeButtonClick } from "./utils.js";
5
+
6
+ export interface EmailListButtonProps
7
+ extends ButtonHTMLAttributes<HTMLButtonElement> {
8
+ /** The Outseta email list UID to subscribe to. */
9
+ emailListUid: string;
10
+ /** Options passed to `Outseta.emailList.open`. */
11
+ options?: Omit<EmailListWidgetOptions, "emailListUid">;
12
+ }
13
+
14
+ /**
15
+ * Unstyled button that opens the Outseta email list widget.
16
+ */
17
+ export function EmailListButton({
18
+ children = "Subscribe",
19
+ emailListUid,
20
+ options,
21
+ onClick,
22
+ type = "button",
23
+ ...props
24
+ }: EmailListButtonProps) {
25
+ const { openEmailList } = useOutseta();
26
+
27
+ return (
28
+ <button
29
+ type={type}
30
+ {...props}
31
+ onClick={composeButtonClick(onClick, () =>
32
+ openEmailList({ mode: "popup", ...options, emailListUid }),
33
+ )}
34
+ >
35
+ {children}
36
+ </button>
37
+ );
38
+ }
@@ -0,0 +1,28 @@
1
+ import type { HTMLAttributes } from "react";
2
+
3
+ export interface EmailListEmbedProps extends HTMLAttributes<HTMLDivElement> {
4
+ /** The Outseta email list UID to subscribe to. */
5
+ emailListUid: string;
6
+ /** Props applied to the element Outseta mounts into. */
7
+ widgetProps?: HTMLAttributes<HTMLDivElement>;
8
+ }
9
+
10
+ /**
11
+ * Renders an embedded Outseta email list widget.
12
+ */
13
+ export function EmailListEmbed({
14
+ emailListUid,
15
+ widgetProps,
16
+ ...props
17
+ }: EmailListEmbedProps) {
18
+ return (
19
+ <div key={emailListUid} {...props}>
20
+ <div
21
+ {...widgetProps}
22
+ data-o-email-list="1"
23
+ data-mode="embed"
24
+ data-email-list-uid={emailListUid}
25
+ />
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,34 @@
1
+ import type { FormHTMLAttributes } from "react";
2
+
3
+ export interface EmailListFormProps extends FormHTMLAttributes<HTMLFormElement> {
4
+ /** Your Outseta domain, e.g. "your-company.outseta.com". */
5
+ domain: string;
6
+ /** The Outseta email list UID to subscribe to. */
7
+ emailListUid: string;
8
+ }
9
+
10
+ function getSubscribeAction(domain: string, emailListUid: string): string {
11
+ const hasProtocol = domain.startsWith("http://") || domain.startsWith("https://");
12
+ const baseUrl = hasProtocol ? domain : `https://${domain}`;
13
+ return `${baseUrl.replace(/\/$/, "")}/email/lists/${emailListUid}/subscribe`;
14
+ }
15
+
16
+ /**
17
+ * Headless native form wrapper for Outseta email list subscriptions.
18
+ * Provide your own fields and styling; Outseta expects an `Email` input.
19
+ */
20
+ export function EmailListForm({
21
+ domain,
22
+ emailListUid,
23
+ action,
24
+ method = "post",
25
+ ...props
26
+ }: EmailListFormProps) {
27
+ return (
28
+ <form
29
+ action={action ?? getSubscribeAction(domain, emailListUid)}
30
+ method={method}
31
+ {...props}
32
+ />
33
+ );
34
+ }
@@ -0,0 +1,38 @@
1
+ import type { ButtonHTMLAttributes } from "react";
2
+ import { useOutseta } from "./OutsetaProvider.js";
3
+ import type { LeadCaptureWidgetOptions } from "./types.js";
4
+ import { composeButtonClick } from "./utils.js";
5
+
6
+ export interface LeadCaptureButtonProps
7
+ extends ButtonHTMLAttributes<HTMLButtonElement> {
8
+ /** The Outseta lead capture form UID to show. */
9
+ formUid: string;
10
+ /** Options passed to `Outseta.leadCapture.open`. */
11
+ options?: Omit<LeadCaptureWidgetOptions, "formUid">;
12
+ }
13
+
14
+ /**
15
+ * Unstyled button that opens the Outseta lead capture widget.
16
+ */
17
+ export function LeadCaptureButton({
18
+ children = "Get in touch",
19
+ formUid,
20
+ options,
21
+ onClick,
22
+ type = "button",
23
+ ...props
24
+ }: LeadCaptureButtonProps) {
25
+ const { openLeadCapture } = useOutseta();
26
+
27
+ return (
28
+ <button
29
+ type={type}
30
+ {...props}
31
+ onClick={composeButtonClick(onClick, () =>
32
+ openLeadCapture({ mode: "popup", ...options, formUid }),
33
+ )}
34
+ >
35
+ {children}
36
+ </button>
37
+ );
38
+ }
@@ -0,0 +1,28 @@
1
+ import type { HTMLAttributes } from "react";
2
+
3
+ export interface LeadCaptureEmbedProps extends HTMLAttributes<HTMLDivElement> {
4
+ /** The Outseta lead capture form UID to show. */
5
+ formUid: string;
6
+ /** Props applied to the element Outseta mounts into. */
7
+ widgetProps?: HTMLAttributes<HTMLDivElement>;
8
+ }
9
+
10
+ /**
11
+ * Renders an embedded Outseta lead capture widget.
12
+ */
13
+ export function LeadCaptureEmbed({
14
+ formUid,
15
+ widgetProps,
16
+ ...props
17
+ }: LeadCaptureEmbedProps) {
18
+ return (
19
+ <div key={formUid} {...props}>
20
+ <div
21
+ {...widgetProps}
22
+ data-o-lead-capture="1"
23
+ data-mode="embed"
24
+ data-form-uid={formUid}
25
+ />
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,18 @@
1
+ import { AuthButton, type AuthButtonProps } from "./AuthButton.js";
2
+
3
+ export type LoginButtonProps = AuthButtonProps;
4
+
5
+ /**
6
+ * Unstyled button that opens the Outseta auth widget in login mode.
7
+ */
8
+ export function LoginButton({
9
+ children = "Log in",
10
+ options,
11
+ ...props
12
+ }: LoginButtonProps) {
13
+ return (
14
+ <AuthButton options={{ ...options, widgetMode: "login" }} {...props}>
15
+ {children}
16
+ </AuthButton>
17
+ );
18
+ }
@@ -1,17 +1,24 @@
1
1
  import type { ButtonHTMLAttributes } from "react";
2
2
  import { useOutseta } from "./OutsetaProvider.js";
3
+ import { composeButtonClick } from "./utils.js";
3
4
 
4
5
  /**
5
6
  * Button that logs the user out of Outseta.
6
7
  */
7
8
  export function LogoutButton({
8
9
  children = "Logout",
10
+ onClick,
11
+ type = "button",
9
12
  ...props
10
13
  }: ButtonHTMLAttributes<HTMLButtonElement>) {
11
14
  const { logout } = useOutseta();
12
15
 
13
16
  return (
14
- <button onClick={logout} {...props}>
17
+ <button
18
+ type={type}
19
+ {...props}
20
+ onClick={composeButtonClick(onClick, logout)}
21
+ >
15
22
  {children}
16
23
  </button>
17
24
  );
@@ -8,18 +8,25 @@ import {
8
8
  } from "react";
9
9
  import type {
10
10
  AuthWidgetOptions,
11
+ EmailListWidgetOptions,
12
+ LeadCaptureWidgetOptions,
11
13
  OutsetaEmbed,
12
14
  OutsetaUser,
13
15
  ProfileWidgetOptions,
16
+ SupportWidgetOptions,
14
17
  } from "./types.js";
15
18
 
16
19
  export interface OutsetaContextValue {
17
20
  user: OutsetaUser | null;
18
21
  isLoading: boolean;
19
22
  logout: () => void;
23
+ openAuth: (options?: AuthWidgetOptions) => void;
20
24
  openLogin: (options?: AuthWidgetOptions) => void;
21
25
  openSignup: (options?: AuthWidgetOptions) => void;
22
26
  openProfile: (options?: ProfileWidgetOptions) => void;
27
+ openSupport: (options?: SupportWidgetOptions) => void;
28
+ openEmailList: (options: EmailListWidgetOptions) => void;
29
+ openLeadCapture: (options: LeadCaptureWidgetOptions) => void;
23
30
  }
24
31
 
25
32
  const OutsetaContext = createContext<OutsetaContextValue | null>(null);
@@ -39,6 +46,11 @@ function getOutseta(): OutsetaEmbed | null {
39
46
  return null;
40
47
  }
41
48
 
49
+ function getCurrentUrl(): string | undefined {
50
+ if (typeof window === "undefined") return undefined;
51
+ return window.location.href;
52
+ }
53
+
42
54
  export function OutsetaProvider({ children }: { children: ReactNode }) {
43
55
  const [status, setStatus] = useState<"init" | "ready">("init");
44
56
  const [user, setUser] = useState<OutsetaUser | null>(null);
@@ -114,35 +126,52 @@ export function OutsetaProvider({ children }: { children: ReactNode }) {
114
126
  outseta.logout();
115
127
  };
116
128
 
117
- const openLogin = (options: AuthWidgetOptions = {}) => {
129
+ const openAuth = (options: AuthWidgetOptions = {}) => {
118
130
  outsetaRef.current?.auth.open({
119
131
  widgetMode: "login|register",
120
- authenticationCallbackUrl: window.location.href,
132
+ mode: "popup",
133
+ authenticationCallbackUrl: getCurrentUrl(),
121
134
  ...options,
122
135
  });
123
136
  };
124
137
 
138
+ const openLogin = (options: AuthWidgetOptions = {}) => {
139
+ openAuth({ ...options, widgetMode: "login" });
140
+ };
141
+
125
142
  const openSignup = (options: AuthWidgetOptions = {}) => {
126
- outsetaRef.current?.auth.open({
127
- widgetMode: "register",
128
- authenticationCallbackUrl: window.location.href,
129
- ...options,
130
- });
143
+ openAuth({ ...options, widgetMode: "register" });
131
144
  };
132
145
 
133
146
  const openProfile = (options: ProfileWidgetOptions = {}) => {
134
147
  outsetaRef.current?.profile.open({ tab: "profile", ...options });
135
148
  };
136
149
 
150
+ const openSupport = (options: SupportWidgetOptions = {}) => {
151
+ outsetaRef.current?.support?.open({ mode: "popup", ...options });
152
+ };
153
+
154
+ const openEmailList = (options: EmailListWidgetOptions) => {
155
+ outsetaRef.current?.emailList?.open({ mode: "popup", ...options });
156
+ };
157
+
158
+ const openLeadCapture = (options: LeadCaptureWidgetOptions) => {
159
+ outsetaRef.current?.leadCapture?.open({ mode: "popup", ...options });
160
+ };
161
+
137
162
  return (
138
163
  <OutsetaContext.Provider
139
164
  value={{
140
165
  user,
141
166
  isLoading: status !== "ready",
142
167
  logout,
168
+ openAuth,
143
169
  openLogin,
144
170
  openSignup,
145
171
  openProfile,
172
+ openSupport,
173
+ openEmailList,
174
+ openLeadCapture,
146
175
  }}
147
176
  >
148
177
  {children}
@@ -0,0 +1,35 @@
1
+ import type { ButtonHTMLAttributes } from "react";
2
+ import { useOutseta } from "./OutsetaProvider.js";
3
+ import type { ProfileWidgetOptions } from "./types.js";
4
+ import { composeButtonClick } from "./utils.js";
5
+
6
+ export interface ProfileButtonProps
7
+ extends ButtonHTMLAttributes<HTMLButtonElement> {
8
+ /** Options passed to `Outseta.profile.open`. */
9
+ options?: ProfileWidgetOptions;
10
+ }
11
+
12
+ /**
13
+ * Unstyled button that opens the Outseta profile widget.
14
+ */
15
+ export function ProfileButton({
16
+ children = "Profile",
17
+ options,
18
+ onClick,
19
+ type = "button",
20
+ ...props
21
+ }: ProfileButtonProps) {
22
+ const { openProfile } = useOutseta();
23
+
24
+ return (
25
+ <button
26
+ type={type}
27
+ {...props}
28
+ onClick={composeButtonClick(onClick, () =>
29
+ openProfile({ mode: "popup", ...options }),
30
+ )}
31
+ >
32
+ {children}
33
+ </button>
34
+ );
35
+ }
@@ -9,6 +9,8 @@ export interface ProfileEmbedProps extends HTMLAttributes<HTMLDivElement> {
9
9
  | "purchaseAddOn"
10
10
  | "passwordChange"
11
11
  | "teamMembers";
12
+ /** Props applied to the element Outseta mounts into. */
13
+ widgetProps?: HTMLAttributes<HTMLDivElement>;
12
14
  }
13
15
 
14
16
  /**
@@ -17,10 +19,14 @@ export interface ProfileEmbedProps extends HTMLAttributes<HTMLDivElement> {
17
19
  * Uses `key={tab}` internally so React re-mounts the widget
18
20
  * when the tab changes.
19
21
  */
20
- export function ProfileEmbed({ tab = "profile", ...props }: ProfileEmbedProps) {
22
+ export function ProfileEmbed({
23
+ tab = "profile",
24
+ widgetProps,
25
+ ...props
26
+ }: ProfileEmbedProps) {
21
27
  return (
22
28
  <div key={tab} {...props}>
23
- <div data-o-profile="1" data-mode="embed" data-tab={tab} />
29
+ <div {...widgetProps} data-o-profile="1" data-mode="embed" data-tab={tab} />
24
30
  </div>
25
31
  );
26
32
  }
@@ -1,5 +1,6 @@
1
1
  import type { ButtonHTMLAttributes } from "react";
2
2
  import { useOutseta } from "./OutsetaProvider.js";
3
+ import { composeButtonClick } from "./utils.js";
3
4
 
4
5
  export interface PurchaseAddonButtonProps
5
6
  extends ButtonHTMLAttributes<HTMLButtonElement> {
@@ -17,20 +18,23 @@ export function PurchaseAddonButton({
17
18
  addonUid,
18
19
  billingRenewalTerm = 4,
19
20
  children = "Buy Add-on",
21
+ onClick,
22
+ type = "button",
20
23
  ...props
21
24
  }: PurchaseAddonButtonProps) {
22
25
  const { openProfile } = useOutseta();
23
26
 
24
27
  return (
25
28
  <button
26
- onClick={() =>
29
+ type={type}
30
+ {...props}
31
+ onClick={composeButtonClick(onClick, () =>
27
32
  openProfile({
28
33
  mode: "popup",
29
34
  tab: "purchaseAddOn",
30
35
  stateProps: { addOnUid: addonUid, billingRenewalTerm },
31
- })
32
- }
33
- {...props}
36
+ }),
37
+ )}
34
38
  >
35
39
  {children}
36
40
  </button>
@@ -0,0 +1,18 @@
1
+ import { AuthButton, type AuthButtonProps } from "./AuthButton.js";
2
+
3
+ export type SignupButtonProps = AuthButtonProps;
4
+
5
+ /**
6
+ * Unstyled button that opens the Outseta auth widget in registration mode.
7
+ */
8
+ export function SignupButton({
9
+ children = "Sign up",
10
+ options,
11
+ ...props
12
+ }: SignupButtonProps) {
13
+ return (
14
+ <AuthButton options={{ ...options, widgetMode: "register" }} {...props}>
15
+ {children}
16
+ </AuthButton>
17
+ );
18
+ }
@@ -0,0 +1,35 @@
1
+ import type { ButtonHTMLAttributes } from "react";
2
+ import { useOutseta } from "./OutsetaProvider.js";
3
+ import type { SupportWidgetOptions } from "./types.js";
4
+ import { composeButtonClick } from "./utils.js";
5
+
6
+ export interface SupportButtonProps
7
+ extends ButtonHTMLAttributes<HTMLButtonElement> {
8
+ /** Options passed to `Outseta.support.open`. */
9
+ options?: SupportWidgetOptions;
10
+ }
11
+
12
+ /**
13
+ * Unstyled button that opens the Outseta support widget.
14
+ */
15
+ export function SupportButton({
16
+ children = "Contact support",
17
+ options,
18
+ onClick,
19
+ type = "button",
20
+ ...props
21
+ }: SupportButtonProps) {
22
+ const { openSupport } = useOutseta();
23
+
24
+ return (
25
+ <button
26
+ type={type}
27
+ {...props}
28
+ onClick={composeButtonClick(onClick, () =>
29
+ openSupport({ mode: "popup", ...options }),
30
+ )}
31
+ >
32
+ {children}
33
+ </button>
34
+ );
35
+ }
@@ -0,0 +1,17 @@
1
+ import type { HTMLAttributes } from "react";
2
+
3
+ export interface SupportEmbedProps extends HTMLAttributes<HTMLDivElement> {
4
+ /** Props applied to the element Outseta mounts into. */
5
+ widgetProps?: HTMLAttributes<HTMLDivElement>;
6
+ }
7
+
8
+ /**
9
+ * Renders an embedded Outseta support widget.
10
+ */
11
+ export function SupportEmbed({ widgetProps, ...props }: SupportEmbedProps) {
12
+ return (
13
+ <div {...props}>
14
+ <div {...widgetProps} data-o-support="1" data-mode="embed" />
15
+ </div>
16
+ );
17
+ }
@@ -1,15 +1,42 @@
1
1
  export type {
2
2
  OutsetaUser,
3
3
  OutsetaEmbed,
4
+ OutsetaWidgetMode,
5
+ AuthWidgetMode,
4
6
  AuthWidgetOptions,
5
7
  ProfileWidgetOptions,
8
+ SupportWidgetOptions,
9
+ EmailListWidgetOptions,
10
+ LeadCaptureWidgetOptions,
6
11
  } from "./types.js";
7
12
  export { OutsetaProvider, useOutseta } from "./OutsetaProvider.js";
8
13
  export type { OutsetaContextValue } from "./OutsetaProvider.js";
9
14
  export { AuthEmbed } from "./AuthEmbed.js";
10
15
  export type { AuthEmbedProps } from "./AuthEmbed.js";
16
+ export { AuthButton } from "./AuthButton.js";
17
+ export type { AuthButtonProps } from "./AuthButton.js";
18
+ export { LoginButton } from "./LoginButton.js";
19
+ export type { LoginButtonProps } from "./LoginButton.js";
20
+ export { SignupButton } from "./SignupButton.js";
21
+ export type { SignupButtonProps } from "./SignupButton.js";
11
22
  export { ProfileEmbed } from "./ProfileEmbed.js";
12
23
  export type { ProfileEmbedProps } from "./ProfileEmbed.js";
24
+ export { ProfileButton } from "./ProfileButton.js";
25
+ export type { ProfileButtonProps } from "./ProfileButton.js";
26
+ export { SupportEmbed } from "./SupportEmbed.js";
27
+ export type { SupportEmbedProps } from "./SupportEmbed.js";
28
+ export { SupportButton } from "./SupportButton.js";
29
+ export type { SupportButtonProps } from "./SupportButton.js";
30
+ export { EmailListEmbed } from "./EmailListEmbed.js";
31
+ export type { EmailListEmbedProps } from "./EmailListEmbed.js";
32
+ export { EmailListButton } from "./EmailListButton.js";
33
+ export type { EmailListButtonProps } from "./EmailListButton.js";
34
+ export { EmailListForm } from "./EmailListForm.js";
35
+ export type { EmailListFormProps } from "./EmailListForm.js";
36
+ export { LeadCaptureEmbed } from "./LeadCaptureEmbed.js";
37
+ export type { LeadCaptureEmbedProps } from "./LeadCaptureEmbed.js";
38
+ export { LeadCaptureButton } from "./LeadCaptureButton.js";
39
+ export type { LeadCaptureButtonProps } from "./LeadCaptureButton.js";
13
40
  export { ProtectedRoute } from "./ProtectedRoute.js";
14
41
  export type { ProtectedRouteProps } from "./ProtectedRoute.js";
15
42
  export { AuthCta } from "./AuthCta.js";