From d87b34240197a51e3db68333e718960f973e4b39 Mon Sep 17 00:00:00 2001 From: tavo Date: Mon, 23 Sep 2024 09:24:41 -0600 Subject: [PATCH] extend period checkout --- public/client.js | 25 ++++++++++ public/index.html | 14 +++++- public/lang/es.json | 2 + public/paypal.js | 94 +++++++++++++++++++++++++++++++++++++ public/static/css/style.css | 21 +++++---- server/db.go | 18 +++++++ server/main.go | 37 +++++++++++++++ 7 files changed, 199 insertions(+), 12 deletions(-) diff --git a/public/client.js b/public/client.js index 26cd7f2..22409e4 100644 --- a/public/client.js +++ b/public/client.js @@ -523,6 +523,16 @@ async function editMode(dir) { setTimeout(() => { dashboard.style.display = 'none'; }, 500); + + const dueDate = await getDueDate(dir); + const previewElement = document.getElementById('checkdir-duedate'); + previewElement.style.display = "block" + + if (dueDate) { + previewElement.innerHTML = `Fecha de término actual: ${dueDate}`; + } else { + previewElement.innerHTML = "No se pudo cargar la fecha de término."; + } } async function fetchAndStoreData(directoryName) { @@ -617,3 +627,18 @@ function extractSitePath(url) { const match = cleanUrl.match(/^conex\.one\/([^\/?#]+)\/?/); return match ? match[1] : null; } + +async function getDueDate(directory) { + try { + const response = await fetch(`https://api.conex.one/api/duedate/${directory}`); + if (!response.ok) { + throw new Error(`Error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + return data.due; + } catch (error) { + console.error("Failed to fetch due date:", error); + return null; + } +} diff --git a/public/index.html b/public/index.html index 30524f8..6db5a53 100644 --- a/public/index.html +++ b/public/index.html @@ -117,8 +117,18 @@

- - +
+ + +
+
+

+

+
+
+
+
+

diff --git a/public/lang/es.json b/public/lang/es.json index 76baba8..1f1aabd 100644 --- a/public/lang/es.json +++ b/public/lang/es.json @@ -15,6 +15,8 @@ "tempCodePlaceholder": "Código de 6 dígitos enviado a su correo", "requestChangesButton": "Enviar correo con el pin", "confirmChangesButton": "Verificar pin y actualizar sitio", + "extendServiceDialogHeader": "Extender el servicio un año por $20", + "extendServiceDialogParagraph": "Puede pagar por adelantado el precio actual, para extender la fecha de término de su servicio por un año.", "continueToBuyModeButton": "Empezar a diseñar", "editDialogHeader": "Editar un sitio existente", "editDialogParagraph": "El sitio ya debe estar registrado en conex.one para poder cargarlo localmente.", diff --git a/public/paypal.js b/public/paypal.js index b7a0b04..7d36e58 100644 --- a/public/paypal.js +++ b/public/paypal.js @@ -122,3 +122,97 @@ function checkoutError(message) { container.style.display = "block"; container.innerHTML = message; } + +paypal.Buttons({ + style: { + shape: "pill", + layout: "vertical", + color: "black", + label: "pay" + }, + async createOrder() { + const savedData = JSON.parse(localStorage.getItem('conex_data')) || {}; + const requestData = { + directory: savedData.directory, + }; + + const orderData = await response.json(); + + if (orderData.id) { + return orderData.id; + } else { + const errorDetail = orderData?.details?.[0]; + checkoutExtendedError(`

No se puede realizar la compra en este momento

`); + } + }, + async onApprove(data, actions) { + const savedData = JSON.parse(localStorage.getItem('conex_data')) || {}; + try { + const requestData = { + directory: savedData.directory + }; + + const response = await fetch(`https://api.conex.one/api/orders/${data.orderID}/capture`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestData), + }); + + 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]; + checkoutExtendedSuccess(` +

+ Estado: ${transaction.status}
+ ID de transacción: ${transaction.id}
+ Se ha tramitado correctamente la extensión de la fecha de cobro. +

+ `,); + document.querySelector("#paypal-button-container").style.display = "none"; + console.log( + "Capture result", + orderData, + JSON.stringify(orderData, null, 2), + ); + } + } catch (error) { + console.error(error); + checkoutExtendedError(`

No se puede realizar la compra en este momento

`); + } + }, +}).render("#paypal-extend-button-container"); + +function checkoutExtendedSuccess(message) { + const container = document.querySelector("#checkout-extended-success-message"); + container.style.display = "block"; + container.innerHTML = message; +} + +function checkoutExtendedError(message) { + const container = document.querySelector("#checkout-extended-error-message"); + container.style.display = "block"; + container.innerHTML = message; +} + diff --git a/public/static/css/style.css b/public/static/css/style.css index d2c09e6..dcd4813 100644 --- a/public/static/css/style.css +++ b/public/static/css/style.css @@ -360,15 +360,13 @@ a { } #dialog button { - margin: 0.5em 0.5em 0 0; + margin: 0em 0.5em 0 0; padding: 0.5em; color: var(--unemph-color); background: var(--background-color); border: 1px solid var(--hover-border); border-radius: 10px; - position: absolute; right: 1.3em; - bottom: 1.5em; } #dialog button:hover { @@ -382,18 +380,14 @@ a { position: absolute; width: 2em; height: 2em; - top: 0em; + top: 0.5em; right: 0.2em; font-size: 0.9em; } #requestChangesButton, #confirmChangesButton { - width: 45%; -} - -#requestChangesButton { - left: 1.78em; + width: 100%; } .input-dialog { @@ -403,7 +397,7 @@ a { background: var(--hover-background); padding: 0.8em; border-radius: var(--border-radius-regular); - margin: 1em auto 1em auto; + margin: 1em auto 0em auto; width: 95%; display: block; } @@ -535,3 +529,10 @@ a { .footer-links a:hover { text-decoration: underline; } + +.buttonsContainer { + display: flex; /* Use Flexbox for layout */ + justify-content: center; /* Center the buttons horizontally */ + align-items: center; /* Center the buttons vertically (if needed) */ + gap: 10px; /* Optional: add space between buttons */ +} diff --git a/server/db.go b/server/db.go index e5786e7..43ac004 100644 --- a/server/db.go +++ b/server/db.go @@ -230,3 +230,21 @@ func FetchSite(db *sql.DB, folder string) (ConexData, error) { return siteData, nil } + +func DueDate(db *sql.DB, folder string) (time.Time, error) { + if len(folder) <= 3 { + return time.Time{}, fmt.Errorf("folder name must be longer than 3 characters") + } + + var due time.Time + if err := db.QueryRow(` + SELECT due FROM sites WHERE folder = $1 + `, folder).Scan(&due); err != nil { + if err == sql.ErrNoRows { + return time.Time{}, fmt.Errorf("no due date found for folder: %s", folder) + } + return time.Time{}, fmt.Errorf("error checking due date: %v", err) + } + + return due, nil +} diff --git a/server/main.go b/server/main.go index 9278f8e..e7654a8 100644 --- a/server/main.go +++ b/server/main.go @@ -132,6 +132,7 @@ func main() { http.HandleFunc("/api/directory/", VerifyDirectoryHandler(db)) http.HandleFunc("/api/fetch/", FetchSiteHandler(db)) http.HandleFunc("/api/upload", UploadFileHandler(s3Client, endpoint, apiEndpoint, apiToken, bucketName, publicEndpoint)) + http.HandleFunc("/api/duedate/", VerifyDueDateHandler(db)) // http.Handle("/", http.FileServer(http.Dir("./public"))) stop := make(chan os.Signal, 1) @@ -462,6 +463,42 @@ func FetchSiteHandler(db *sql.DB) http.HandlerFunc { } } +func VerifyDueDateHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + enableCORS(w) + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + + errClientNotice := "Error verifying duedate against db" + + path := strings.TrimPrefix(r.URL.Path, "/api/duedate/") + parts := strings.Split(path, "/") + folder := parts[0] + if folder == "" { + httpErrorAndLog(w, nil, "Error getting directory", errClientNotice) + return + } + + var response struct { + Due time.Time `json:"due"` + } + + due, err := DueDate(db, folder) + if err != nil { + httpErrorAndLog(w, err, "Error getting due date", errClientNotice) + return + } else { + response.Due = due + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } +} + func enableCORS(w http.ResponseWriter) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")