Files
gpt-plus-gpt/pkg/stripe/payment.go
2026-03-15 20:48:19 +08:00

920 lines
32 KiB
Go

package stripe
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
mrand "math/rand"
"net/http"
"net/url"
"strings"
"gpt-plus/pkg/httpclient"
)
// randomTimeOnPage returns a random time_on_page value between 15000-45000ms.
// A constant value (e.g. "30000") is a fingerprint signal for bot detection.
func randomTimeOnPage() string {
return fmt.Sprintf("%d", mrand.Intn(30001)+15000)
}
// localeForCountry returns a browser locale based on billing country code.
func localeForCountry(country string) string {
switch country {
case "GB":
return "en-GB"
case "DE", "AT", "CH":
return "de-DE"
case "FR":
return "fr-FR"
case "JP":
return "ja-JP"
case "CN":
return "zh-CN"
default:
return "en-US"
}
}
// timezoneForCountry returns a browser timezone based on billing country code.
func timezoneForCountry(country string) string {
switch country {
case "GB":
return "Europe/London"
case "DE", "AT", "CH":
return "Europe/Berlin"
case "FR":
return "Europe/Paris"
case "JP":
return "Asia/Tokyo"
case "CN":
return "Asia/Shanghai"
default:
return "America/Chicago"
}
}
// ErrCardDeclined indicates the card was declined by Stripe.
var ErrCardDeclined = errors.New("card declined")
const (
// DefaultLanguage is the browser locale for US cards.
DefaultLanguage = "en-US"
// DefaultAcceptLanguage is the Accept-Language header for US cards.
DefaultAcceptLanguage = "en-US,en;q=0.9"
chromeSecChUa = `"Google Chrome";v="145", "Chromium";v="145", "Not-A.Brand";v="99"`
stripeOrigin = "https://js.stripe.com"
)
// CheckoutResult holds the response from creating a checkout session.
type CheckoutResult struct {
CheckoutSessionID string
PublishableKey string
ClientSecret string
ExpectedAmount int
ProcessorEntity string
}
// CreateCheckoutSession creates a Stripe checkout session via ChatGPT API.
func CreateCheckoutSession(client *httpclient.Client, accessToken, deviceID, sentinelToken string, body map[string]interface{}) (*CheckoutResult, error) {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("marshal checkout body: %w", err)
}
req, err := http.NewRequest("POST", "https://chatgpt.com/backend-api/payments/checkout", strings.NewReader(string(jsonBody)))
if err != nil {
return nil, fmt.Errorf("create checkout request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Origin", "https://chatgpt.com")
req.Header.Set("Referer", "https://chatgpt.com/")
req.Header.Set("Oai-Device-Id", deviceID)
req.Header.Set("Openai-Sentinel-Token", sentinelToken)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("send checkout request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read checkout response: %w", err)
}
log.Printf("[stripe] checkout response (status=%d): %s", resp.StatusCode, string(respBody))
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("checkout failed: status %d, body: %s", resp.StatusCode, string(respBody))
}
var result map[string]interface{}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("parse checkout response: %w", err)
}
cr := &CheckoutResult{
ProcessorEntity: "openai_llc",
}
if v, ok := result["checkout_session_id"].(string); ok {
cr.CheckoutSessionID = v
} else if v, ok := result["session_id"].(string); ok {
cr.CheckoutSessionID = v
}
if v, ok := result["publishable_key"].(string); ok {
cr.PublishableKey = v
} else if v, ok := result["stripe_publishable_key"].(string); ok {
cr.PublishableKey = v
}
if v, ok := result["client_secret"].(string); ok {
cr.ClientSecret = v
}
if v, ok := result["amount_total"].(float64); ok {
cr.ExpectedAmount = int(v)
} else if v, ok := result["amount"].(float64); ok {
cr.ExpectedAmount = int(v)
}
if v, ok := result["processor_entity"].(string); ok {
cr.ProcessorEntity = v
}
return cr, nil
}
// InitResult holds the response from init checkout session.
type InitResult struct {
InitChecksum string
}
// stripeVersionWithBetas returns the _stripe_version value with checkout betas appended.
func stripeVersionWithBetas(base string) string {
return base + "; checkout_server_update_beta=v1; checkout_manual_approval_preview=v1"
}
// InitCheckoutSession calls POST /v1/payment_pages/{cs_id}/init to get init_checksum.
func InitCheckoutSession(client *httpclient.Client, csID, publishableKey, stripeVersion, stripeJsID, userAgent, billingCountry string) (*InitResult, error) {
initURL := fmt.Sprintf("https://api.stripe.com/v1/payment_pages/%s/init", csID)
form := url.Values{}
form.Set("browser_locale", localeForCountry(billingCountry))
form.Set("browser_timezone", timezoneForCountry(billingCountry))
form.Set("elements_session_client[client_betas][0]", "custom_checkout_server_updates_1")
form.Set("elements_session_client[client_betas][1]", "custom_checkout_manual_approval_1")
form.Set("elements_session_client[elements_init_source]", "custom_checkout")
form.Set("elements_session_client[referrer_host]", "chatgpt.com")
form.Set("elements_session_client[stripe_js_id]", stripeJsID)
form.Set("elements_session_client[locale]", localeForCountry(billingCountry))
form.Set("elements_session_client[is_aggregation_expected]", "false")
form.Set("key", publishableKey)
form.Set("_stripe_version", stripeVersionWithBetas(stripeVersion))
req, err := http.NewRequest("POST", initURL, strings.NewReader(form.Encode()))
if err != nil {
return nil, fmt.Errorf("create init request: %w", err)
}
setStripeHeaders(req, userAgent)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("send init request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read init response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("init failed: status %d, body: %s", resp.StatusCode, string(respBody))
}
var result map[string]interface{}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("parse init response: %w", err)
}
ir := &InitResult{}
if v, ok := result["init_checksum"].(string); ok {
ir.InitChecksum = v
}
// Log amount fields from init for debugging
log.Printf("[stripe] init response: amount_total=%v, amount=%v",
result["amount_total"], result["amount"])
return ir, nil
}
// UpdateCheckoutParams holds parameters for the intermediate checkout update.
type UpdateCheckoutParams struct {
CheckoutSessionID string
PublishableKey string
StripeVersion string // API version e.g. "2025-03-31.basil"
StripeJsID string
BillingCountry string
BillingAddress string
BillingCity string
BillingState string
BillingZip string
UserAgent string
}
// UpdateResult holds the response from updating a checkout session.
type UpdateResult struct {
UpdatedAmount int // amount_total after tax recalculation
}
// UpdateCheckoutSession sends an intermediate POST to /v1/payment_pages/{cs_id}
// to update tax_region info. This must be called before confirm to accept terms.
func UpdateCheckoutSession(client *httpclient.Client, params *UpdateCheckoutParams) (*UpdateResult, error) {
updateURL := fmt.Sprintf("https://api.stripe.com/v1/payment_pages/%s", params.CheckoutSessionID)
form := url.Values{}
form.Set("tax_region[country]", params.BillingCountry)
if params.BillingAddress != "" {
form.Set("tax_region[line1]", params.BillingAddress)
}
if params.BillingCity != "" {
form.Set("tax_region[city]", params.BillingCity)
}
if params.BillingState != "" {
form.Set("tax_region[state]", params.BillingState)
}
if params.BillingZip != "" {
form.Set("tax_region[postal_code]", params.BillingZip)
}
form.Set("elements_session_client[client_betas][0]", "custom_checkout_server_updates_1")
form.Set("elements_session_client[client_betas][1]", "custom_checkout_manual_approval_1")
form.Set("elements_session_client[elements_init_source]", "custom_checkout")
form.Set("elements_session_client[referrer_host]", "chatgpt.com")
form.Set("elements_session_client[stripe_js_id]", params.StripeJsID)
form.Set("elements_session_client[locale]", localeForCountry(params.BillingCountry))
form.Set("elements_session_client[is_aggregation_expected]", "false")
form.Set("client_attribution_metadata[merchant_integration_additional_elements][0]", "payment")
form.Set("client_attribution_metadata[merchant_integration_additional_elements][1]", "address")
form.Set("key", params.PublishableKey)
form.Set("_stripe_version", stripeVersionWithBetas(params.StripeVersion))
req, err := http.NewRequest("POST", updateURL, strings.NewReader(form.Encode()))
if err != nil {
return nil, fmt.Errorf("create update request: %w", err)
}
setStripeHeaders(req, params.UserAgent)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("send update request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read update response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("update checkout failed: status %d, body: %s", resp.StatusCode, string(respBody))
}
var result map[string]interface{}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("parse update response: %w", err)
}
ur := &UpdateResult{}
// Try top-level amount fields first
if v, ok := result["amount_total"].(float64); ok {
ur.UpdatedAmount = int(v)
} else if v, ok := result["amount"].(float64); ok {
ur.UpdatedAmount = int(v)
}
// Try total_summary.due (Stripe checkout session structure: {"due":2000,"subtotal":2000,"total":2000})
if ur.UpdatedAmount == 0 {
if ts, ok := result["total_summary"].(map[string]interface{}); ok {
if v, ok := ts["due"].(float64); ok {
ur.UpdatedAmount = int(v)
} else if v, ok := ts["total"].(float64); ok {
ur.UpdatedAmount = int(v)
}
}
}
log.Printf("[stripe] update resolved amount=%d", ur.UpdatedAmount)
return ur, nil
}
// PaymentMethodParams holds parameters for creating a payment method.
type PaymentMethodParams struct {
CardNumber string
CardCVC string
CardExpMonth string
CardExpYear string
BillingName string
BillingEmail string
BillingCountry string
BillingAddress string
BillingCity string
BillingState string
BillingZip string
GUID string
MUID string
SID string
PublishableKey string
BuildHash string // e.g. "ede17ac9fd" — used in payment_user_agent
StripeVersion string // e.g. "2025-03-31.basil" — used in _stripe_version
UserAgent string
}
// PaymentMethodResult holds the response from creating a payment method.
type PaymentMethodResult struct {
PaymentMethodID string
}
// CreatePaymentMethod calls POST /v1/payment_methods to create a pm_xxx.
func CreatePaymentMethod(client *httpclient.Client, params *PaymentMethodParams) (*PaymentMethodResult, error) {
form := url.Values{}
form.Set("type", "card")
form.Set("card[number]", params.CardNumber)
form.Set("card[cvc]", params.CardCVC)
form.Set("card[exp_month]", params.CardExpMonth)
form.Set("card[exp_year]", params.CardExpYear)
form.Set("billing_details[name]", params.BillingName)
form.Set("billing_details[email]", params.BillingEmail)
form.Set("billing_details[address][country]", params.BillingCountry)
if params.BillingAddress != "" {
form.Set("billing_details[address][line1]", params.BillingAddress)
}
if params.BillingCity != "" {
form.Set("billing_details[address][city]", params.BillingCity)
}
if params.BillingState != "" {
form.Set("billing_details[address][state]", params.BillingState)
}
if params.BillingZip != "" {
form.Set("billing_details[address][postal_code]", params.BillingZip)
}
form.Set("allow_redisplay", "unspecified")
form.Set("pasted_fields", "number,exp,cvc")
form.Set("payment_user_agent", fmt.Sprintf("stripe.js/%s; stripe-js-v3/%s; payment-element; deferred-intent",
params.BuildHash, params.BuildHash))
form.Set("referrer", "https://chatgpt.com")
form.Set("time_on_page", randomTimeOnPage())
form.Set("guid", params.GUID)
form.Set("muid", params.MUID)
form.Set("sid", params.SID)
form.Set("key", params.PublishableKey)
if params.StripeVersion != "" {
form.Set("_stripe_version", stripeVersionWithBetas(params.StripeVersion))
}
req, err := http.NewRequest("POST", "https://api.stripe.com/v1/payment_methods", strings.NewReader(form.Encode()))
if err != nil {
return nil, fmt.Errorf("create payment method request: %w", err)
}
setStripeHeaders(req, params.UserAgent)
req.AddCookie(&http.Cookie{Name: "__stripe_mid", Value: params.MUID})
req.AddCookie(&http.Cookie{Name: "__stripe_sid", Value: params.SID})
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("send payment method request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read payment method response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("create payment method failed: status %d, body: %s", resp.StatusCode, string(respBody))
}
var result map[string]interface{}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("parse payment method response: %w", err)
}
pmr := &PaymentMethodResult{}
if v, ok := result["id"].(string); ok {
pmr.PaymentMethodID = v
}
if pmr.PaymentMethodID == "" {
return nil, fmt.Errorf("no payment method ID in response: %s", string(respBody))
}
return pmr, nil
}
// ConfirmParams holds all parameters needed to confirm a Stripe payment.
type ConfirmParams struct {
CheckoutSessionID string
PaymentMethodID string
InitChecksum string
StripeJsID string
GUID string
MUID string
SID string
ExpectedAmount int
PublishableKey string
StripeVersion string // build hash, e.g. "ede17ac9fd"
StripeAPIVersion string // API version, e.g. "2025-03-31.basil"
UserAgent string
}
// DirectConfirmParams holds parameters for confirm with inline card data (no tokenization).
type DirectConfirmParams struct {
CheckoutSessionID string
// Card details (inline)
CardNumber string
CardCVC string
CardExpMonth string
CardExpYear string
// Billing
BillingName string
BillingEmail string
BillingCountry string
BillingAddress string
BillingCity string
BillingState string
BillingZip string
// Fingerprint
GUID string
MUID string
SID string
// Stripe
ExpectedAmount int
InitChecksum string // from InitCheckoutSession
PublishableKey string
BuildHash string // e.g. "ede17ac9fd"
StripeVersion string // API version, e.g. "2025-03-31.basil"
StripeJsID string // per-session UUID, like stripe.js generates
UserAgent string
PassiveCaptchaToken string // passive hCaptcha token from telemetry
PPageID string // payment page ID for telemetry
}
// ConfirmResult holds the parsed confirm response.
type ConfirmResult struct {
// RequiresAction is true when setup_intent needs challenge verification.
RequiresAction bool
// Challenge fields (only set when RequiresAction is true)
SetupIntentID string // seti_xxx
ClientSecret string // seti_xxx_secret_xxx
SiteKey string // hCaptcha site_key from stripe_js
RqData string // hCaptcha rqdata from stripe_js
VerificationURL string // /v1/setup_intents/{seti}/verify_challenge
// ReturnURL is the merchant-configured return URL from the confirm response.
// For Team plans, it contains account_id (workspace ID) as a query parameter.
ReturnURL string
}
// ConfirmPayment confirms payment directly with Stripe using pm_xxx.
func ConfirmPayment(client *httpclient.Client, params *ConfirmParams) (*ConfirmResult, error) {
confirmURL := fmt.Sprintf("https://api.stripe.com/v1/payment_pages/%s/confirm", params.CheckoutSessionID)
returnURL := fmt.Sprintf(
"https://checkout.stripe.com/c/pay/%s?returned_from_redirect=true",
params.CheckoutSessionID,
)
form := url.Values{}
form.Set("guid", params.GUID)
form.Set("muid", params.MUID)
form.Set("sid", params.SID)
form.Set("payment_method", params.PaymentMethodID)
if params.InitChecksum != "" {
form.Set("init_checksum", params.InitChecksum)
}
form.Set("version", params.StripeVersion)
form.Set("expected_amount", fmt.Sprintf("%d", params.ExpectedAmount))
form.Set("expected_payment_method_type", "card")
form.Set("return_url", returnURL)
form.Set("elements_session_client[client_betas][0]", "custom_checkout_server_updates_1")
form.Set("elements_session_client[client_betas][1]", "custom_checkout_manual_approval_1")
form.Set("elements_session_client[elements_init_source]", "custom_checkout")
form.Set("elements_session_client[referrer_host]", "chatgpt.com")
form.Set("elements_session_client[stripe_js_id]", params.StripeJsID)
form.Set("elements_session_client[locale]", "en-US")
form.Set("elements_session_client[is_aggregation_expected]", "false")
form.Set("client_attribution_metadata[merchant_integration_source]", "checkout")
form.Set("client_attribution_metadata[merchant_integration_version]", "custom")
form.Set("client_attribution_metadata[merchant_integration_subtype]", "payment-element")
form.Set("client_attribution_metadata[merchant_integration_additional_elements][0]", "payment")
form.Set("client_attribution_metadata[merchant_integration_additional_elements][1]", "address")
form.Set("client_attribution_metadata[payment_intent_creation_flow]", "deferred")
form.Set("client_attribution_metadata[payment_method_selection_flow]", "automatic")
form.Set("consent[terms_of_service]", "accepted")
form.Set("key", params.PublishableKey)
form.Set("_stripe_version", stripeVersionWithBetas(params.StripeAPIVersion))
req, err := http.NewRequest("POST", confirmURL, strings.NewReader(form.Encode()))
if err != nil {
return nil, fmt.Errorf("create confirm request: %w", err)
}
setStripeHeaders(req, params.UserAgent)
req.AddCookie(&http.Cookie{Name: "__stripe_mid", Value: params.MUID})
req.AddCookie(&http.Cookie{Name: "__stripe_sid", Value: params.SID})
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("send confirm request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read confirm response: %w", err)
}
log.Printf("[stripe] confirm response (status=%d): %s", resp.StatusCode, string(respBody))
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("confirm failed: status %d, body: %s", resp.StatusCode, string(respBody))
}
return parseConfirmResponse(respBody)
}
// parseConfirmResponse extracts challenge info from the confirm JSON response.
func parseConfirmResponse(body []byte) (*ConfirmResult, error) {
var raw map[string]interface{}
if err := json.Unmarshal(body, &raw); err != nil {
return nil, fmt.Errorf("parse confirm response: %w", err)
}
cr := &ConfirmResult{}
// Extract return_url — for Team plans this contains the workspace account_id
if v, ok := raw["return_url"].(string); ok {
cr.ReturnURL = v
log.Printf("[stripe] confirm return_url: %s", v)
}
// Check setup_intent.status
si, ok := raw["setup_intent"].(map[string]interface{})
if !ok {
// No setup_intent — payment may have succeeded directly
return cr, nil
}
status, _ := si["status"].(string)
// Check for setup errors that indicate card decline (200 response but failed state)
if lastErr, ok := si["last_setup_error"].(map[string]interface{}); ok {
errCode, _ := lastErr["code"].(string)
errMsg, _ := lastErr["message"].(string)
errType, _ := lastErr["type"].(string)
if errCode != "" || errType == "card_error" {
return nil, fmt.Errorf("setup_intent error: code=%s, type=%s, message=%s", errCode, errType, errMsg)
}
}
// Check for requires_payment_method (card was rejected, need new card)
if status == "requires_payment_method" {
return nil, fmt.Errorf("setup_intent requires new payment method (card rejected)")
}
if status != "requires_action" {
// succeeded or other non-challenge status
return cr, nil
}
cr.RequiresAction = true
cr.SetupIntentID, _ = si["id"].(string)
cr.ClientSecret, _ = si["client_secret"].(string)
// Extract challenge details from next_action.use_stripe_sdk.stripe_js
nextAction, _ := si["next_action"].(map[string]interface{})
if nextAction == nil {
return cr, nil
}
useSDK, _ := nextAction["use_stripe_sdk"].(map[string]interface{})
if useSDK == nil {
return cr, nil
}
// stripe_js contains site_key, rqdata, verification_url
stripeJS, _ := useSDK["stripe_js"].(map[string]interface{})
if stripeJS != nil {
cr.SiteKey, _ = stripeJS["site_key"].(string)
cr.RqData, _ = stripeJS["rqdata"].(string)
cr.VerificationURL, _ = stripeJS["verification_url"].(string)
}
// Fallback: top-level fields in use_stripe_sdk
if cr.SiteKey == "" {
cr.SiteKey, _ = useSDK["hcaptcha_site_key"].(string)
}
if cr.RqData == "" {
cr.RqData, _ = useSDK["hcaptcha_rqdata"].(string)
}
if cr.VerificationURL == "" {
cr.VerificationURL, _ = useSDK["verification_url"].(string)
}
log.Printf("[stripe] confirm requires_action: seti=%s, site_key=%s, has_rqdata=%v",
cr.SetupIntentID, cr.SiteKey, cr.RqData != "")
return cr, nil
}
// VerifyChallengeParams holds parameters for the verify_challenge call.
type VerifyChallengeParams struct {
SetupIntentID string // seti_xxx
ClientSecret string // seti_xxx_secret_xxx
CaptchaToken string // P1_eyJ... from hCaptcha
CaptchaEKey string // E1_... from hCaptcha (respKey)
PublishableKey string
StripeVersion string // API version e.g. "2025-03-31.basil"
UserAgent string
MUID string
SID string
}
// VerifyChallenge calls POST /v1/setup_intents/{seti}/verify_challenge to complete the hCaptcha challenge.
func VerifyChallenge(client *httpclient.Client, params *VerifyChallengeParams) error {
verifyURL := fmt.Sprintf("https://api.stripe.com/v1/setup_intents/%s/verify_challenge", params.SetupIntentID)
form := url.Values{}
form.Set("challenge_response_token", params.CaptchaToken)
if params.CaptchaEKey != "" {
form.Set("challenge_response_ekey", params.CaptchaEKey)
}
form.Set("client_secret", params.ClientSecret)
form.Set("captcha_vendor_name", "hcaptcha")
form.Set("key", params.PublishableKey)
form.Set("_stripe_version", stripeVersionWithBetas(params.StripeVersion))
req, err := http.NewRequest("POST", verifyURL, strings.NewReader(form.Encode()))
if err != nil {
return fmt.Errorf("create verify challenge request: %w", err)
}
setStripeHeaders(req, params.UserAgent)
req.AddCookie(&http.Cookie{Name: "__stripe_mid", Value: params.MUID})
req.AddCookie(&http.Cookie{Name: "__stripe_sid", Value: params.SID})
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("send verify challenge: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read verify challenge response: %w", err)
}
log.Printf("[stripe] verify_challenge response (status=%d): %s", resp.StatusCode, string(respBody))
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("verify challenge failed: status %d, body: %s", resp.StatusCode, string(respBody))
}
// Check that setup_intent status is now succeeded
var result map[string]interface{}
if err := json.Unmarshal(respBody, &result); err != nil {
return fmt.Errorf("parse verify challenge response: %w", err)
}
status, _ := result["status"].(string)
if status == "succeeded" {
log.Printf("[stripe] verify_challenge succeeded")
return nil
}
// Check for error
lastErr, _ := result["last_setup_error"].(map[string]interface{})
if lastErr != nil {
msg, _ := lastErr["message"].(string)
code, _ := lastErr["code"].(string)
return fmt.Errorf("verify challenge error: code=%s, message=%s", code, msg)
}
return fmt.Errorf("verify challenge unexpected status: %s", status)
}
// PaymentPagePollParams holds the query parameters for GET /v1/payment_pages/{cs_id}/poll.
type PaymentPagePollParams struct {
CheckoutSessionID string
PublishableKey string
StripeVersion string // API version e.g. "2025-03-31.basil"
UserAgent string
}
// PaymentPagePollResult captures the fields needed to determine whether Stripe has fully finalized checkout.
type PaymentPagePollResult struct {
State string
PaymentObjectStatus string
ReturnURL string
}
// PollPaymentPage fetches the current payment page state after confirm/verify_challenge.
func PollPaymentPage(client *httpclient.Client, params *PaymentPagePollParams) (*PaymentPagePollResult, error) {
q := url.Values{}
q.Set("key", params.PublishableKey)
q.Set("_stripe_version", stripeVersionWithBetas(params.StripeVersion))
pollURL := fmt.Sprintf(
"https://api.stripe.com/v1/payment_pages/%s/poll?%s",
params.CheckoutSessionID,
q.Encode(),
)
req, err := http.NewRequest("GET", pollURL, nil)
if err != nil {
return nil, fmt.Errorf("create poll request: %w", err)
}
setStripeHeaders(req, params.UserAgent)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("send poll request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read poll response: %w", err)
}
log.Printf("[stripe] payment page poll response (status=%d): %s", resp.StatusCode, string(respBody))
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("payment page poll failed: status %d, body: %s", resp.StatusCode, string(respBody))
}
var raw map[string]interface{}
if err := json.Unmarshal(respBody, &raw); err != nil {
return nil, fmt.Errorf("parse poll response: %w", err)
}
result := &PaymentPagePollResult{}
result.State, _ = raw["state"].(string)
result.PaymentObjectStatus, _ = raw["payment_object_status"].(string)
result.ReturnURL, _ = raw["return_url"].(string)
return result, nil
}
// ConfirmPaymentDirect confirms payment with inline card data — no init or payment_methods step needed.
// Flow: checkout → fingerprint(m.stripe.com/6) → confirm(inline card).
func ConfirmPaymentDirect(client *httpclient.Client, params *DirectConfirmParams) (*ConfirmResult, error) {
confirmURL := fmt.Sprintf("https://api.stripe.com/v1/payment_pages/%s/confirm", params.CheckoutSessionID)
returnURL := fmt.Sprintf(
"https://checkout.stripe.com/c/pay/%s?returned_from_redirect=true",
params.CheckoutSessionID,
)
form := url.Values{}
// Inline card data
form.Set("payment_method_data[type]", "card")
form.Set("payment_method_data[card][number]", params.CardNumber)
form.Set("payment_method_data[card][cvc]", params.CardCVC)
form.Set("payment_method_data[card][exp_month]", params.CardExpMonth)
form.Set("payment_method_data[card][exp_year]", params.CardExpYear)
form.Set("payment_method_data[billing_details][name]", params.BillingName)
if params.BillingEmail != "" {
form.Set("payment_method_data[billing_details][email]", params.BillingEmail)
}
form.Set("payment_method_data[billing_details][address][country]", params.BillingCountry)
if params.BillingAddress != "" {
form.Set("payment_method_data[billing_details][address][line1]", params.BillingAddress)
}
if params.BillingCity != "" {
form.Set("payment_method_data[billing_details][address][city]", params.BillingCity)
}
if params.BillingState != "" {
form.Set("payment_method_data[billing_details][address][state]", params.BillingState)
}
if params.BillingZip != "" {
form.Set("payment_method_data[billing_details][address][postal_code]", params.BillingZip)
}
form.Set("payment_method_data[allow_redisplay]", "unspecified")
form.Set("payment_method_data[pasted_fields]", "number,exp,cvc")
form.Set("payment_method_data[payment_user_agent]", fmt.Sprintf("stripe.js/%s; stripe-js-v3/%s; payment-element; deferred-intent",
params.BuildHash, params.BuildHash))
form.Set("payment_method_data[referrer]", "https://chatgpt.com")
form.Set("payment_method_data[time_on_page]", randomTimeOnPage())
// Fingerprint
form.Set("payment_method_data[guid]", params.GUID)
form.Set("payment_method_data[muid]", params.MUID)
form.Set("payment_method_data[sid]", params.SID)
form.Set("guid", params.GUID)
form.Set("muid", params.MUID)
form.Set("sid", params.SID)
// Build hash version — required by Stripe, was missing from direct confirm
form.Set("version", params.BuildHash)
if params.InitChecksum != "" {
form.Set("init_checksum", params.InitChecksum)
}
// Payment details
form.Set("expected_amount", fmt.Sprintf("%d", params.ExpectedAmount))
form.Set("expected_payment_method_type", "card")
form.Set("return_url", returnURL)
// Elements session client — required for Stripe risk assessment, was missing from direct confirm
form.Set("elements_session_client[client_betas][0]", "custom_checkout_server_updates_1")
form.Set("elements_session_client[client_betas][1]", "custom_checkout_manual_approval_1")
form.Set("elements_session_client[elements_init_source]", "custom_checkout")
form.Set("elements_session_client[referrer_host]", "chatgpt.com")
form.Set("elements_session_client[stripe_js_id]", params.StripeJsID)
form.Set("elements_session_client[locale]", localeForCountry(params.BillingCountry))
form.Set("elements_session_client[is_aggregation_expected]", "false")
// Client attribution metadata — required for Stripe risk assessment, was missing from direct confirm
form.Set("client_attribution_metadata[merchant_integration_source]", "checkout")
form.Set("client_attribution_metadata[merchant_integration_version]", "custom")
form.Set("client_attribution_metadata[merchant_integration_subtype]", "payment-element")
form.Set("client_attribution_metadata[merchant_integration_additional_elements][0]", "payment")
form.Set("client_attribution_metadata[merchant_integration_additional_elements][1]", "address")
form.Set("client_attribution_metadata[payment_intent_creation_flow]", "deferred")
form.Set("client_attribution_metadata[payment_method_selection_flow]", "automatic")
// js_checksum and rv_timestamp (Stripe.js anti-bot fields)
ppageID := params.PPageID
if ppageID == "" {
ppageID = params.CheckoutSessionID
}
form.Set("js_checksum", JsChecksum(ppageID))
form.Set("rv_timestamp", RvTimestamp())
// Passive captcha token (from telemetry)
if params.PassiveCaptchaToken != "" {
form.Set("passive_captcha[passive_token]", params.PassiveCaptchaToken)
form.Set("passive_captcha[vendor]", "hcaptcha")
}
// Consent & auth
form.Set("consent[terms_of_service]", "accepted")
form.Set("key", params.PublishableKey)
form.Set("_stripe_version", stripeVersionWithBetas(params.StripeVersion))
req, err := http.NewRequest("POST", confirmURL, strings.NewReader(form.Encode()))
if err != nil {
return nil, fmt.Errorf("create direct confirm request: %w", err)
}
setStripeHeaders(req, params.UserAgent)
req.AddCookie(&http.Cookie{Name: "__stripe_mid", Value: params.MUID})
req.AddCookie(&http.Cookie{Name: "__stripe_sid", Value: params.SID})
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("send direct confirm request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read direct confirm response: %w", err)
}
log.Printf("[stripe] direct confirm response (status=%d): %s", resp.StatusCode, string(respBody))
if resp.StatusCode != http.StatusOK {
if strings.Contains(string(respBody), "card_declined") {
return nil, fmt.Errorf("confirm payment: %w", ErrCardDeclined)
}
return nil, fmt.Errorf("direct confirm failed: status %d, body: %s", resp.StatusCode, string(respBody))
}
return parseConfirmResponse(respBody)
}
// setStripeHeaders sets common headers for Stripe API requests.
func setStripeHeaders(req *http.Request, userAgent string) {
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Origin", stripeOrigin)
req.Header.Set("Referer", stripeOrigin+"/")
if userAgent != "" {
req.Header.Set("User-Agent", userAgent)
}
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Site", "cross-site")
req.Header.Set("Sec-Ch-Ua", chromeSecChUa)
req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
req.Header.Set("Sec-Ch-Ua-Platform", `"Windows"`)
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
}