mirror of
https://github.com/tavo-wasd-gh/conex-builder.git
synced 2025-06-07 12:13:30 -06:00
better ui, paypal order ready, subscription still missing
This commit is contained in:
parent
f2e07946d0
commit
1ff85e982a
8 changed files with 229 additions and 53 deletions
59
main.go
59
main.go
|
@ -10,10 +10,24 @@ import (
|
|||
"os/signal"
|
||||
"syscall"
|
||||
"bytes"
|
||||
// "time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type OrderData struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
PurchaseUnits []struct {
|
||||
Payments struct {
|
||||
Captures []struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
} `json:"captures"`
|
||||
} `json:"payments"`
|
||||
} `json:"purchase_units"`
|
||||
}
|
||||
|
||||
var (
|
||||
baseURL = "https://api-m.sandbox.paypal.com"
|
||||
)
|
||||
|
@ -27,6 +41,7 @@ func init() {
|
|||
func main() {
|
||||
// Handlers
|
||||
http.HandleFunc("/api/orders", CreateOrder)
|
||||
http.HandleFunc("/api/orders/", CaptureOrder)
|
||||
http.Handle("/", http.FileServer(http.Dir("./public")))
|
||||
|
||||
// Channel to listen for signals
|
||||
|
@ -121,3 +136,47 @@ func CreateOrder(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
func CaptureOrder(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/orders/")
|
||||
parts := strings.Split(path, "/")
|
||||
orderID := parts[0]
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders/"+orderID+"/capture", nil)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := Token()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get access token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to send request", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Create an instance of AutoGenerated
|
||||
var result OrderData
|
||||
|
||||
// Decode the response into the AutoGenerated struct
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
http.Error(w, "Failed to decode response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Now, `result` contains the entire structured response
|
||||
// You can send the whole `result` back to the client, or you can selectively send fields.
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(result); err != nil {
|
||||
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
|
122
public/form.js
122
public/form.js
|
@ -46,10 +46,10 @@ function togglePaymentMethod(selectedButtonId) {
|
|||
|
||||
if (selectedButtonId === 'showOneTimeButton') {
|
||||
document.getElementById('paypal-button-container').classList.add('active');
|
||||
document.getElementById('paypalOneTimeButton').classList.add('active');
|
||||
document.getElementById('paypal-button-container-order').classList.add('active');
|
||||
} else if (selectedButtonId === 'showSubButton') {
|
||||
document.getElementById('paypal-button-container').classList.add('active');
|
||||
document.getElementById('paypalSubButton').classList.add('active');
|
||||
document.getElementById('paypal-button-container-subscribe').classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,37 +57,99 @@ function isFormValid(form) {
|
|||
return form.checkValidity();
|
||||
}
|
||||
|
||||
paypal_onetime.Buttons({
|
||||
window.paypal_order.Buttons({
|
||||
style: { shape: 'pill', color: 'black', layout: 'vertical', label: 'pay' },
|
||||
createOrder: function(data, actions) {
|
||||
return actions.order.create({
|
||||
intent: 'CAPTURE',
|
||||
purchase_units: [{
|
||||
amount: {
|
||||
currency_code: 'USD',
|
||||
value: '20.00'
|
||||
}
|
||||
}]
|
||||
});
|
||||
},
|
||||
onApprove: function(data, actions) {
|
||||
return actions.order.capture().then(function(details) {
|
||||
alert('Transaction completed by ' + details.payer.name.given_name);
|
||||
});
|
||||
}
|
||||
}).render("#paypalOneTimeButton");
|
||||
async createOrder() {
|
||||
try {
|
||||
const response = await fetch("/api/orders", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
// use the "body" param to optionally pass additional order information
|
||||
// like product ids and quantities
|
||||
body: JSON.stringify({
|
||||
cart: [
|
||||
{
|
||||
id: "YOUR_PRODUCT_ID",
|
||||
quantity: "YOUR_PRODUCT_QUANTITY",
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
paypal_subscribe.Buttons({
|
||||
style: { shape: 'pill', color: 'black', layout: 'vertical', label: 'subscribe' },
|
||||
createSubscription: function(data, actions) {
|
||||
return actions.subscription.create({
|
||||
plan_id: PlanID
|
||||
});
|
||||
const orderData = await response.json();
|
||||
|
||||
if (orderData.id) {
|
||||
return orderData.id;
|
||||
} else {
|
||||
const errorDetail = orderData?.details?.[0];
|
||||
const errorMessage = errorDetail
|
||||
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
|
||||
: JSON.stringify(orderData);
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
|
||||
}
|
||||
},
|
||||
onApprove: function(data, actions) {
|
||||
alert(data.subscriptionID); // You can add optional success message for the subscriber here
|
||||
}
|
||||
}).render('#paypalSubButton');
|
||||
async onApprove(data, actions) {
|
||||
try {
|
||||
const response = await fetch(`/api/orders/${data.orderID}/capture`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const orderData = await response.json();
|
||||
// Three cases to handle:
|
||||
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
|
||||
// (2) Other non-recoverable errors -> Show a failure message
|
||||
// (3) Successful transaction -> Show confirmation or thank you message
|
||||
|
||||
const errorDetail = orderData?.details?.[0];
|
||||
|
||||
if (errorDetail?.issue === "INSTRUMENT_DECLINED") {
|
||||
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
|
||||
// recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
|
||||
return actions.restart();
|
||||
} else if (errorDetail) {
|
||||
// (2) Other non-recoverable errors -> Show a failure message
|
||||
throw new Error(`${errorDetail.description} (${orderData.debug_id})`);
|
||||
} else if (!orderData.purchase_units) {
|
||||
throw new Error(JSON.stringify(orderData));
|
||||
} else {
|
||||
// (3) Successful transaction -> Show confirmation or thank you message
|
||||
// Or go to another URL: actions.redirect('thank_you.html');
|
||||
const transaction =
|
||||
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
|
||||
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
|
||||
resultMessage(
|
||||
`Transaction ${transaction.status}: ${transaction.id}<br><br>See console for all available details`,
|
||||
);
|
||||
console.log(
|
||||
"Capture result",
|
||||
orderData,
|
||||
JSON.stringify(orderData, null, 2),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
resultMessage(
|
||||
`Sorry, your transaction could not be processed...<br><br>${error}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
}).render("#paypal-button-container-order");
|
||||
|
||||
// Example function to show a result to the user. Your site's UI library can be used instead.
|
||||
function resultMessage(message) {
|
||||
const container = document.querySelector("#checkout");
|
||||
container.innerHTML = message;
|
||||
}
|
||||
|
||||
document.getElementById('showOneTimeButton').addEventListener('click', function() {
|
||||
if (isFormValid(form)) {
|
||||
|
|
|
@ -26,20 +26,23 @@
|
|||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
|
||||
<!-- EDITORJS -->
|
||||
<script
|
||||
src="https://www.paypal.com/sdk/js?client-id=AcCW43LI1S6lLQgtLkF4V8UOPfmXcqXQ8xfEl41hRuMxSskR2jkWNwQN6Ab1WK7E2E52GNaoYBHqgIKd&disable-funding=venmo¤cy=USD"
|
||||
data-namespace="paypal_onetime"
|
||||
></script>
|
||||
<script
|
||||
src="https://www.paypal.com/sdk/js?client-id=AUJPUXq47cceshojipubmg0wvCgdJC-bg4O4xvMQf_ic1MzlS27OVW6EGpymJtFXssAiXNOwBQDKqWiE&vault=true&intent=subscription"
|
||||
data-namespace="paypal_subscribe"
|
||||
src="https://www.paypal.com/sdk/js?client-id=AcCW43LI1S6lLQgtLkF4V8UOPfmXcqXQ8xfEl41hRuMxSskR2jkWNwQN6Ab1WK7E2E52GNaoYBHqgIKd&components=buttons&enable-funding=card&disable-funding=paylater,venmo"
|
||||
data-namespace="paypal_order"
|
||||
></script>
|
||||
<!-- <script -->
|
||||
<!-- src="" -->
|
||||
<!-- data-namespace="paypal_subscribe" -->
|
||||
<!-- ></script> -->
|
||||
</head>
|
||||
<form action="submit.php" id="mainForm" method="post">
|
||||
<div class="banner" style="background-image: url(/static/banner.jpg);">
|
||||
<div class="desc">
|
||||
<input type="text" name="title" class="input-title" placeholder="[Nombre Ejemplo]">
|
||||
<input type="text" name="slogan" class="input-slogan" placeholder="[Slogan llamativo o breve descripción]">
|
||||
<button type="button" id="openDialogButton">Solicitar por $20/año</button>
|
||||
<button type="button" id="openDialogButton">
|
||||
<img src="/static/svg/cart.svg">
|
||||
<span >$20/año</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<body>
|
||||
|
@ -52,7 +55,12 @@
|
|||
<div id="dialog">
|
||||
<div>
|
||||
<h2>Información de Contacto</h2>
|
||||
<button id="cancelDialogButton" type="button">x</button>
|
||||
<button id="cancelDialogButton" type="button">
|
||||
<picture>
|
||||
<source srcset="/static/svg/xd.svg" media="(prefers-color-scheme: dark)">
|
||||
<img src="/static/svg/x.svg" style="width: 0.7em; height: 0.7em;" alt="Close" id="closeIcon">
|
||||
</picture>
|
||||
</button>
|
||||
</div>
|
||||
<p>Utilizaremos esta información para contactarle acerca de la publicación del sitio.</p>
|
||||
<div id="form-container">
|
||||
|
@ -66,14 +74,15 @@
|
|||
<p><strong>Pago Automático:</strong> Requiere cuenta de PayPal para rebajo automático, si no tiene una le pedirá configurar rápidamente los datos.</p>
|
||||
</div>
|
||||
<div id="warning-message"><p>Por favor digite los campos requeridos.</p></div>
|
||||
<div id="method-button-container">
|
||||
<button id="showOneTimeButton" type="button">Pago Único</button>
|
||||
<button id="showSubButton" type="button">Pago Automático</button>
|
||||
</div>
|
||||
<div id="error-with-payment"></div>
|
||||
<div id="paypal-button-container">
|
||||
<div id="paypalOneTimeButton"></div>
|
||||
<div id="paypalSubButton"></div>
|
||||
<div id="checkout">
|
||||
<div id="method-button-container">
|
||||
<button id="showOneTimeButton" type="button">Pago Único</button>
|
||||
<button id="showSubButton" type="button">Pago Automático</button>
|
||||
</div>
|
||||
<div id="paypal-button-container">
|
||||
<div id="paypal-button-container-order"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -25,6 +25,9 @@
|
|||
--hover-border: #505050;
|
||||
--hyper-color: #00b4db;
|
||||
}
|
||||
#closeIcon {
|
||||
content: url('/static/svg/xd.svg');
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
@ -208,6 +211,8 @@ button {
|
|||
}
|
||||
|
||||
#openDialogButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
bottom: 2em;
|
||||
right: 1em;
|
||||
|
@ -273,6 +278,19 @@ button {
|
|||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
#checkout {
|
||||
}
|
||||
|
||||
#openDialogButton img {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#openDialogButton span {
|
||||
margin-left: 0.6em;
|
||||
}
|
||||
|
||||
/* Custom SimpleMDE styling */
|
||||
.CodeMirror {
|
||||
border-radius: 10px;
|
||||
|
@ -286,5 +304,3 @@ button {
|
|||
font-size: 1em;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -71,9 +71,9 @@ var editor = new EditorJS({
|
|||
type : 'list',
|
||||
data : {
|
||||
items : [
|
||||
'It is a block-styled editor',
|
||||
'It returns clean data output in JSON',
|
||||
'Designed to be extendable and pluggable with a simple API',
|
||||
'Resolvemos una necesidad clave de mercado',
|
||||
'Inversión en crecimiento con presupuesto sostenible.',
|
||||
'Enfoque en satisfacción del cliente',
|
||||
],
|
||||
style: 'unordered'
|
||||
}
|
||||
|
@ -82,9 +82,9 @@ var editor = new EditorJS({
|
|||
type: 'table',
|
||||
data: {
|
||||
content: [
|
||||
['Header 1', 'Header 2', 'Header 3'],
|
||||
['Row 1, Cell 1', 'Row 1, Cell 2', 'Row 1, Cell 3'],
|
||||
['Row 2, Cell 1', 'Row 2, Cell 2', 'Row 2, Cell 3']
|
||||
['Servicios', 'Descripción', 'Costo'],
|
||||
['Impresión', 'Breve descripción', '1000'],
|
||||
['laminado', 'Breve descripción', '2000'],
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
5
public/static/svg/cart.svg
Normal file
5
public/static/svg/cart.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="#fff" d="M528.12 301.319l47.273-208C578.806 78.301 567.391 64 551.99 64H159.208l-9.166-44.81C147.758 8.021 137.93 0 126.529 0H24C10.745 0 0 10.745 0 24v16c0 13.255 10.745 24 24 24h69.883l70.248 343.435C147.325 417.1 136 435.222 136 456c0 30.928 25.072 56 56 56s56-25.072 56-56c0-15.674-6.447-29.835-16.824-40h209.647C430.447 426.165 424 440.326 424 456c0 30.928 25.072 56 56 56s56-25.072 56-56c0-22.172-12.888-41.332-31.579-50.405l5.517-24.276c3.413-15.018-8.002-29.319-23.403-29.319H218.117l-6.545-32h293.145c11.206 0 20.92-7.754 23.403-18.681z"/></svg>
|
||||
<!--
|
||||
Font Awesome Free 5.2.0 by @fontawesome - https://fontawesome.com
|
||||
License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
-->
|
After Width: | Height: | Size: 804 B |
12
public/static/svg/x.svg
Normal file
12
public/static/svg/x.svg
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 460.775 460.775" xml:space="preserve">
|
||||
<path d="M285.08,230.397L456.218,59.27c6.076-6.077,6.076-15.911,0-21.986L423.511,4.565c-2.913-2.911-6.866-4.55-10.992-4.55
|
||||
c-4.127,0-8.08,1.639-10.993,4.55l-171.138,171.14L59.25,4.565c-2.913-2.911-6.866-4.55-10.993-4.55
|
||||
c-4.126,0-8.08,1.639-10.992,4.55L4.558,37.284c-6.077,6.075-6.077,15.909,0,21.986l171.138,171.128L4.575,401.505
|
||||
c-6.074,6.077-6.074,15.911,0,21.986l32.709,32.719c2.911,2.911,6.865,4.55,10.992,4.55c4.127,0,8.08-1.639,10.994-4.55
|
||||
l171.117-171.12l171.118,171.12c2.913,2.911,6.866,4.55,10.993,4.55c4.128,0,8.081-1.639,10.992-4.55l32.709-32.719
|
||||
c6.074-6.075,6.074-15.909,0-21.986L285.08,230.397z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1 KiB |
13
public/static/svg/xd.svg
Normal file
13
public/static/svg/xd.svg
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg fill="#fff" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 460.775 460.775" xml:space="preserve">
|
||||
<path d="M285.08,230.397L456.218,59.27c6.076-6.077,6.076-15.911,0-21.986L423.511,4.565c-2.913-2.911-6.866-4.55-10.992-4.55
|
||||
c-4.127,0-8.08,1.639-10.993,4.55l-171.138,171.14L59.25,4.565c-2.913-2.911-6.866-4.55-10.993-4.55
|
||||
c-4.126,0-8.08,1.639-10.992,4.55L4.558,37.284c-6.077,6.075-6.077,15.909,0,21.986l171.138,171.128L4.575,401.505
|
||||
c-6.074,6.077-6.074,15.911,0,21.986l32.709,32.719c2.911,2.911,6.865,4.55,10.992,4.55c4.127,0,8.08-1.639,10.994-4.55
|
||||
l171.117-171.12l171.118,171.12c2.913,2.911,6.866,4.55,10.993,4.55c4.128,0,8.081-1.639,10.992-4.55l32.709-32.719
|
||||
c6.074-6.075,6.074-15.909,0-21.986L285.08,230.397z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
Loading…
Reference in a new issue