mirror of
https://github.com/hanzoai/karma-style.git
synced 2026-10-30 18:33:08 +00:00
Two RED must-fix findings on the checkout.js widget path.
[high] checkout.js widget mis-charged every line as a $50 "MEGA Hat".
lineItemsOf() sent {id,name,unitPrice,quantity} and the vendored SDK's M()
mapper dropped productSlug/productId/variantSku, so the commerce server
(hasCatalogRef = productId||productSlug||variantSku) never re-priced and each
line fell to the legacy custom-hat branch (safeName('')='MEGA Hat', $50). The
server IGNORES client unitPrice, so this was a missing-catalog-ref defect, not
price tampering. Fix: lineItemsOf() now sets productSlug (i.slug), and M() in
vendor/checkout.global.js forwards productSlug/productId/variantSku so the ref
reaches the wire and the server prices the real product. Verified end-to-end
through the actual vendored SDK (productSlug reaches POST /v1/checkout/sessions).
[medium] GA4/Meta `purchase` fired for unpaid orders. thankYou() fired on ANY
/thank-you visit using a stash written at checkout START (pre-payment). Fix:
fire only on a proven paid round-trip — require ?ok=1 AND ?txn === the stashed
transaction_id; the confirmation detail line is gated the same way. Added a
cancel marker (cancelUrl -> /shop?checkout=cancel) that clears the pre-payment
stash and keeps the bag. The refresh once-guard (takePendingOrder clears the
key) and the Square fallback (no stash, no query -> never fired) are unaffected.
verify.html: purchase row + section 3 now state the paid-proof gate and the
catalog-ref repricing (place one live test order before enabling SPA_CHECKOUT_KEY
in prod). Bumped the asset cache-bust token 052 -> 053 in lockstep so the changed
JS surfaces past the 24h edge cache. Vendored bundle carries a forward-fix header
(pending upstream checkout.js >= 2.1.23). Build GREEN: node --check all site/*.js.
7 lines
8.9 KiB
JavaScript
7 lines
8.9 KiB
JavaScript
// karma forward-fix (NOT in stock [email protected]): mapper M() forwards
|
|
// productSlug/productId/variantSku so the commerce server can reprice widget line
|
|
// items by catalog reference. Without it every line falls to the legacy custom-item
|
|
// branch (wrong name/price). Carry this forward on any re-vendor until it lands
|
|
// upstream (checkout.js >= 2.1.23). Pairs with site/checkout.js lineItemsOf().
|
|
"use strict";var HanzoCheckout=(()=>{var p=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var S=Object.getOwnPropertyNames;var w=Object.prototype.hasOwnProperty;var I=(r,e)=>{for(var t in e)p(r,t,{get:e[t],enumerable:!0})},R=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of S(e))!w.call(r,i)&&i!==t&&p(r,i,{get:()=>e[i],enumerable:!(n=C(e,i))||n.enumerable});return r};var q=r=>R(p({},"__esModule",{value:!0}),r);var z={};I(z,{HanzoCheckout:()=>c,create:()=>f,createCheckout:()=>s,default:()=>A,init:()=>y,open:()=>k,popup:()=>T});var x="https://api.hanzo.ai";var g=class{constructor(e={}){this.baseUrl=(e.baseUrl??e.commerceUrl??x).replace(/\/+$/,""),this.token=e.token,this.timeoutMs=e.timeoutMs??15e3}setToken(e){this.token=e}async request(e,t){let n=new URL(e,this.baseUrl);if(t?.params)for(let[a,m]of Object.entries(t.params))n.searchParams.set(a,m);let i=new AbortController,u=setTimeout(()=>i.abort(),this.timeoutMs),o={Accept:"application/json"},d=t?.token??this.token;d&&(o.Authorization=`Bearer ${d}`),t?.body&&(o["Content-Type"]="application/json"),t?.headers&&Object.assign(o,t.headers);try{let a=await fetch(n.toString(),{method:t?.method??"GET",headers:o,body:t?.body?JSON.stringify(t.body):void 0,signal:i.signal});if(!a.ok){let m=await a.text().catch(()=>"");throw new l(a.status,`${a.statusText}: ${m}`.trim())}return await a.json()}finally{clearTimeout(u)}}async getBalance(e,t="usd",n,i){return this.request("/v1/billing/balance",{params:{user:e,currency:t},token:n,headers:i})}async getAllBalances(e,t){return this.request("/v1/billing/balance/all",{params:{user:e},token:t})}async getTier(e,t){return this.request("/v1/billing/tier",{params:{user:e},token:t?.token,headers:t?.headers})}async addUsageRecord(e,t,n){return this.request("/v1/billing/usage",{method:"POST",body:e,token:t,headers:n})}async getUsageRecords(e,t="usd",n){return this.request("/v1/billing/usage",{params:{user:e,currency:t},token:n})}async addDeposit(e,t){return this.request("/v1/billing/deposit",{method:"POST",body:e,token:t})}async grantStarterCredit(e,t){return this.request("/v1/billing/credit",{method:"POST",body:{user:e},token:t})}async getPlans(e){return this.request("/v1/billing/plans",{token:e})}async getPlan(e,t){try{return await this.request(`/v1/billing/plans/${e}`,{token:t})}catch{return null}}async subscribe(e,t){return this.request("/v1/billing/subscriptions",{method:"POST",body:e,token:t})}async getSubscription(e,t){try{return await this.request(`/v1/billing/subscriptions/${e}`,{token:t})}catch{return null}}async listSubscriptions(e,t){return this.request("/v1/billing/subscriptions",{params:e,token:t})}async updateSubscription(e,t,n){return this.request(`/v1/billing/subscriptions/${e}`,{method:"PATCH",body:t,token:n})}async cancelSubscription(e,t=!1,n){return this.request(`/v1/billing/subscriptions/${e}/cancel`,{method:"POST",body:{immediately:t},token:n})}async reactivateSubscription(e,t){return this.request(`/v1/billing/subscriptions/${e}/reactivate`,{method:"POST",token:t})}async createCheckoutSession(e,t){return this.request("/v1/checkout/sessions",{method:"POST",body:e,token:t})}async tokenizeCard(e,t){return this.request("/v1/billing/card/tokenize",{method:"POST",body:{number:e.number.replace(/\s/g,""),expiry_month:e.expiryMonth,expiry_year:e.expiryYear,cvc:e.cvc,name:e.name,zip:e.zip},token:t})}async addPaymentMethod(e,t){return this.request("/v1/billing/payment-methods",{method:"POST",body:e,token:t})}async listPaymentMethods(e,t){return this.request("/v1/billing/payment-methods",{params:{customerId:e},token:t})}async removePaymentMethod(e,t){await this.request(`/v1/billing/payment-methods/${e}`,{method:"DELETE",token:t})}async setDefaultPaymentMethod(e,t,n){return this.request(`/v1/billing/customers/${e}/default-payment-method`,{method:"POST",body:{paymentMethodId:t},token:n})}async validateCoupon(e,t,n){try{return{valid:!0,coupon:await this.request("/v1/coupon/validate",{method:"POST",body:{code:e.toUpperCase().trim(),subtotalCents:t},token:n})}}catch(i){return{valid:!1,error:i instanceof l?i.message:"Invalid coupon"}}}async redeemCoupon(e,t,n){return this.request("/v1/coupon/redeem",{method:"POST",body:{code:e.toUpperCase().trim(),userId:t},token:n})}async getOrCreateReferrer(e,t){return this.request("/v1/referrer",{method:"POST",body:{userId:e},token:t})}async getReferrals(e,t){return this.request(`/v1/user/${e}/referrals`,{token:t})}async getReferrers(e,t){return this.request(`/v1/user/${e}/referrers`,{token:t})}async getAffiliate(e,t){try{return await this.request(`/v1/user/${e}/affiliate`,{token:t})}catch{return null}}async createAffiliate(e,t){return this.request("/v1/affiliate",{method:"POST",body:{userId:e},token:t})}async getAffiliateReferrals(e,t){return this.request(`/v1/affiliate/${e}/referrals`,{token:t})}async getAffiliateOrders(e,t){return this.request(`/v1/affiliate/${e}/orders`,{token:t})}async getAffiliateTransactions(e,t){return this.request(`/v1/affiliate/${e}/transactions`,{token:t})}async authorize(e,t){return this.request(`/v1/authorize/${e}`,{method:"POST",token:t})}async capture(e,t){return this.request(`/v1/capture/${e}`,{method:"POST",token:t})}async charge(e,t){return this.request(`/v1/charge/${e}`,{method:"POST",token:t})}async refund(e,t){return this.request(`/v1/refund/${e}`,{method:"POST",token:t})}async billingRefund(e,t){return this.request("/v1/billing/refund",{method:"POST",body:e,token:t})}async getWalletAddress(e,t){return this.request("/api/wallet/account",{method:"POST",body:{name:`${e.userId}-${e.chain}`,blockchainType:e.chain},token:t})}async addBankAccount(e,t){return this.request("/v1/billing/payment-methods",{method:"POST",body:{customerId:e.customerId,type:"bank_account",plaidToken:e.plaidToken,bankName:e.bankName,accountType:e.accountType},token:t})}async addCryptoWallet(e,t){return this.request("/v1/billing/payment-methods",{method:"POST",body:{customerId:e.customerId,type:"crypto",chain:e.chain,address:e.address,label:e.label},token:t})}async topup(e,t){return this.request("/v1/billing/topup",{method:"POST",body:e,token:t})}};var l=class extends Error{constructor(e,t){super(t),this.name="CommerceApiError",this.status=e}};var U="width=480,height=760,menubar=no,toolbar=no,location=no,status=no";function O(r){return r.toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"item"}function M(r){return{id:r.id??O(r.name),name:r.name,price:r.unitPrice,quantity:r.quantity??1,description:r.description,imageUrl:r.imageUrl,productSlug:r.productSlug,productId:r.productId,variantSku:r.variantSku}}function h(){return typeof window<"u"?window.location.href:""}var c=class{constructor(e){this.urls=new Map;this.options=e,this.commerce=new g({baseUrl:e.baseUrl,token:e.token??e.apiKey})}async createSession(e){let t=await this.commerce.createCheckoutSession({items:e.lineItems.map(M),successUrl:e.successUrl??h(),cancelUrl:e.cancelUrl??h(),currency:e.currency??this.options.currency,couponCode:e.couponCode,customer:e.customer,metadata:e.metadata});return this.urls.set(t.sessionId,t.checkoutUrl),{...t,id:t.sessionId,url:t.checkoutUrl}}resolve(e){if(typeof e!="string")return e.url;if(/^https?:\/\//i.test(e))return e;let t=this.urls.get(e);if(!t)throw new Error(`checkout.js: unknown session "${e}"`);return t}redirectToCheckout(e){typeof window>"u"||window.location.assign(this.resolve(e))}openPopup(e){return typeof window>"u"?null:window.open(this.resolve(e),"hanzo_checkout",U)}};function s(r){return new c(r)}var b="__hanzoCheckoutBound";function y(r){if(typeof document>"u")return;let e=()=>{document.querySelectorAll("[data-hanzo-checkout]").forEach(t=>{let n=t;n[b]||(n[b]=!0,t.addEventListener("click",i=>{i.preventDefault();let u=t.dataset.hanzoKey??r;if(!u)throw new Error("checkout.js: missing data-hanzo-key or init(apiKey)");let o=s({apiKey:u});o.createSession({lineItems:[{name:t.dataset.hanzoName??"Order",unitPrice:Number(t.dataset.hanzoAmount??"0"),quantity:1}],currency:t.dataset.hanzoCurrency}).then(d=>o.redirectToCheckout(d))}))})};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e,{once:!0}):e()}var f=s;function P(r){return r.lineItems??[{name:"Order",unitPrice:r.amount,quantity:1}]}async function k(r){let e=s({apiKey:r.apiKey}),t=await e.createSession({lineItems:P(r),currency:r.currency,successUrl:r.successUrl,cancelUrl:r.cancelUrl});e.redirectToCheckout(t)}async function T(r){let e=s({apiKey:r.apiKey}),t=await e.createSession({lineItems:P(r),currency:r.currency});return e.openPopup(t)}var v={create:f,createCheckout:s,HanzoCheckout:c,init:y,open:k,popup:T},A=v;typeof window<"u"&&(window.HanzoCheckout=v);return q(z);})();
|