StartPurchase + ListPublicPolicies (parity with TS / Rust / Python 0.2.0)
The Go SDK's online client previously only exposed Validate +
PublicKey. This adds the purchase-side surface so Go consumers
have the same capabilities as the other three language clients:
- StartPurchaseOptions struct (BuyerEmail, BuyerNote, RedirectURL,
Code, PolicySlug). Zero-valued fields are omitted from the
JSON request body.
- Client.StartPurchase(ctx, productSlug, opts) → PurchaseSession
with InvoiceID, BTCPayInvoiceID, CheckoutURL, AmountSats,
PollURL.
- Client.ListPublicPolicies(ctx, productSlug) →
PublicPoliciesResponse for rendering an in-app tier picker.
Public endpoint, no auth.
session, err := client.StartPurchase(ctx, "recap",
keysat.StartPurchaseOptions{
PolicySlug: "pro",
BuyerEmail: "buyer@example.com",
RedirectURL: "https://recap.app/thank-you",
})
// open session.CheckoutURL in the buyer's browser
tiers, err := client.ListPublicPolicies(ctx, "recap")
for _, p := range tiers.Policies {
fmt.Println(p.Slug, p.Name, p.PriceSats, p.Entitlements)
}
Build + existing crosscheck tests pass clean.
This commit is contained in:
@@ -154,3 +154,140 @@ func (c *Client) PublicKey(ctx context.Context) (string, error) {
|
|||||||
}
|
}
|
||||||
return wrap.PublicKeyPEM, nil
|
return wrap.PublicKeyPEM, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartPurchaseOptions captures the optional fields on POST /v1/purchase.
|
||||||
|
// Zero-valued fields are omitted from the request. To buy a specific
|
||||||
|
// tier, set PolicySlug — list available tiers with ListPublicPolicies.
|
||||||
|
type StartPurchaseOptions struct {
|
||||||
|
BuyerEmail string
|
||||||
|
BuyerNote string
|
||||||
|
RedirectURL string
|
||||||
|
Code string
|
||||||
|
// PolicySlug — when set, the licensing service prices the invoice
|
||||||
|
// at the policy's price_sats_override and the issued license
|
||||||
|
// carries that policy's entitlements / duration / max_machines /
|
||||||
|
// trial flag. When omitted, falls back to the product's default
|
||||||
|
// policy.
|
||||||
|
PolicySlug string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurchaseSession is the response from POST /v1/purchase.
|
||||||
|
type PurchaseSession struct {
|
||||||
|
InvoiceID string `json:"invoice_id"`
|
||||||
|
BTCPayInvoiceID string `json:"btcpay_invoice_id"`
|
||||||
|
CheckoutURL string `json:"checkout_url"`
|
||||||
|
AmountSats int64 `json:"amount_sats"`
|
||||||
|
BasePriceSats int64 `json:"base_price_sats,omitempty"`
|
||||||
|
DiscountApplied int64 `json:"discount_applied_sats,omitempty"`
|
||||||
|
PollURL string `json:"poll_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartPurchase opens a purchase invoice with the daemon. The buyer
|
||||||
|
// opens the returned CheckoutURL in their browser; once payment
|
||||||
|
// settles, the issued license_key is available via PollPurchase
|
||||||
|
// (or the corresponding webhook).
|
||||||
|
func (c *Client) StartPurchase(ctx context.Context, productSlug string, opts StartPurchaseOptions) (PurchaseSession, error) {
|
||||||
|
body := map[string]any{"product": productSlug}
|
||||||
|
if opts.BuyerEmail != "" {
|
||||||
|
body["buyer_email"] = opts.BuyerEmail
|
||||||
|
}
|
||||||
|
if opts.BuyerNote != "" {
|
||||||
|
body["buyer_note"] = opts.BuyerNote
|
||||||
|
}
|
||||||
|
if opts.RedirectURL != "" {
|
||||||
|
body["redirect_url"] = opts.RedirectURL
|
||||||
|
}
|
||||||
|
if opts.Code != "" {
|
||||||
|
body["code"] = opts.Code
|
||||||
|
}
|
||||||
|
if opts.PolicySlug != "" {
|
||||||
|
body["policy_slug"] = opts.PolicySlug
|
||||||
|
}
|
||||||
|
jsonBody, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return PurchaseSession{}, err
|
||||||
|
}
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/v1/purchase", strings.NewReader(string(jsonBody)))
|
||||||
|
if err != nil {
|
||||||
|
return PurchaseSession{}, err
|
||||||
|
}
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := c.HTTP.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return PurchaseSession{}, fmt.Errorf("start_purchase: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return PurchaseSession{}, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return PurchaseSession{}, fmt.Errorf("daemon returned HTTP %d: %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
var session PurchaseSession
|
||||||
|
if err := json.Unmarshal(respBody, &session); err != nil {
|
||||||
|
return PurchaseSession{}, fmt.Errorf("decode purchase response: %w", err)
|
||||||
|
}
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicPolicy is one tier in the buyer-visible policy list.
|
||||||
|
type PublicPolicy struct {
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
PriceSats int64 `json:"price_sats"`
|
||||||
|
DurationSeconds int64 `json:"duration_seconds"`
|
||||||
|
MaxMachines int64 `json:"max_machines"`
|
||||||
|
IsTrial bool `json:"is_trial"`
|
||||||
|
Entitlements []string `json:"entitlements"`
|
||||||
|
Highlighted bool `json:"highlighted"`
|
||||||
|
IsRecurring bool `json:"is_recurring"`
|
||||||
|
RenewalPeriodDays int64 `json:"renewal_period_days"`
|
||||||
|
TrialDays int64 `json:"trial_days"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicPoliciesProduct is the product-level fields on the public
|
||||||
|
// policies response.
|
||||||
|
type PublicPoliciesProduct struct {
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
BasePriceSats int64 `json:"base_price_sats"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicPoliciesResponse is the response from GET /v1/products/<slug>/policies.
|
||||||
|
type PublicPoliciesResponse struct {
|
||||||
|
Product PublicPoliciesProduct `json:"product"`
|
||||||
|
Policies []PublicPolicy `json:"policies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPublicPolicies returns the buyer-visible tier list for a
|
||||||
|
// product. No auth required — same data the licensing service's
|
||||||
|
// /buy/<slug> page reads server-side. Use this to render an in-app
|
||||||
|
// tier picker that stays in sync with the operator's admin-side
|
||||||
|
// tier setup.
|
||||||
|
func (c *Client) ListPublicPolicies(ctx context.Context, productSlug string) (PublicPoliciesResponse, error) {
|
||||||
|
url := c.BaseURL + "/v1/products/" + productSlug + "/policies"
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return PublicPoliciesResponse{}, err
|
||||||
|
}
|
||||||
|
resp, err := c.HTTP.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return PublicPoliciesResponse{}, fmt.Errorf("list_public_policies: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return PublicPoliciesResponse{}, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return PublicPoliciesResponse{}, fmt.Errorf("daemon returned HTTP %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
var out PublicPoliciesResponse
|
||||||
|
if err := json.Unmarshal(body, &out); err != nil {
|
||||||
|
return PublicPoliciesResponse{}, fmt.Errorf("decode policies response: %w", err)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user