@outseta/react 0.1.1 → 0.2.0
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/dist/index.js +208 -1
- package/package.json +1 -1
- package/src/components/AuthEmbed.tsx +38 -0
- package/src/components/LogoutButton.tsx +18 -0
- package/src/components/OutsetaProvider.tsx +151 -0
- package/src/components/ProfileEmbed.tsx +26 -0
- package/src/components/ProtectedRoute.tsx +42 -0
- package/src/components/PurchaseAddonButton.tsx +38 -0
- package/src/components/index.ts +17 -0
- package/src/components/types.ts +72 -0
- package/src/index.ts +3 -0
package/dist/index.js
CHANGED
|
@@ -1,3 +1,210 @@
|
|
|
1
|
+
// src/components/OutsetaProvider.tsx
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState
|
|
8
|
+
} from "react";
|
|
9
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
10
|
+
var OutsetaContext = createContext(null);
|
|
11
|
+
function useOutseta() {
|
|
12
|
+
const context = useContext(OutsetaContext);
|
|
13
|
+
if (!context) {
|
|
14
|
+
throw new Error("useOutseta must be used within an OutsetaProvider");
|
|
15
|
+
}
|
|
16
|
+
return context;
|
|
17
|
+
}
|
|
18
|
+
function getOutseta() {
|
|
19
|
+
if (typeof window !== "undefined" && window.Outseta) {
|
|
20
|
+
return window.Outseta;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function OutsetaProvider({ children }) {
|
|
25
|
+
const [status, setStatus] = useState("init");
|
|
26
|
+
const [user, setUser] = useState(null);
|
|
27
|
+
const outsetaRef = useRef(null);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const outseta = getOutseta();
|
|
30
|
+
if (!outseta)
|
|
31
|
+
return;
|
|
32
|
+
outsetaRef.current = outseta;
|
|
33
|
+
const updateUser = async () => {
|
|
34
|
+
try {
|
|
35
|
+
const outsetaUser = await outseta.getUser();
|
|
36
|
+
setUser(outsetaUser);
|
|
37
|
+
} catch {
|
|
38
|
+
setUser(null);
|
|
39
|
+
} finally {
|
|
40
|
+
setStatus("ready");
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
outseta.on("subscription.update", updateUser);
|
|
44
|
+
outseta.on("profile.update", updateUser);
|
|
45
|
+
outseta.on("account.update", updateUser);
|
|
46
|
+
const params = new URLSearchParams(window.location.search);
|
|
47
|
+
const accessToken = params.get("access_token");
|
|
48
|
+
if (accessToken) {
|
|
49
|
+
outseta.setAccessToken(accessToken);
|
|
50
|
+
params.delete("access_token");
|
|
51
|
+
const newSearch = params.toString();
|
|
52
|
+
const newUrl = window.location.pathname + (newSearch ? `?${newSearch}` : "") + window.location.hash;
|
|
53
|
+
window.history.replaceState({}, "", newUrl);
|
|
54
|
+
}
|
|
55
|
+
const checkAuth = async () => {
|
|
56
|
+
try {
|
|
57
|
+
const token = await outseta.getAccessToken();
|
|
58
|
+
if (token) {
|
|
59
|
+
await updateUser();
|
|
60
|
+
} else {
|
|
61
|
+
setStatus("ready");
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
setStatus("ready");
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
checkAuth();
|
|
68
|
+
return () => {
|
|
69
|
+
outseta.on("subscription.update", () => {});
|
|
70
|
+
outseta.on("profile.update", () => {});
|
|
71
|
+
outseta.on("account.update", () => {});
|
|
72
|
+
};
|
|
73
|
+
}, []);
|
|
74
|
+
const logout = () => {
|
|
75
|
+
const outseta = outsetaRef.current;
|
|
76
|
+
if (!outseta)
|
|
77
|
+
return;
|
|
78
|
+
outseta.setAccessToken("");
|
|
79
|
+
setUser(null);
|
|
80
|
+
outseta.logout();
|
|
81
|
+
};
|
|
82
|
+
const openLogin = (options = {}) => {
|
|
83
|
+
outsetaRef.current?.auth.open({
|
|
84
|
+
widgetMode: "login|register",
|
|
85
|
+
authenticationCallbackUrl: window.location.href,
|
|
86
|
+
...options
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
const openSignup = (options = {}) => {
|
|
90
|
+
outsetaRef.current?.auth.open({
|
|
91
|
+
widgetMode: "register",
|
|
92
|
+
authenticationCallbackUrl: window.location.href,
|
|
93
|
+
...options
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const openProfile = (options = {}) => {
|
|
97
|
+
outsetaRef.current?.profile.open({ tab: "profile", ...options });
|
|
98
|
+
};
|
|
99
|
+
return /* @__PURE__ */ jsxDEV(OutsetaContext.Provider, {
|
|
100
|
+
value: {
|
|
101
|
+
user,
|
|
102
|
+
isLoading: status !== "ready",
|
|
103
|
+
logout,
|
|
104
|
+
openLogin,
|
|
105
|
+
openSignup,
|
|
106
|
+
openProfile
|
|
107
|
+
},
|
|
108
|
+
children
|
|
109
|
+
}, undefined, false, undefined, this);
|
|
110
|
+
}
|
|
111
|
+
// src/components/AuthEmbed.tsx
|
|
112
|
+
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
113
|
+
function AuthEmbed({
|
|
114
|
+
widgetMode = "login|register",
|
|
115
|
+
registrationDefaults,
|
|
116
|
+
...props
|
|
117
|
+
}) {
|
|
118
|
+
const defaults = typeof registrationDefaults === "string" ? registrationDefaults : registrationDefaults ? JSON.stringify(registrationDefaults) : undefined;
|
|
119
|
+
return /* @__PURE__ */ jsxDEV2("div", {
|
|
120
|
+
...props,
|
|
121
|
+
children: /* @__PURE__ */ jsxDEV2("div", {
|
|
122
|
+
"data-o-auth": "1",
|
|
123
|
+
"data-mode": "embed",
|
|
124
|
+
"data-widget-mode": widgetMode,
|
|
125
|
+
"data-registration-defaults": defaults
|
|
126
|
+
}, undefined, false, undefined, this)
|
|
127
|
+
}, widgetMode, false, undefined, this);
|
|
128
|
+
}
|
|
129
|
+
// src/components/ProfileEmbed.tsx
|
|
130
|
+
import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
|
|
131
|
+
function ProfileEmbed({ tab = "profile", ...props }) {
|
|
132
|
+
return /* @__PURE__ */ jsxDEV3("div", {
|
|
133
|
+
...props,
|
|
134
|
+
children: /* @__PURE__ */ jsxDEV3("div", {
|
|
135
|
+
"data-o-profile": "1",
|
|
136
|
+
"data-mode": "embed",
|
|
137
|
+
"data-tab": tab
|
|
138
|
+
}, undefined, false, undefined, this)
|
|
139
|
+
}, tab, false, undefined, this);
|
|
140
|
+
}
|
|
141
|
+
// src/components/ProtectedRoute.tsx
|
|
142
|
+
import { jsxDEV as jsxDEV4, Fragment } from "react/jsx-dev-runtime";
|
|
143
|
+
function ProtectedRoute({
|
|
144
|
+
plans,
|
|
145
|
+
children,
|
|
146
|
+
loading = null,
|
|
147
|
+
accessDenied = null,
|
|
148
|
+
unauthenticated = null
|
|
149
|
+
}) {
|
|
150
|
+
const { user, isLoading } = useOutseta();
|
|
151
|
+
if (isLoading)
|
|
152
|
+
return /* @__PURE__ */ jsxDEV4(Fragment, {
|
|
153
|
+
children: loading
|
|
154
|
+
}, undefined, false, undefined, this);
|
|
155
|
+
if (!user)
|
|
156
|
+
return /* @__PURE__ */ jsxDEV4(Fragment, {
|
|
157
|
+
children: unauthenticated
|
|
158
|
+
}, undefined, false, undefined, this);
|
|
159
|
+
const userPlanUid = user.Account?.CurrentSubscription?.Plan?.Uid;
|
|
160
|
+
const hasAccess = userPlanUid ? plans.includes(userPlanUid) : false;
|
|
161
|
+
if (!hasAccess)
|
|
162
|
+
return /* @__PURE__ */ jsxDEV4(Fragment, {
|
|
163
|
+
children: accessDenied
|
|
164
|
+
}, undefined, false, undefined, this);
|
|
165
|
+
return /* @__PURE__ */ jsxDEV4(Fragment, {
|
|
166
|
+
children
|
|
167
|
+
}, undefined, false, undefined, this);
|
|
168
|
+
}
|
|
169
|
+
// src/components/PurchaseAddonButton.tsx
|
|
170
|
+
import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
|
|
171
|
+
function PurchaseAddonButton({
|
|
172
|
+
addonUid,
|
|
173
|
+
billingRenewalTerm = 4,
|
|
174
|
+
children = "Buy Add-on",
|
|
175
|
+
...props
|
|
176
|
+
}) {
|
|
177
|
+
const { openProfile } = useOutseta();
|
|
178
|
+
return /* @__PURE__ */ jsxDEV5("button", {
|
|
179
|
+
onClick: () => openProfile({
|
|
180
|
+
mode: "popup",
|
|
181
|
+
tab: "purchaseAddOn",
|
|
182
|
+
stateProps: { addOnUid: addonUid, billingRenewalTerm }
|
|
183
|
+
}),
|
|
184
|
+
...props,
|
|
185
|
+
children
|
|
186
|
+
}, undefined, false, undefined, this);
|
|
187
|
+
}
|
|
188
|
+
// src/components/LogoutButton.tsx
|
|
189
|
+
import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
|
|
190
|
+
function LogoutButton({
|
|
191
|
+
children = "Logout",
|
|
192
|
+
...props
|
|
193
|
+
}) {
|
|
194
|
+
const { logout } = useOutseta();
|
|
195
|
+
return /* @__PURE__ */ jsxDEV6("button", {
|
|
196
|
+
onClick: logout,
|
|
197
|
+
...props,
|
|
198
|
+
children
|
|
199
|
+
}, undefined, false, undefined, this);
|
|
200
|
+
}
|
|
1
201
|
export {
|
|
2
|
-
|
|
202
|
+
useOutseta,
|
|
203
|
+
createClient,
|
|
204
|
+
PurchaseAddonButton,
|
|
205
|
+
ProtectedRoute,
|
|
206
|
+
ProfileEmbed,
|
|
207
|
+
OutsetaProvider,
|
|
208
|
+
LogoutButton,
|
|
209
|
+
AuthEmbed
|
|
3
210
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
export interface AuthEmbedProps extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** Widget mode: "login", "register", or "login|register" */
|
|
5
|
+
widgetMode?: "login" | "register" | "login|register";
|
|
6
|
+
/** Registration defaults as a JSON string or object */
|
|
7
|
+
registrationDefaults?: string | Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders an embedded Outseta auth widget (login/register).
|
|
12
|
+
*
|
|
13
|
+
* Uses `key={widgetMode}` internally so React re-mounts the widget
|
|
14
|
+
* when the mode changes.
|
|
15
|
+
*/
|
|
16
|
+
export function AuthEmbed({
|
|
17
|
+
widgetMode = "login|register",
|
|
18
|
+
registrationDefaults,
|
|
19
|
+
...props
|
|
20
|
+
}: AuthEmbedProps) {
|
|
21
|
+
const defaults =
|
|
22
|
+
typeof registrationDefaults === "string"
|
|
23
|
+
? registrationDefaults
|
|
24
|
+
: registrationDefaults
|
|
25
|
+
? JSON.stringify(registrationDefaults)
|
|
26
|
+
: undefined;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div key={widgetMode} {...props}>
|
|
30
|
+
<div
|
|
31
|
+
data-o-auth="1"
|
|
32
|
+
data-mode="embed"
|
|
33
|
+
data-widget-mode={widgetMode}
|
|
34
|
+
data-registration-defaults={defaults}
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes } from "react";
|
|
2
|
+
import { useOutseta } from "./OutsetaProvider.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Button that logs the user out of Outseta.
|
|
6
|
+
*/
|
|
7
|
+
export function LogoutButton({
|
|
8
|
+
children = "Logout",
|
|
9
|
+
...props
|
|
10
|
+
}: ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
11
|
+
const { logout } = useOutseta();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<button onClick={logout} {...props}>
|
|
15
|
+
{children}
|
|
16
|
+
</button>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
9
|
+
import type {
|
|
10
|
+
AuthWidgetOptions,
|
|
11
|
+
OutsetaEmbed,
|
|
12
|
+
OutsetaUser,
|
|
13
|
+
ProfileWidgetOptions,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
export interface OutsetaContextValue {
|
|
17
|
+
user: OutsetaUser | null;
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
logout: () => void;
|
|
20
|
+
openLogin: (options?: AuthWidgetOptions) => void;
|
|
21
|
+
openSignup: (options?: AuthWidgetOptions) => void;
|
|
22
|
+
openProfile: (options?: ProfileWidgetOptions) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const OutsetaContext = createContext<OutsetaContextValue | null>(null);
|
|
26
|
+
|
|
27
|
+
export function useOutseta(): OutsetaContextValue {
|
|
28
|
+
const context = useContext(OutsetaContext);
|
|
29
|
+
if (!context) {
|
|
30
|
+
throw new Error("useOutseta must be used within an OutsetaProvider");
|
|
31
|
+
}
|
|
32
|
+
return context;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getOutseta(): OutsetaEmbed | null {
|
|
36
|
+
if (typeof window !== "undefined" && window.Outseta) {
|
|
37
|
+
return window.Outseta;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function OutsetaProvider({ children }: { children: ReactNode }) {
|
|
43
|
+
const [status, setStatus] = useState<"init" | "ready">("init");
|
|
44
|
+
const [user, setUser] = useState<OutsetaUser | null>(null);
|
|
45
|
+
const outsetaRef = useRef<OutsetaEmbed | null>(null);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const outseta = getOutseta();
|
|
49
|
+
if (!outseta) return;
|
|
50
|
+
outsetaRef.current = outseta;
|
|
51
|
+
|
|
52
|
+
const updateUser = async () => {
|
|
53
|
+
try {
|
|
54
|
+
const outsetaUser = await outseta.getUser();
|
|
55
|
+
setUser(outsetaUser);
|
|
56
|
+
} catch {
|
|
57
|
+
setUser(null);
|
|
58
|
+
} finally {
|
|
59
|
+
setStatus("ready");
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Subscribe to user-related events
|
|
64
|
+
outseta.on("subscription.update", updateUser);
|
|
65
|
+
outseta.on("profile.update", updateUser);
|
|
66
|
+
outseta.on("account.update", updateUser);
|
|
67
|
+
|
|
68
|
+
// Handle OAuth callback access_token from URL
|
|
69
|
+
const params = new URLSearchParams(window.location.search);
|
|
70
|
+
const accessToken = params.get("access_token");
|
|
71
|
+
|
|
72
|
+
if (accessToken) {
|
|
73
|
+
outseta.setAccessToken(accessToken);
|
|
74
|
+
|
|
75
|
+
// Clean up URL without triggering navigation
|
|
76
|
+
params.delete("access_token");
|
|
77
|
+
const newSearch = params.toString();
|
|
78
|
+
const newUrl =
|
|
79
|
+
window.location.pathname +
|
|
80
|
+
(newSearch ? `?${newSearch}` : "") +
|
|
81
|
+
window.location.hash;
|
|
82
|
+
window.history.replaceState({}, "", newUrl);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check existing auth state
|
|
86
|
+
const checkAuth = async () => {
|
|
87
|
+
try {
|
|
88
|
+
const token = await outseta.getAccessToken();
|
|
89
|
+
if (token) {
|
|
90
|
+
await updateUser();
|
|
91
|
+
} else {
|
|
92
|
+
setStatus("ready");
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
setStatus("ready");
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
checkAuth();
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
// Replace event handlers with no-ops on cleanup
|
|
103
|
+
outseta.on("subscription.update", () => {});
|
|
104
|
+
outseta.on("profile.update", () => {});
|
|
105
|
+
outseta.on("account.update", () => {});
|
|
106
|
+
};
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
const logout = () => {
|
|
110
|
+
const outseta = outsetaRef.current;
|
|
111
|
+
if (!outseta) return;
|
|
112
|
+
outseta.setAccessToken("");
|
|
113
|
+
setUser(null);
|
|
114
|
+
outseta.logout();
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const openLogin = (options: AuthWidgetOptions = {}) => {
|
|
118
|
+
outsetaRef.current?.auth.open({
|
|
119
|
+
widgetMode: "login|register",
|
|
120
|
+
authenticationCallbackUrl: window.location.href,
|
|
121
|
+
...options,
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const openSignup = (options: AuthWidgetOptions = {}) => {
|
|
126
|
+
outsetaRef.current?.auth.open({
|
|
127
|
+
widgetMode: "register",
|
|
128
|
+
authenticationCallbackUrl: window.location.href,
|
|
129
|
+
...options,
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const openProfile = (options: ProfileWidgetOptions = {}) => {
|
|
134
|
+
outsetaRef.current?.profile.open({ tab: "profile", ...options });
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<OutsetaContext.Provider
|
|
139
|
+
value={{
|
|
140
|
+
user,
|
|
141
|
+
isLoading: status !== "ready",
|
|
142
|
+
logout,
|
|
143
|
+
openLogin,
|
|
144
|
+
openSignup,
|
|
145
|
+
openProfile,
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
{children}
|
|
149
|
+
</OutsetaContext.Provider>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
export interface ProfileEmbedProps extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** Profile tab to display */
|
|
5
|
+
tab?:
|
|
6
|
+
| "profile"
|
|
7
|
+
| "billing"
|
|
8
|
+
| "planChange"
|
|
9
|
+
| "purchaseAddOn"
|
|
10
|
+
| "passwordChange"
|
|
11
|
+
| "teamMembers";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Renders an embedded Outseta profile widget.
|
|
16
|
+
*
|
|
17
|
+
* Uses `key={tab}` internally so React re-mounts the widget
|
|
18
|
+
* when the tab changes.
|
|
19
|
+
*/
|
|
20
|
+
export function ProfileEmbed({ tab = "profile", ...props }: ProfileEmbedProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div key={tab} {...props}>
|
|
23
|
+
<div data-o-profile="1" data-mode="embed" data-tab={tab} />
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { useOutseta } from "./OutsetaProvider.js";
|
|
3
|
+
|
|
4
|
+
export interface ProtectedRouteProps {
|
|
5
|
+
/** Plan UIDs that grant access to this route */
|
|
6
|
+
plans: string[];
|
|
7
|
+
/** Content to render when access is granted */
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
/** Rendered while checking auth status (defaults to null) */
|
|
10
|
+
loading?: ReactNode;
|
|
11
|
+
/** Rendered when user is logged in but lacks the right plan */
|
|
12
|
+
accessDenied?: ReactNode;
|
|
13
|
+
/** Rendered when user is not logged in */
|
|
14
|
+
unauthenticated?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Protects content behind plan-based access control.
|
|
19
|
+
*
|
|
20
|
+
* Checks the current user's subscription plan UID against the
|
|
21
|
+
* provided list of allowed plan UIDs.
|
|
22
|
+
*/
|
|
23
|
+
export function ProtectedRoute({
|
|
24
|
+
plans,
|
|
25
|
+
children,
|
|
26
|
+
loading = null,
|
|
27
|
+
accessDenied = null,
|
|
28
|
+
unauthenticated = null,
|
|
29
|
+
}: ProtectedRouteProps) {
|
|
30
|
+
const { user, isLoading } = useOutseta();
|
|
31
|
+
|
|
32
|
+
if (isLoading) return <>{loading}</>;
|
|
33
|
+
|
|
34
|
+
if (!user) return <>{unauthenticated}</>;
|
|
35
|
+
|
|
36
|
+
const userPlanUid = user.Account?.CurrentSubscription?.Plan?.Uid;
|
|
37
|
+
const hasAccess = userPlanUid ? plans.includes(userPlanUid) : false;
|
|
38
|
+
|
|
39
|
+
if (!hasAccess) return <>{accessDenied}</>;
|
|
40
|
+
|
|
41
|
+
return <>{children}</>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes } from "react";
|
|
2
|
+
import { useOutseta } from "./OutsetaProvider.js";
|
|
3
|
+
|
|
4
|
+
export interface PurchaseAddonButtonProps
|
|
5
|
+
extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
6
|
+
/** The Outseta add-on UID to purchase */
|
|
7
|
+
addonUid: string;
|
|
8
|
+
/** Billing renewal term (4 = one-time purchase by default) */
|
|
9
|
+
billingRenewalTerm?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Button that opens the Outseta profile widget to the
|
|
14
|
+
* "Purchase Add-on" tab for a specific add-on.
|
|
15
|
+
*/
|
|
16
|
+
export function PurchaseAddonButton({
|
|
17
|
+
addonUid,
|
|
18
|
+
billingRenewalTerm = 4,
|
|
19
|
+
children = "Buy Add-on",
|
|
20
|
+
...props
|
|
21
|
+
}: PurchaseAddonButtonProps) {
|
|
22
|
+
const { openProfile } = useOutseta();
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<button
|
|
26
|
+
onClick={() =>
|
|
27
|
+
openProfile({
|
|
28
|
+
mode: "popup",
|
|
29
|
+
tab: "purchaseAddOn",
|
|
30
|
+
stateProps: { addOnUid: addonUid, billingRenewalTerm },
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</button>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
OutsetaUser,
|
|
3
|
+
OutsetaEmbed,
|
|
4
|
+
AuthWidgetOptions,
|
|
5
|
+
ProfileWidgetOptions,
|
|
6
|
+
} from "./types.js";
|
|
7
|
+
export { OutsetaProvider, useOutseta } from "./OutsetaProvider.js";
|
|
8
|
+
export type { OutsetaContextValue } from "./OutsetaProvider.js";
|
|
9
|
+
export { AuthEmbed } from "./AuthEmbed.js";
|
|
10
|
+
export type { AuthEmbedProps } from "./AuthEmbed.js";
|
|
11
|
+
export { ProfileEmbed } from "./ProfileEmbed.js";
|
|
12
|
+
export type { ProfileEmbedProps } from "./ProfileEmbed.js";
|
|
13
|
+
export { ProtectedRoute } from "./ProtectedRoute.js";
|
|
14
|
+
export type { ProtectedRouteProps } from "./ProtectedRoute.js";
|
|
15
|
+
export { PurchaseAddonButton } from "./PurchaseAddonButton.js";
|
|
16
|
+
export type { PurchaseAddonButtonProps } from "./PurchaseAddonButton.js";
|
|
17
|
+
export { LogoutButton } from "./LogoutButton.js";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the Outseta embed script (window.Outseta).
|
|
3
|
+
* These describe the client-side SDK loaded via script tag,
|
|
4
|
+
* NOT the REST API types generated by Orval.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface OutsetaUser {
|
|
8
|
+
Uid: string;
|
|
9
|
+
Email: string;
|
|
10
|
+
FirstName?: string;
|
|
11
|
+
LastName?: string;
|
|
12
|
+
FullName?: string;
|
|
13
|
+
Account?: {
|
|
14
|
+
Uid?: string;
|
|
15
|
+
Name?: string;
|
|
16
|
+
CurrentSubscription?: {
|
|
17
|
+
Uid?: string;
|
|
18
|
+
Plan?: {
|
|
19
|
+
Uid?: string;
|
|
20
|
+
Name?: string;
|
|
21
|
+
};
|
|
22
|
+
SubscriptionAddOns?: Array<{
|
|
23
|
+
AddOn?: {
|
|
24
|
+
Uid?: string;
|
|
25
|
+
Name?: string;
|
|
26
|
+
};
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AuthWidgetOptions {
|
|
34
|
+
widgetMode?: "login" | "register" | "login|register";
|
|
35
|
+
authenticationCallbackUrl?: string;
|
|
36
|
+
mode?: "popup" | "embed";
|
|
37
|
+
registrationDefaults?: string;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ProfileWidgetOptions {
|
|
42
|
+
tab?:
|
|
43
|
+
| "profile"
|
|
44
|
+
| "billing"
|
|
45
|
+
| "planChange"
|
|
46
|
+
| "purchaseAddOn"
|
|
47
|
+
| "passwordChange"
|
|
48
|
+
| "teamMembers";
|
|
49
|
+
mode?: "popup" | "embed";
|
|
50
|
+
stateProps?: Record<string, unknown>;
|
|
51
|
+
[key: string]: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface OutsetaEmbed {
|
|
55
|
+
setAccessToken: (token: string) => void;
|
|
56
|
+
getAccessToken: () => Promise<string | null>;
|
|
57
|
+
getUser: () => Promise<OutsetaUser>;
|
|
58
|
+
logout: () => void;
|
|
59
|
+
on: (event: string, callback: () => void) => void;
|
|
60
|
+
auth: {
|
|
61
|
+
open: (options?: AuthWidgetOptions) => void;
|
|
62
|
+
};
|
|
63
|
+
profile: {
|
|
64
|
+
open: (options?: ProfileWidgetOptions) => void;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
declare global {
|
|
69
|
+
interface Window {
|
|
70
|
+
Outseta?: OutsetaEmbed;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { createClient } from "@outseta/api-client";
|
|
2
2
|
export type { OutsetaCredentials } from "@outseta/api-client";
|
|
3
3
|
|
|
4
|
+
// Outseta embed components and hooks
|
|
5
|
+
export * from "./components/index.js";
|
|
6
|
+
|
|
4
7
|
// Re-export generated React Query hooks once generated
|
|
5
8
|
// export * from "./generated/index.js";
|