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" "net/http"
"go.signoz.io/signoz/ee/query-service/constants" "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/ee/query-service/model"
"go.signoz.io/signoz/pkg/http/render" "go.signoz.io/signoz/pkg/http/render"
"go.uber.org/zap" "go.uber.org/zap"
@ -48,6 +49,10 @@ type details struct {
BillTotal float64 `json:"billTotal"` BillTotal float64 `json:"billTotal"`
} }
type Redirect struct {
RedirectURL string `json:"redirectURL"`
}
type billingDetails struct { type billingDetails struct {
Status string `json:"status"` Status string `json:"status"`
Data struct { Data struct {
@ -119,36 +124,31 @@ func (ah *APIHandler) refreshLicensesV3(w http.ResponseWriter, r *http.Request)
render.Success(w, http.StatusNoContent, nil) 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) { func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) {
type checkoutResponse struct { checkoutRequest := &model.CheckoutRequest{}
Status string `json:"status"` if err := json.NewDecoder(r.Body).Decode(checkoutRequest); err != nil {
Data struct { RespondError(w, model.BadRequest(err), nil)
RedirectURL string `json:"redirectURL"` return
} `json:"data"`
} }
hClient := &http.Client{} license := ah.LM().GetActiveLicense()
req, err := http.NewRequest("POST", constants.LicenseSignozIo+"/checkout", r.Body) 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 { if err != nil {
RespondError(w, model.InternalError(err), nil) RespondError(w, 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)
return return
} }
// decode response body ah.Respond(w, getCheckoutPortalResponse(redirectUrl))
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)
} }
func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) { 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) { func (ah *APIHandler) portalSession(w http.ResponseWriter, r *http.Request) {
type checkoutResponse struct { portalRequest := &model.PortalRequest{}
Status string `json:"status"` if err := json.NewDecoder(r.Body).Decode(portalRequest); err != nil {
Data struct { RespondError(w, model.BadRequest(err), nil)
RedirectURL string `json:"redirectURL"` return
} `json:"data"`
} }
hClient := &http.Client{} license := ah.LM().GetActiveLicense()
req, err := http.NewRequest("POST", constants.LicenseSignozIo+"/portal", r.Body) 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 { if err != nil {
RespondError(w, model.InternalError(err), nil) RespondError(w, 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)
return return
} }
// decode response body ah.Respond(w, getCheckoutPortalResponse(redirectUrl))
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)
} }

View File

@ -6,3 +6,11 @@ type ValidateLicenseResponse struct {
Status status `json:"status"` Status status `json:"status"`
Data map[string]interface{} `json:"data"` 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: case 200, 201:
return nil return nil
case 400, 401: 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")) "bad request error received from license.signoz.io"))
default: 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")) "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 return license, nil
} }
func (lm *Manager) GetActiveLicense() *model.LicenseV3 {
return lm.activeLicenseV3
}
// CheckFeature will be internally used by backend routines // CheckFeature will be internally used by backend routines
// for feature gating // for feature gating
func (lm *Manager) CheckFeature(featureKey string) error { 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> => { ): Promise<SuccessResponse<CheckoutSuccessPayloadProps> | ErrorResponse> => {
try { try {
const response = await axios.post('/checkout', { const response = await axios.post('/checkout', {
licenseKey: props.licenseKey, url: props.url,
successURL: props.successURL,
cancelURL: props.cancelURL, // temp
}); });
return { return {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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