better ui, paypal order ready, subscription still missing

This commit is contained in:
tavo-wasd 2024-08-18 01:16:13 -06:00
parent f2e07946d0
commit 1ff85e982a
8 changed files with 229 additions and 53 deletions

59
main.go
View file

@ -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)
}
}

View file

@ -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)) {

View file

@ -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&currency=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>

View file

@ -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;
}

View file

@ -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'],
]
}
},

View 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
View 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
View 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