@@ -159,6 +159,7 @@ const WORKER_STATUS_POLL_MS = 5000;
159159const DEFAULT_AUTH_NAME = "OpenWork User" ;
160160const OPENWORK_APP_CONNECT_BASE_URL = ( process . env . NEXT_PUBLIC_OPENWORK_APP_CONNECT_URL ?? "" ) . trim ( ) ;
161161const OPENWORK_AUTH_CALLBACK_BASE_URL = ( process . env . NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL ?? "" ) . trim ( ) ;
162+ const BILLING_DISABLED_FOR_EXPERIMENT = true ;
162163
163164function getEmailDomain ( email : string ) : string {
164165 const atIndex = email . lastIndexOf ( "@" ) ;
@@ -241,6 +242,32 @@ function getSocialProviderLabel(provider: SocialAuthProvider): string {
241242 return provider === "github" ? "GitHub" : "Google" ;
242243}
243244
245+ function getExperimentBillingSummary ( ) : BillingSummary {
246+ return {
247+ featureGateEnabled : false ,
248+ hasActivePlan : false ,
249+ checkoutRequired : false ,
250+ checkoutUrl : null ,
251+ portalUrl : null ,
252+ price : null ,
253+ subscription : null ,
254+ invoices : [ ] ,
255+ productId : null ,
256+ benefitId : null
257+ } ;
258+ }
259+
260+ function getAdditionalWorkerRequestHref ( ) : string {
261+ const subject = "requesting an additional worker" ;
262+ const body = [
263+ "Hey Ben," ,
264+ "" ,
265+ "I would like to create an additional worker in order to {INSERT REASON}"
266+ ] . join ( "\n" ) ;
267+
268+ return `mailto:ben@openwork.software?subject=${ encodeURIComponent ( subject ) } &body=${ encodeURIComponent ( body ) } ` ;
269+ }
270+
244271function GitHubLogo ( ) {
245272 return (
246273 < svg viewBox = "0 0 16 16" aria-hidden = "true" className = "ow-social-icon" >
@@ -1088,6 +1115,8 @@ export function CloudControlPanel() {
10881115 const showAuthFeedback = authInfo !== defaultAuthInfo || authError !== null ;
10891116 const openworkConnectUrl = activeWorker ?. openworkUrl ?? activeWorker ?. instanceUrl ?? null ;
10901117 const hasWorkspaceScopedUrl = Boolean ( openworkConnectUrl && / \/ w \/ [ ^ / ? # ] + / . test ( openworkConnectUrl ) ) ;
1118+ const ownedWorkerCount = workers . filter ( ( item ) => item . isMine ) . length ;
1119+ const workerLimitReached = Boolean ( user && ownedWorkerCount > 0 ) ;
10911120 const openworkDeepLink = buildOpenworkDeepLink (
10921121 openworkConnectUrl ,
10931122 activeWorker ?. clientToken ?? null ,
@@ -1189,7 +1218,7 @@ export function CloudControlPanel() {
11891218
11901219 const selectedWorkerStatus = activeWorker ?. status ?? selectedWorker ?. status ?? "unknown" ;
11911220 const selectedStatusMeta = getWorkerStatusMeta ( selectedWorkerStatus ) ;
1192- const effectiveCheckoutUrl = checkoutUrl ?? billingSummary ?. checkoutUrl ?? null ;
1221+ const effectiveCheckoutUrl = BILLING_DISABLED_FOR_EXPERIMENT ? null : ( checkoutUrl ?? billingSummary ?. checkoutUrl ?? null ) ;
11931222 const billingSubscription = billingSummary ?. subscription ?? null ;
11941223 const billingPrice = billingSummary ?. price ?? null ;
11951224 const runtimeUpgradeCount = runtimeSnapshot ?. services . filter ( ( item ) => item . upgradeAvailable ) . length ?? 0 ;
@@ -1428,6 +1457,14 @@ export function CloudControlPanel() {
14281457 }
14291458
14301459 async function refreshBilling ( options : { includeCheckout ?: boolean ; quiet ?: boolean } = { } ) {
1460+ if ( BILLING_DISABLED_FOR_EXPERIMENT ) {
1461+ const summary = getExperimentBillingSummary ( ) ;
1462+ setBillingSummary ( summary ) ;
1463+ setCheckoutUrl ( null ) ;
1464+ setBillingError ( null ) ;
1465+ return summary ;
1466+ }
1467+
14311468 if ( ! user ) {
14321469 setBillingSummary ( null ) ;
14331470 if ( ! options . quiet ) {
@@ -1499,6 +1536,13 @@ export function CloudControlPanel() {
14991536 }
15001537
15011538 async function handleSubscriptionCancellation ( cancelAtPeriodEnd : boolean ) {
1539+ if ( BILLING_DISABLED_FOR_EXPERIMENT ) {
1540+ setBillingSummary ( getExperimentBillingSummary ( ) ) ;
1541+ setCheckoutUrl ( null ) ;
1542+ setBillingError ( "Billing is disabled for this experiment." ) ;
1543+ return ;
1544+ }
1545+
15021546 if ( ! user || billingSubscriptionBusy ) {
15031547 return ;
15041548 }
@@ -1647,6 +1691,13 @@ export function CloudControlPanel() {
16471691 } , [ activeWorker ?. workerId , selectedWorker ?. workerId , runtimeSnapshot ?. upgrade . status ] ) ;
16481692
16491693 useEffect ( ( ) => {
1694+ if ( BILLING_DISABLED_FOR_EXPERIMENT ) {
1695+ setBillingSummary ( getExperimentBillingSummary ( ) ) ;
1696+ setBillingError ( null ) ;
1697+ setCheckoutUrl ( null ) ;
1698+ return ;
1699+ }
1700+
16501701 if ( ! user ) {
16511702 setBillingSummary ( null ) ;
16521703 setBillingError ( null ) ;
@@ -1657,6 +1708,13 @@ export function CloudControlPanel() {
16571708 } , [ user ?. id , authToken ] ) ;
16581709
16591710 useEffect ( ( ) => {
1711+ if ( BILLING_DISABLED_FOR_EXPERIMENT ) {
1712+ if ( shellView !== "workers" ) {
1713+ setShellView ( "workers" ) ;
1714+ }
1715+ return ;
1716+ }
1717+
16601718 if ( ! user || shellView !== "billing" ) {
16611719 return ;
16621720 }
@@ -1701,16 +1759,21 @@ export function CloudControlPanel() {
17011759 return ;
17021760 }
17031761
1704- setPaymentReturned ( true ) ;
1762+ // Polar checkout returns are ignored while billing is disabled for this experiment.
1763+ // TODO(den-free-first-worker): Re-enable the original Polar checkout return flow after the experiment.
1764+ // setPaymentReturned(true);
1765+ // setCheckoutUrl(null);
1766+ // setShellView("billing");
1767+ // setLaunchStatus("Checkout return detected. Click launch to continue worker provisioning.");
1768+ // setAuthInfo("Checkout return detected. Sign in to continue to Billing.");
1769+ // appendEvent("success", "Returned from checkout", `Session ${shortValue(customerSessionToken)}`);
1770+ // trackPosthogEvent("den_paywall_checkout_returned", {
1771+ // source: "polar",
1772+ // session_token_present: true
1773+ // });
17051774 setCheckoutUrl ( null ) ;
1706- setShellView ( "billing" ) ;
1707- setLaunchStatus ( "Checkout return detected. Click launch to continue worker provisioning." ) ;
1708- setAuthInfo ( "Checkout return detected. Sign in to continue to Billing." ) ;
1709- appendEvent ( "success" , "Returned from checkout" , `Session ${ shortValue ( customerSessionToken ) } ` ) ;
1710- trackPosthogEvent ( "den_paywall_checkout_returned" , {
1711- source : "polar" ,
1712- session_token_present : true
1713- } ) ;
1775+ setShellView ( "workers" ) ;
1776+ setLaunchStatus ( "Name your worker and click launch." ) ;
17141777
17151778 params . delete ( "customer_session_token" ) ;
17161779 const nextQuery = params . toString ( ) ;
@@ -1723,7 +1786,7 @@ export function CloudControlPanel() {
17231786 return ;
17241787 }
17251788
1726- void refreshBilling ( { quiet : true } ) ;
1789+ // Billing refresh intentionally disabled for the one-worker experiment.
17271790 } , [ paymentReturned , user ?. id , authToken ] ) ;
17281791
17291792 useEffect ( ( ) => {
@@ -2119,33 +2182,41 @@ export function CloudControlPanel() {
21192182 12000
21202183 ) ;
21212184
2122- if ( response . status === 402 ) {
2123- const url = getCheckoutUrl ( payload ) ;
2124- setCheckoutUrl ( url ) ;
2125- setShellView ( "billing" ) ;
2126- setBillingSummary ( ( current ) => {
2127- if ( ! current ) {
2128- return current ;
2129- }
2130-
2131- return {
2132- ...current ,
2133- hasActivePlan : false ,
2134- checkoutRequired : true ,
2135- checkoutUrl : url ?? current . checkoutUrl
2136- } ;
2137- } ) ;
2138- setLaunchStatus ( "Payment is required. Complete checkout and return to continue launch." ) ;
2139- setLaunchError ( url ? null : "Checkout URL missing from paywall response." ) ;
2140- appendEvent ( "warning" , "Paywall required" , url ? "Checkout URL generated" : "Checkout URL missing" ) ;
2141- trackPosthogEvent ( "den_paywall_required" , {
2142- checkout_url_present : Boolean ( url )
2143- } ) ;
2144-
2145- if ( ! url ) {
2146- void refreshBilling ( { includeCheckout : true , quiet : true } ) ;
2147- }
2148-
2185+ // TODO(den-free-first-worker): Restore this 402 paywall branch after the one-worker experiment ends.
2186+ // if (response.status === 402) {
2187+ // const url = getCheckoutUrl(payload);
2188+ // setCheckoutUrl(url);
2189+ // setShellView("billing");
2190+ // setBillingSummary((current) => {
2191+ // if (!current) {
2192+ // return current;
2193+ // }
2194+ //
2195+ // return {
2196+ // ...current,
2197+ // hasActivePlan: false,
2198+ // checkoutRequired: true,
2199+ // checkoutUrl: url ?? current.checkoutUrl
2200+ // };
2201+ // });
2202+ // setLaunchStatus("Payment is required. Complete checkout and return to continue launch.");
2203+ // setLaunchError(url ? null : "Checkout URL missing from paywall response.");
2204+ // appendEvent("warning", "Paywall required", url ? "Checkout URL generated" : "Checkout URL missing");
2205+ // trackPosthogEvent("den_paywall_required", {
2206+ // checkout_url_present: Boolean(url)
2207+ // });
2208+ //
2209+ // if (!url) {
2210+ // void refreshBilling({ includeCheckout: true, quiet: true });
2211+ // }
2212+ //
2213+ // return;
2214+ // }
2215+ if ( response . status === 409 ) {
2216+ const message = getErrorMessage ( payload , "You can only create one cloud worker during this experiment." ) ;
2217+ setLaunchStatus ( "Worker limit reached." ) ;
2218+ setLaunchError ( message ) ;
2219+ appendEvent ( "warning" , "Worker limit reached" , message ) ;
21492220 return ;
21502221 }
21512222
@@ -2582,15 +2653,16 @@ export function CloudControlPanel() {
25822653 >
25832654 Workers
25842655 </ button >
2585- < button
2656+ { /* TODO(den-free-first-worker): Restore Billing nav button after the experiment. */ }
2657+ { /* <button
25862658 type="button"
25872659 onClick={() => setShellView("billing")}
25882660 className={`rounded-[12px] px-3 py-1.5 text-sm font-medium transition ${
25892661 shellView === "billing" ? "bg-[#1B29FF]/10 text-[#1B29FF]" : "text-slate-600 hover:bg-slate-100"
25902662 }`}
25912663 >
25922664 Billing
2593- </ button >
2665+ </button> */ }
25942666 </ div >
25952667 < button
25962668 type = "button"
@@ -2602,7 +2674,7 @@ export function CloudControlPanel() {
26022674 </ button >
26032675 </ div >
26042676
2605- { shellView === "workers" ? (
2677+ { shellView === "workers" || BILLING_DISABLED_FOR_EXPERIMENT ? (
26062678 < div className = "flex h-full min-h-0 flex-col gap-4 lg:flex-row" >
26072679 < aside className = "hidden h-full w-[260px] shrink-0 flex-col justify-between rounded-[32px] border border-slate-200 bg-white p-5 shadow-sm lg:flex" >
26082680 < div >
@@ -2618,13 +2690,14 @@ export function CloudControlPanel() {
26182690 >
26192691 Workers
26202692 </ button >
2621- < button
2693+ { /* TODO(den-free-first-worker): Restore Billing sidebar button after the experiment. */ }
2694+ { /* <button
26222695 type="button"
26232696 className="w-full rounded-[14px] px-3 py-2.5 text-left text-sm font-medium text-slate-500 transition hover:bg-slate-50"
26242697 onClick={() => setShellView("billing")}
26252698 >
26262699 Billing
2627- </ button >
2700+ </button> */ }
26282701 < span className = "block rounded-[14px] px-3 py-2.5 text-sm font-medium text-slate-400" > Settings</ span >
26292702 < span className = "block rounded-[14px] px-3 py-2.5 text-sm font-medium text-slate-400" > Help Center</ span >
26302703 </ nav >
@@ -2698,10 +2771,12 @@ export function CloudControlPanel() {
26982771 type = "button"
26992772 className = "w-full rounded-[12px] bg-[#1B29FF] px-3 py-2.5 text-sm font-semibold text-white transition hover:bg-[#151FDA] disabled:cursor-not-allowed disabled:opacity-60"
27002773 onClick = { handleLaunchWorker }
2701- disabled = { ! user || launchBusy || worker ?. status === "provisioning" }
2774+ disabled = { ! user || launchBusy || worker ?. status === "provisioning" || workerLimitReached }
27022775 >
27032776 { launchBusy
27042777 ? "Starting worker..."
2778+ : workerLimitReached
2779+ ? "Worker limit reached"
27052780 : worker ?. status === "provisioning"
27062781 ? "Worker is starting..."
27072782 : `Launch "${ workerName || "Cloud Worker" } "` }
@@ -2714,6 +2789,15 @@ export function CloudControlPanel() {
27142789 </ div >
27152790 ) : null }
27162791
2792+ { workerLimitReached ? (
2793+ < a
2794+ href = { getAdditionalWorkerRequestHref ( ) }
2795+ className = "mt-3 inline-flex w-full items-center justify-center rounded-[12px] border border-slate-300 bg-white px-3 py-2.5 text-sm font-semibold text-slate-700 transition hover:border-slate-400 hover:text-slate-900"
2796+ >
2797+ Request an additional worker
2798+ </ a >
2799+ ) : null }
2800+
27172801 { effectiveCheckoutUrl ? (
27182802 < div className = "mt-3 rounded-[12px] border border-amber-200 bg-amber-50 px-3 py-2.5" >
27192803 < p className = "text-sm font-semibold text-amber-800" > Payment needed before launch</ p >
@@ -2803,10 +2887,12 @@ export function CloudControlPanel() {
28032887 type = "button"
28042888 className = "w-full rounded-[12px] bg-[#1B29FF] px-3 py-2.5 text-sm font-semibold text-white transition hover:bg-[#151FDA] disabled:cursor-not-allowed disabled:opacity-60"
28052889 onClick = { handleLaunchWorker }
2806- disabled = { ! user || launchBusy || worker ?. status === "provisioning" }
2890+ disabled = { ! user || launchBusy || worker ?. status === "provisioning" || workerLimitReached }
28072891 >
28082892 { launchBusy
28092893 ? "Starting worker..."
2894+ : workerLimitReached
2895+ ? "Worker limit reached"
28102896 : worker ?. status === "provisioning"
28112897 ? "Worker is starting..."
28122898 : `Launch "${ workerName || "Cloud Worker" } "` }
@@ -2819,7 +2905,17 @@ export function CloudControlPanel() {
28192905 </ div >
28202906 ) : null }
28212907
2822- { effectiveCheckoutUrl ? (
2908+ { workerLimitReached ? (
2909+ < a
2910+ href = { getAdditionalWorkerRequestHref ( ) }
2911+ className = "mt-3 inline-flex w-full items-center justify-center rounded-[12px] border border-slate-300 bg-white px-3 py-2.5 text-sm font-semibold text-slate-700 transition hover:border-slate-400 hover:text-slate-900"
2912+ >
2913+ Request an additional worker
2914+ </ a >
2915+ ) : null }
2916+
2917+ { /* TODO(den-free-first-worker): Restore checkout CTA block when paywall returns. */ }
2918+ { /* {effectiveCheckoutUrl ? (
28232919 <div className="mt-3 rounded-[12px] border border-amber-200 bg-amber-50 px-3 py-2.5">
28242920 <p className="text-sm font-semibold text-amber-800">Payment needed before launch</p>
28252921 <a
@@ -2830,7 +2926,8 @@ export function CloudControlPanel() {
28302926 Continue to checkout
28312927 </a>
28322928 </div>
2833- ) : null }
2929+ ) : null} */ }
2930+
28342931 </ div >
28352932 ) : null }
28362933
0 commit comments