chore(subscription): update the checkout and portal endpoints to use zeus (#7310)

### Summary

- update the checkout and portal endpoints to use `zeus` instead of license server
This commit is contained in:
Vikrant Gupta 2025-03-17 15:22:04 +05:30 committed by GitHub
parent 458bd1171b
commit c26277cd42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 164 additions and 132 deletions

View File

@ -7,6 +7,7 @@ import (
"net/http"
"go.signoz.io/signoz/ee/query-service/constants"
"go.signoz.io/signoz/ee/query-service/integrations/signozio"
"go.signoz.io/signoz/ee/query-service/model"
"go.signoz.io/signoz/pkg/http/render"
"go.uber.org/zap"
@ -48,6 +49,10 @@ type details struct {
BillTotal float64 `json:"billTotal"`
}
type Redirect struct {
RedirectURL string `json:"redirectURL"`
}
type billingDetails struct {
Status string `json:"status"`
Data struct {
@ -119,36 +124,31 @@ func (ah *APIHandler) refreshLicensesV3(w http.ResponseWriter, r *http.Request)
render.Success(w, http.StatusNoContent, nil)
}
func getCheckoutPortalResponse(redirectURL string) *Redirect {
return &Redirect{RedirectURL: redirectURL}
}
func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) {
type checkoutResponse struct {
Status string `json:"status"`
Data struct {
RedirectURL string `json:"redirectURL"`
} `json:"data"`
checkoutRequest := &model.CheckoutRequest{}
if err := json.NewDecoder(r.Body).Decode(checkoutRequest); err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
hClient := &http.Client{}
req, err := http.NewRequest("POST", constants.LicenseSignozIo+"/checkout", r.Body)
license := ah.LM().GetActiveLicense()
if license == nil {
RespondError(w, model.BadRequestStr("cannot proceed with checkout without license key"), nil)
return
}
redirectUrl, err := signozio.CheckoutSession(r.Context(), checkoutRequest, license.Key)
if err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey)
licenseResp, err := hClient.Do(req)
if err != nil {
RespondError(w, model.InternalError(err), nil)
RespondError(w, err, nil)
return
}
// decode response body
var resp checkoutResponse
if err := json.NewDecoder(licenseResp.Body).Decode(&resp); err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
ah.Respond(w, resp.Data)
ah.Respond(w, getCheckoutPortalResponse(redirectUrl))
}
func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
@ -298,32 +298,23 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
func (ah *APIHandler) portalSession(w http.ResponseWriter, r *http.Request) {
type checkoutResponse struct {
Status string `json:"status"`
Data struct {
RedirectURL string `json:"redirectURL"`
} `json:"data"`
portalRequest := &model.PortalRequest{}
if err := json.NewDecoder(r.Body).Decode(portalRequest); err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
hClient := &http.Client{}
req, err := http.NewRequest("POST", constants.LicenseSignozIo+"/portal", r.Body)
license := ah.LM().GetActiveLicense()
if license == nil {
RespondError(w, model.BadRequestStr("cannot request the portal session without license key"), nil)
return
}
redirectUrl, err := signozio.PortalSession(r.Context(), portalRequest, license.Key)
if err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey)
licenseResp, err := hClient.Do(req)
if err != nil {
RespondError(w, model.InternalError(err), nil)
RespondError(w, err, nil)
return
}
// decode response body
var resp checkoutResponse
if err := json.NewDecoder(licenseResp.Body).Decode(&resp); err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
ah.Respond(w, resp.Data)
ah.Respond(w, getCheckoutPortalResponse(redirectUrl))
}

View File

@ -6,3 +6,11 @@ type ValidateLicenseResponse struct {
Status status `json:"status"`
Data map[string]interface{} `json:"data"`
}
type CheckoutSessionRedirect struct {
RedirectURL string `json:"url"`
}
type CheckoutResponse struct {
Status status `json:"status"`
Data CheckoutSessionRedirect `json:"data"`
}

View File

@ -126,10 +126,98 @@ func SendUsage(ctx context.Context, usage model.UsagePayload) *model.ApiError {
case 200, 201:
return nil
case 400, 401:
return model.BadRequest(errors.Wrap(fmt.Errorf(string(body)),
return model.BadRequest(errors.Wrap(errors.New(string(body)),
"bad request error received from license.signoz.io"))
default:
return model.InternalError(errors.Wrap(fmt.Errorf(string(body)),
return model.InternalError(errors.Wrap(errors.New(string(body)),
"internal error received from license.signoz.io"))
}
}
func CheckoutSession(ctx context.Context, checkoutRequest *model.CheckoutRequest, licenseKey string) (string, *model.ApiError) {
hClient := &http.Client{}
reqString, err := json.Marshal(checkoutRequest)
if err != nil {
return "", model.BadRequest(err)
}
req, err := http.NewRequestWithContext(ctx, "POST", C.GatewayUrl+"/v2/subscriptions/me/sessions/checkout", bytes.NewBuffer(reqString))
if err != nil {
return "", model.BadRequest(err)
}
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
response, err := hClient.Do(req)
if err != nil {
return "", model.BadRequest(err)
}
body, err := io.ReadAll(response.Body)
if err != nil {
return "", model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to read checkout response from %v", C.GatewayUrl)))
}
defer response.Body.Close()
switch response.StatusCode {
case 201:
a := CheckoutResponse{}
err = json.Unmarshal(body, &a)
if err != nil {
return "", model.BadRequest(errors.Wrap(err, "failed to unmarshal zeus checkout response"))
}
return a.Data.RedirectURL, nil
case 400:
return "", model.BadRequest(errors.Wrap(errors.New(string(body)),
fmt.Sprintf("bad request error received from %v", C.GatewayUrl)))
case 401:
return "", model.Unauthorized(errors.Wrap(errors.New(string(body)),
fmt.Sprintf("unauthorized request error received from %v", C.GatewayUrl)))
default:
return "", model.InternalError(errors.Wrap(errors.New(string(body)),
fmt.Sprintf("internal request error received from %v", C.GatewayUrl)))
}
}
func PortalSession(ctx context.Context, checkoutRequest *model.PortalRequest, licenseKey string) (string, *model.ApiError) {
hClient := &http.Client{}
reqString, err := json.Marshal(checkoutRequest)
if err != nil {
return "", model.BadRequest(err)
}
req, err := http.NewRequestWithContext(ctx, "POST", C.GatewayUrl+"/v2/subscriptions/me/sessions/portal", bytes.NewBuffer(reqString))
if err != nil {
return "", model.BadRequest(err)
}
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
response, err := hClient.Do(req)
if err != nil {
return "", model.BadRequest(err)
}
body, err := io.ReadAll(response.Body)
if err != nil {
return "", model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to read portal response from %v", C.GatewayUrl)))
}
defer response.Body.Close()
switch response.StatusCode {
case 201:
a := CheckoutResponse{}
err = json.Unmarshal(body, &a)
if err != nil {
return "", model.BadRequest(errors.Wrap(err, "failed to unmarshal zeus portal response"))
}
return a.Data.RedirectURL, nil
case 400:
return "", model.BadRequest(errors.Wrap(errors.New(string(body)),
fmt.Sprintf("bad request error received from %v", C.GatewayUrl)))
case 401:
return "", model.Unauthorized(errors.Wrap(errors.New(string(body)),
fmt.Sprintf("unauthorized request error received from %v", C.GatewayUrl)))
default:
return "", model.InternalError(errors.Wrap(errors.New(string(body)),
fmt.Sprintf("internal request error received from %v", C.GatewayUrl)))
}
}

View File

@ -265,6 +265,10 @@ func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseRe
return license, nil
}
func (lm *Manager) GetActiveLicense() *model.LicenseV3 {
return lm.activeLicenseV3
}
// CheckFeature will be internally used by backend routines
// for feature gating
func (lm *Manager) CheckFeature(featureKey string) error {

View File

@ -236,3 +236,11 @@ func ConvertLicenseV3ToLicenseV2(l *LicenseV3) *License {
}
}
type CheckoutRequest struct {
SuccessURL string `json:"url"`
}
type PortalRequest struct {
SuccessURL string `json:"url"`
}

View File

@ -12,9 +12,7 @@ const updateCreditCardApi = async (
): Promise<SuccessResponse<CheckoutSuccessPayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/checkout', {
licenseKey: props.licenseKey,
successURL: props.successURL,
cancelURL: props.cancelURL, // temp
url: props.url,
});
return {

View File

@ -12,8 +12,7 @@ const manageCreditCardApi = async (
): Promise<SuccessResponse<CheckoutSuccessPayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/portal', {
licenseKey: props.licenseKey,
returnURL: props.successURL,
url: props.url,
});
return {

View File

@ -4,33 +4,19 @@ import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useNotifications } from 'hooks/useNotifications';
import { CreditCard, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
export default function ChatSupportGateway(): JSX.Element {
const { notifications } = useNotifications();
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
const { licenses, isFetchingLicenses } = useAppContext();
useEffect(() => {
if (!isFetchingLicenses && licenses) {
const activeValidLicense =
licenses.licenses?.find((license) => license.isCurrent === true) || null;
setActiveLicense(activeValidLicense);
}
}, [licenses, isFetchingLicenses]);
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
): void => {
@ -66,9 +52,7 @@ export default function ChatSupportGateway(): JSX.Element {
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
url: window.location.href,
});
};

View File

@ -11,12 +11,11 @@ import { useNotifications } from 'hooks/useNotifications';
import { defaultTo } from 'lodash-es';
import { CreditCard, HelpCircle, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
export interface LaunchChatSupportProps {
eventName: string;
@ -42,13 +41,11 @@ function LaunchChatSupport({
const { notifications } = useNotifications();
const {
licenses,
isFetchingLicenses,
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
isLoggedIn,
} = useAppContext();
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
@ -104,14 +101,6 @@ function LaunchChatSupport({
licenses,
]);
useEffect(() => {
if (!isFetchingLicenses && licenses) {
const activeValidLicense =
licenses.licenses?.find((license) => license.isCurrent === true) || null;
setActiveLicense(activeValidLicense);
}
}, [isFetchingLicenses, licenses]);
const handleFacingIssuesClick = (): void => {
if (showAddCreditCardModal) {
logEvent('Disabled Chat Support: Clicked', {
@ -164,9 +153,7 @@ function LaunchChatSupport({
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
url: window.location.href,
});
};

View File

@ -292,14 +292,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}, [user.role]);
const handleFailedPayment = useCallback((): void => {
if (activeLicenseV3?.key) {
manageCreditCard({
licenseKey: activeLicenseV3?.key || '',
successURL: window.location.origin,
cancelURL: window.location.origin,
url: window.location.href,
});
}
}, [activeLicenseV3?.key, manageCreditCard]);
}, [manageCreditCard]);
const isHome = (): boolean => routeKey === 'HOME';

View File

@ -326,9 +326,7 @@ export default function BillingContainer(): JSX.Element {
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
url: window.location.href,
});
} else {
logEvent('Billing : Manage Billing', {
@ -337,14 +335,11 @@ export default function BillingContainer(): JSX.Element {
});
manageCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
url: window.location.href,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
activeLicense?.key,
isFreeTrial,
licenses?.trialConvertedToSubscription,
manageCreditCard,

View File

@ -20,7 +20,6 @@ import { useMutation } from 'react-query';
import { useHistory, useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
const { Title, Text } = Typography;
@ -81,7 +80,6 @@ export default function Support(): JSX.Element {
const history = useHistory();
const { notifications } = useNotifications();
const { licenses, featureFlags } = useAppContext();
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
@ -110,13 +108,6 @@ export default function Support(): JSX.Element {
const showAddCreditCardModal =
!isPremiumChatSupportEnabled && !licenses?.trialConvertedToSubscription;
useEffect(() => {
const activeValidLicense =
licenses?.licenses?.find((license) => license.isCurrent === true) || null;
setActiveLicense(activeValidLicense);
}, [licenses?.licenses]);
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
): void => {
@ -152,9 +143,7 @@ export default function Support(): JSX.Element {
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
url: window.location.href,
});
};

View File

@ -23,10 +23,9 @@ import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { CircleArrowRight } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import { License } from 'types/api/licenses/def';
import { getFormattedDate } from 'utils/timeUtils';
import CustomerStoryCard from './CustomerStoryCard';
@ -41,7 +40,6 @@ import {
export default function WorkspaceBlocked(): JSX.Element {
const { user, licenses, isFetchingLicenses } = useAppContext();
const isAdmin = user.role === 'ADMIN';
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const { notifications } = useNotifications();
const { t } = useTranslation(['workspaceLocked']);
@ -72,11 +70,6 @@ export default function WorkspaceBlocked(): JSX.Element {
if (!shouldBlockWorkspace) {
history.push(ROUTES.APPLICATION);
}
const activeValidLicense =
licenses?.licenses?.find((license) => license.isCurrent === true) || null;
setActiveLicense(activeValidLicense);
}
}, [isFetchingLicenses, licenses]);
@ -103,12 +96,10 @@ export default function WorkspaceBlocked(): JSX.Element {
logEvent('Workspace Blocked: User Clicked Update Credit Card', {});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.origin,
cancelURL: window.location.origin,
url: window.location.href,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeLicense?.key, updateCreditCard]);
}, [updateCreditCard]);
const handleExtendTrial = (): void => {
logEvent('Workspace Blocked: User Clicked Extend Trial', {});

View File

@ -51,11 +51,9 @@ function WorkspaceSuspended(): JSX.Element {
const handleUpdateCreditCard = useCallback(async () => {
manageCreditCard({
licenseKey: activeLicenseV3?.key || '',
successURL: window.location.origin,
cancelURL: window.location.origin,
url: window.location.origin,
});
}, [activeLicenseV3?.key, manageCreditCard]);
}, [manageCreditCard]);
useEffect(() => {
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {

View File

@ -113,7 +113,6 @@ export function getAppContextMock(
status: '',
updated_at: '0',
},
key: 'does-not-matter',
state: LicenseState.ACTIVE,
status: LicenseStatus.VALID,
platform: LicensePlatform.CLOUD,

View File

@ -3,7 +3,5 @@ export interface CheckoutSuccessPayloadProps {
}
export interface CheckoutRequestPayloadProps {
licenseKey: string;
successURL: string;
cancelURL: string;
url: string;
}

View File

@ -27,7 +27,6 @@ export type LicenseV3EventQueueResModel = {
};
export type LicenseV3ResModel = {
key: string;
status: LicenseStatus;
state: LicenseState;
event_queue: LicenseV3EventQueueResModel;