diff --git a/ee/query-service/app/api/license.go b/ee/query-service/app/api/license.go index 576a46559e..cbee71be88 100644 --- a/ee/query-service/app/api/license.go +++ b/ee/query-service/app/api/license.go @@ -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)) } diff --git a/ee/query-service/integrations/signozio/response.go b/ee/query-service/integrations/signozio/response.go index 891ea77da1..b3a32ff3fb 100644 --- a/ee/query-service/integrations/signozio/response.go +++ b/ee/query-service/integrations/signozio/response.go @@ -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"` +} diff --git a/ee/query-service/integrations/signozio/signozio.go b/ee/query-service/integrations/signozio/signozio.go index 9c39a9d955..d57132e04b 100644 --- a/ee/query-service/integrations/signozio/signozio.go +++ b/ee/query-service/integrations/signozio/signozio.go @@ -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))) + } +} diff --git a/ee/query-service/license/manager.go b/ee/query-service/license/manager.go index 6ba862a1f6..fd9626800b 100644 --- a/ee/query-service/license/manager.go +++ b/ee/query-service/license/manager.go @@ -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 { diff --git a/ee/query-service/model/license.go b/ee/query-service/model/license.go index e66a2f1bb6..d7c8cb49dc 100644 --- a/ee/query-service/model/license.go +++ b/ee/query-service/model/license.go @@ -236,3 +236,11 @@ func ConvertLicenseV3ToLicenseV2(l *LicenseV3) *License { } } + +type CheckoutRequest struct { + SuccessURL string `json:"url"` +} + +type PortalRequest struct { + SuccessURL string `json:"url"` +} diff --git a/frontend/src/api/billing/checkout.ts b/frontend/src/api/billing/checkout.ts index e6c7640629..f8eaf39748 100644 --- a/frontend/src/api/billing/checkout.ts +++ b/frontend/src/api/billing/checkout.ts @@ -12,9 +12,7 @@ const updateCreditCardApi = async ( ): Promise | ErrorResponse> => { try { const response = await axios.post('/checkout', { - licenseKey: props.licenseKey, - successURL: props.successURL, - cancelURL: props.cancelURL, // temp + url: props.url, }); return { diff --git a/frontend/src/api/billing/manage.ts b/frontend/src/api/billing/manage.ts index dca561bdba..1ea8fa762d 100644 --- a/frontend/src/api/billing/manage.ts +++ b/frontend/src/api/billing/manage.ts @@ -12,8 +12,7 @@ const manageCreditCardApi = async ( ): Promise | ErrorResponse> => { try { const response = await axios.post('/portal', { - licenseKey: props.licenseKey, - returnURL: props.successURL, + url: props.url, }); return { diff --git a/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx b/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx index 7cc4d0bd16..8d9f3bd6b0 100644 --- a/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx +++ b/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx @@ -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(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, ): 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, }); }; diff --git a/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx b/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx index 53f5fa7a24..288a87063c 100644 --- a/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx +++ b/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx @@ -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(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, }); }; diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 189588d9f6..a02ec2fd5a 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -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, - }); - } - }, [activeLicenseV3?.key, manageCreditCard]); + manageCreditCard({ + url: window.location.href, + }); + }, [manageCreditCard]); const isHome = (): boolean => routeKey === 'HOME'; diff --git a/frontend/src/container/BillingContainer/BillingContainer.tsx b/frontend/src/container/BillingContainer/BillingContainer.tsx index 5ada2e8eff..c0408e5a70 100644 --- a/frontend/src/container/BillingContainer/BillingContainer.tsx +++ b/frontend/src/container/BillingContainer/BillingContainer.tsx @@ -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, diff --git a/frontend/src/pages/Support/Support.tsx b/frontend/src/pages/Support/Support.tsx index 9f40f02592..21ffc0041e 100644 --- a/frontend/src/pages/Support/Support.tsx +++ b/frontend/src/pages/Support/Support.tsx @@ -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(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, ): 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, }); }; diff --git a/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.tsx b/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.tsx index 8058df0d60..4385b1a22d 100644 --- a/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.tsx +++ b/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.tsx @@ -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(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', {}); diff --git a/frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx b/frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx index e4dd11dc0b..d5bf6302bf 100644 --- a/frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx +++ b/frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx @@ -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) { diff --git a/frontend/src/tests/test-utils.tsx b/frontend/src/tests/test-utils.tsx index 56311f7a9b..122851d859 100644 --- a/frontend/src/tests/test-utils.tsx +++ b/frontend/src/tests/test-utils.tsx @@ -113,7 +113,6 @@ export function getAppContextMock( status: '', updated_at: '0', }, - key: 'does-not-matter', state: LicenseState.ACTIVE, status: LicenseStatus.VALID, platform: LicensePlatform.CLOUD, diff --git a/frontend/src/types/api/billing/checkout.ts b/frontend/src/types/api/billing/checkout.ts index b299b3ef84..78523376f0 100644 --- a/frontend/src/types/api/billing/checkout.ts +++ b/frontend/src/types/api/billing/checkout.ts @@ -3,7 +3,5 @@ export interface CheckoutSuccessPayloadProps { } export interface CheckoutRequestPayloadProps { - licenseKey: string; - successURL: string; - cancelURL: string; + url: string; } diff --git a/frontend/src/types/api/licensesV3/getActive.ts b/frontend/src/types/api/licensesV3/getActive.ts index 94b5887891..12745ac3f9 100644 --- a/frontend/src/types/api/licensesV3/getActive.ts +++ b/frontend/src/types/api/licensesV3/getActive.ts @@ -27,7 +27,6 @@ export type LicenseV3EventQueueResModel = { }; export type LicenseV3ResModel = { - key: string; status: LicenseStatus; state: LicenseState; event_queue: LicenseV3EventQueueResModel;