advancements

This commit is contained in:
tavo-wasd 2024-08-21 00:55:17 -06:00
parent 2f423d35a0
commit e9e0429e7c
7 changed files with 460 additions and 291 deletions

View file

@ -1,12 +1,12 @@
BIN = builder BIN = builder
SRC = main.go SRC = main.go
GOFILES = go.sum go.mod GOFILES = go.sum go.mod
GOMODS = github.com/joho/godotenv GOMODS = github.com/joho/godotenv github.com/lib/pq
all: ${BIN} all: ${BIN}
${BIN}: ${SRC} ${GOFILES} ${BIN}: ${SRC} ${GOFILES}
go build -o $@ go build -o builder
${GOFILES}: ${GOFILES}:
go mod init ${BIN} go mod init ${BIN}

370
main.go
View file

@ -1,21 +1,89 @@
package main package main
import ( import (
"bytes"
"database/sql"
"encoding/json" "encoding/json"
"fmt"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"strings"
"os" "os"
"fmt"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"bytes" "time"
// "time"
"github.com/joho/godotenv" "github.com/joho/godotenv"
_ "github.com/lib/pq"
) )
type OrderData struct { var (
baseURL string
clientID string
clientSecret string
planID string
returnUrl string
cancelUrl string
exists bool
err error
dbHost string
dbPort string
dbUser string
dbPass string
dbName string
db *sql.DB
query string
)
func init() {
// Load .env
if err := godotenv.Load(); err != nil {
log.Fatalf("Error loading .env file: %v", err)
}
// Set variables
baseURL = os.Getenv("BASE_URL")
clientID = os.Getenv("CLIENT_ID")
clientSecret = os.Getenv("CLIENT_SECRET")
planID = os.Getenv("PLAN_ID")
returnUrl = os.Getenv("RETURN_URL")
cancelUrl = os.Getenv("CANCEL_URL")
// DB creds
dbHost = os.Getenv("DB_HOST")
dbPort = os.Getenv("DB_PORT")
dbUser = os.Getenv("DB_USER")
dbPass = os.Getenv("DB_PASS")
dbName = os.Getenv("DB_NAME")
// Error if empty
if baseURL == "" || clientID == "" || clientSecret == "" || planID == "" || returnUrl == "" || cancelUrl == "" ||
dbHost == "" || dbPort == "" || dbUser == "" || dbPass == "" || dbName == "" {
log.Fatalf("Error setting credentials")
}
// Connect to DB
var err error
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
dbHost, dbPort, dbUser, dbPass, dbName)
db, err = sql.Open("postgres", connStr)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// Ping DB
if err = db.Ping(); err != nil {
log.Fatalf("Failed to ping database: %v", err)
}
}
type CreateOrderResponse struct {
ID string `json:"id"`
}
type OrderResponse struct {
ID string `json:"id"` ID string `json:"id"`
Status string `json:"status"` Status string `json:"status"`
PurchaseUnits []struct { PurchaseUnits []struct {
@ -23,67 +91,51 @@ type OrderData struct {
Captures []struct { Captures []struct {
ID string `json:"id"` ID string `json:"id"`
Status string `json:"status"` Status string `json:"status"`
CreateTime time.Time `json:"create_time"`
} `json:"captures"` } `json:"captures"`
} `json:"payments"` } `json:"payments"`
} `json:"purchase_units"` } `json:"purchase_units"`
Payer struct {
Name struct {
GivenName string `json:"given_name"`
Surname string `json:"surname"`
} `json:"name"`
EmailAddress string `json:"email_address"`
Phone struct {
PhoneType string `json:"phone_type"`
PhoneNumber struct {
NationalNumber string `json:"national_number"`
} `json:"phone_number"`
} `json:"phone"`
Address struct {
CountryCode string `json:"country_code"`
} `json:"address"`
} `json:"payer"`
} }
type SubscriptionData struct { type SubscriptionResponse struct {
ID string `json:"id"`
Status string `json:"status"` Status string `json:"status"`
// StatusUpdateTime time.Time `json:"status_update_time"` StatusUpdateTime time.Time `json:"status_update_time"`
PlanID string `json:"plan_id"` StartTime time.Time `json:"start_time"`
PlanOverridden bool `json:"plan_overridden"`
// StartTime time.Time `json:"start_time"`
Quantity string `json:"quantity"`
ShippingAmount struct {
CurrencyCode string `json:"currency_code"`
Value string `json:"value"`
} `json:"shipping_amount"`
Subscriber struct { Subscriber struct {
Name struct { Name struct {
GivenName string `json:"given_name"` GivenName string `json:"given_name"`
Surname string `json:"surname"` Surname string `json:"surname"`
} `json:"name"` } `json:"name"`
EmailAddress string `json:"email_address"` EmailAddress string `json:"email_address"`
PayerID string `json:"payer_id"`
ShippingAddress struct {
Name struct {
FullName string `json:"full_name"`
} `json:"name"`
Address struct {
AddressLine1 string `json:"address_line_1"`
AddressLine2 string `json:"address_line_2"`
AdminArea2 string `json:"admin_area_2"`
AdminArea1 string `json:"admin_area_1"`
PostalCode string `json:"postal_code"`
CountryCode string `json:"country_code"`
} `json:"address"`
} `json:"shipping_address"`
} `json:"subscriber"` } `json:"subscriber"`
// CreateTime time.Time `json:"create_time"` CreateTime time.Time `json:"create_time"`
Links []struct {
Href string `json:"href"`
Rel string `json:"rel"`
Method string `json:"method"`
} `json:"links"`
} }
var ( type Cart struct {
baseURL = "https://api-m.sandbox.paypal.com" Directory string `json:"directory"`
)
func init() {
if err := godotenv.Load() ; err != nil {
log.Fatalf("Error loading .env file: %v", err)
}
} }
func main() { func main() {
// Handlers http.HandleFunc("/api/order", CreateOrder)
http.HandleFunc("/api/orders", CreateOrder) http.HandleFunc("/api/order/", CaptureOrder)
http.HandleFunc("/api/orders/", CaptureOrder) http.HandleFunc("/api/paypal/subscribe", CreateSubscription)
http.HandleFunc("/api/paypal/create-subscription", CreateSubscription) http.HandleFunc("/api/paypal/subscribe/", CaptureSubscription)
http.Handle("/", http.FileServer(http.Dir("./public"))) http.Handle("/", http.FileServer(http.Dir("./public")))
// Channel to listen for signals // Channel to listen for signals
@ -103,13 +155,13 @@ func main() {
} }
func Token() (string, error) { func Token() (string, error) {
// Create request // Create
req, err := http.NewRequest("POST", baseURL+"/v1/oauth2/token", strings.NewReader(`grant_type=client_credentials`)) req, err := http.NewRequest("POST", baseURL+"/v1/oauth2/token", strings.NewReader(`grant_type=client_credentials`))
if err != nil { if err != nil {
return "", fmt.Errorf("Error creating request: %v", err) return "", fmt.Errorf("Error creating request: %v", err)
} }
// Make POST req, should return JSON with AccessToken // Send
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(os.Getenv("CLIENT_ID"), os.Getenv("CLIENT_SECRET")) req.SetBasicAuth(os.Getenv("CLIENT_ID"), os.Getenv("CLIENT_SECRET"))
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
@ -118,7 +170,7 @@ func Token() (string, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
// Decode response into result // Decode
var result struct { var result struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
} }
@ -126,9 +178,64 @@ func Token() (string, error) {
return "", fmt.Errorf("Error decoding response: %v", err) return "", fmt.Errorf("Error decoding response: %v", err)
} }
// Return
return result.AccessToken, nil return result.AccessToken, nil
} }
func RegisterOrder(order OrderResponse, directory string) {
var (
capture string
status string
name string
surname string
email string
phone string
country string
date time.Time
)
for _, Unit := range order.PurchaseUnits {
for _, Capture := range Unit.Payments.Captures {
capture = Capture.ID
status = Capture.Status
date = Capture.CreateTime
}
}
name = order.Payer.Name.GivenName
surname = order.Payer.Name.Surname
email = order.Payer.EmailAddress
phone = order.Payer.Phone.PhoneNumber.NationalNumber
country = order.Payer.Address.CountryCode
// Register Payment
_, err = db.Exec(`INSERT INTO payments (id, client, directory, status, step, date) VALUES ($1, $2, $3, $4, $5, $6);`,
capture, email, directory, status, "REGISTERED", date)
if err != nil {
fmt.Printf("$v", err) // TODO consider logging in server
}
// Register Client
err = db.QueryRow(`SELECT EXISTS(SELECT 1 FROM clients WHERE email = $1);`, email).Scan(&exists)
if err != nil {
fmt.Printf("$v", err) // TODO consider logging in server
}
if !exists {
_, err = db.Exec(`INSERT INTO clients (email, name, surname, phone, country) VALUES ($1, $2, $3, $4, $5);`,
email, name, surname, phone, country)
if err != nil {
fmt.Printf("$v", err) // TODO consider logging in server
}
}
// Register Site
_, err = db.Exec(`INSERT INTO sites (directory, client, status, ends) VALUES ($1, $2, $3, $4);`,
directory, email, "ACTIVE", date.AddDate(1, 0, 0)) // Ends a year later
if err != nil {
fmt.Printf("$v", err) // TODO consider logging in server
}
}
func CreateOrder(w http.ResponseWriter, r *http.Request) { func CreateOrder(w http.ResponseWriter, r *http.Request) {
token, err := Token() token, err := Token()
if err != nil { if err != nil {
@ -144,17 +251,26 @@ func CreateOrder(w http.ResponseWriter, r *http.Request) {
"value": "20.00" "value": "20.00"
} }
}], }],
"payment_source": {
"paypal": {
"address" : {
"country_code": "CR"
}
}
},
"application_context": { "application_context": {
"shipping_preference": "NO_SHIPPING" "shipping_preference": "NO_SHIPPING"
} }
}` }`
// Create
req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders", bytes.NewBuffer([]byte(data))) req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders", bytes.NewBuffer([]byte(data)))
if err != nil { if err != nil {
http.Error(w, "Failed to create request", http.StatusInternalServerError) http.Error(w, "Failed to create request", http.StatusInternalServerError)
return return
} }
// Send
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
@ -164,74 +280,110 @@ func CreateOrder(w http.ResponseWriter, r *http.Request) {
} }
defer resp.Body.Close() defer resp.Body.Close()
var result map[string]interface{} // Decode
var result CreateOrderResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
http.Error(w, "Failed to decode response", http.StatusInternalServerError) http.Error(w, "Failed to decode response", http.StatusInternalServerError)
return return
} }
if id, ok := result["id"].(string); ok { w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id": id}) json.NewEncoder(w).Encode(map[string]string{"id": result.ID})
return return
} else {
http.Error(w, "Order ID not found", http.StatusInternalServerError)
return
}
} }
func CaptureOrder(w http.ResponseWriter, r *http.Request) { func CaptureOrder(w http.ResponseWriter, r *http.Request) {
// Read body from order
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}
// Parse to get directory
var cart Cart
err = json.Unmarshal(body, &cart)
if err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
return
}
directory := cart.Directory
// Get orderID
path := strings.TrimPrefix(r.URL.Path, "/api/orders/") path := strings.TrimPrefix(r.URL.Path, "/api/orders/")
parts := strings.Split(path, "/") parts := strings.Split(path, "/")
orderID := parts[0] orderID := parts[0]
if orderID == "" {
http.Error(w, "Failed to get orderID from client URL", http.StatusInternalServerError)
return
}
client := &http.Client{} token, err := Token()
if err != nil {
http.Error(w, "Failed to get access token", http.StatusInternalServerError)
return
}
// Create
req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders/"+orderID+"/capture", nil) req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders/"+orderID+"/capture", nil)
if err != nil { if err != nil {
http.Error(w, "Failed to create request", http.StatusInternalServerError) http.Error(w, "Failed to create request", http.StatusInternalServerError)
return return
} }
token, err := Token() // Check if directory already exists
err = db.QueryRow("SELECT EXISTS (SELECT 1 FROM sites WHERE directory = $1 LIMIT 1);", directory).Scan(&exists)
if err != nil { if err != nil {
http.Error(w, "Failed to get access token", http.StatusInternalServerError) http.Error(w, "Failed to check directory ID against database", http.StatusBadRequest)
return
}
if exists {
http.Error(w, "This directory ID is already taken", http.StatusBadRequest)
return return
} }
// Send, PAYMENT MADE HERE
req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
http.Error(w, "Failed to send request", http.StatusInternalServerError) http.Error(w, "Failed to send request", http.StatusInternalServerError)
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
// Create an instance of AutoGenerated // Decode
var result OrderData var order OrderResponse
if err := json.NewDecoder(resp.Body).Decode(&order); err != nil {
// 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) http.Error(w, "Failed to decode response", http.StatusInternalServerError)
return return
} }
// Now, `result` contains the entire structured response RegisterOrder(order, directory)
// You can send the whole `result` back to the client, or you can selectively send fields.
// Respond
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); err != nil { if err := json.NewEncoder(w).Encode(order); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError) http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
} }
} }
func CreateSubscription(w http.ResponseWriter, r *http.Request) { func CreateSubscription(w http.ResponseWriter, r *http.Request) {
log.Printf("asked to create sub") // Read body from order
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}
planID := os.Getenv("PLAN_ID") // Parse to get directory
returnUrl := "https://suckless.org" var cart Cart
cancelUrl := "https://suckless.org" err = json.Unmarshal(body, &cart)
if err != nil {
log.Printf("This is the planid: %s", planID) http.Error(w, "Failed to parse request body", http.StatusBadRequest)
return
}
directory := cart.Directory
token, err := Token() token, err := Token()
if err != nil { if err != nil {
@ -239,9 +391,7 @@ func CreateSubscription(w http.ResponseWriter, r *http.Request) {
return return
} }
log.Printf("This is the token: %s", token) payload := map[string]interface{}{
body := map[string]interface{}{
"plan_id": planID, "plan_id": planID,
"application_context": map[string]string{ "application_context": map[string]string{
"shipping_preference": "NO_SHIPPING", "shipping_preference": "NO_SHIPPING",
@ -249,12 +399,13 @@ func CreateSubscription(w http.ResponseWriter, r *http.Request) {
"cancel_url": cancelUrl, "cancel_url": cancelUrl,
}, },
} }
jsonData, err := json.Marshal(body) jsonData, err := json.Marshal(payload)
if err != nil { if err != nil {
http.Error(w, "Server error", http.StatusInternalServerError) http.Error(w, "Server error", http.StatusInternalServerError)
return return
} }
// Create request
log.Printf("Creating request") log.Printf("Creating request")
req, err := http.NewRequest("POST", baseURL+"/v1/billing/subscriptions", bytes.NewBuffer(jsonData)) req, err := http.NewRequest("POST", baseURL+"/v1/billing/subscriptions", bytes.NewBuffer(jsonData))
if err != nil { if err != nil {
@ -262,38 +413,69 @@ func CreateSubscription(w http.ResponseWriter, r *http.Request) {
return return
} }
// Check if directory already exists
err = db.QueryRow("SELECT EXISTS (SELECT 1 FROM sites WHERE directory = $1 LIMIT 1);", directory).Scan(&exists)
if err != nil {
http.Error(w, "Failed to check directory ID against database", http.StatusBadRequest)
return
}
if exists {
http.Error(w, "This directory ID is already taken", http.StatusBadRequest)
return
}
// Send
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
req.Header.Set("Prefer", "return=representation") req.Header.Set("Prefer", "return=representation")
resp, err := http.DefaultClient.Do(req)
log.Printf("Sending request")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil { if err != nil {
http.Error(w, "Failed to send request", http.StatusInternalServerError) http.Error(w, "Failed to send request", http.StatusInternalServerError)
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
log.Printf("Request sent")
// Create an instance of AutoGenerated // Decode
// var result SubscriptionData
var result map[string]interface{} var result map[string]interface{}
// Decode the response into the AutoGenerated struct
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
http.Error(w, "Failed to decode response", http.StatusInternalServerError) http.Error(w, "Failed to decode response", http.StatusInternalServerError)
return return
} }
log.Printf("Raw JSON Response: %v", result)
// Now, `result` contains the entire structured response // Respond
// You can send the whole `result` back to the client, or you can selectively send fields.
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); err != nil { if err := json.NewEncoder(w).Encode(result); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError) http.Error(w, "Failed to encode response", http.StatusInternalServerError)
} }
log.Printf("sent response to client") log.Printf("sent response to client")
}
// Capture just like CaptureOrder, but with response from paypal
// https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_get
func CaptureSubscription(w http.ResponseWriter, r *http.Request) {
// Read body from order
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}
// Parse to get directory
var cart Cart
err = json.Unmarshal(body, &cart)
if err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
return
}
// directory := cart.Directory
// Get subID
path := strings.TrimPrefix(r.URL.Path, "/api/subscribe/")
parts := strings.Split(path, "/")
subID := parts[0]
if subID == "" {
http.Error(w, "Failed to get subID from client URL", http.StatusInternalServerError)
return
}
} }

View file

@ -1,44 +1,24 @@
// TODO document.addEventListener("DOMContentLoaded", function() {
// const dialog = document.getElementById("dialog");
// 1. Try to disable asking for shipping info (although could be const overlay = document.getElementById("overlay");
// useful to mark as sent). const menu = document.getElementById("floatingButtons");
//
// 2. Read about IPN and Webhooks to automate registering process.
const clientId = ""; function openDialog() {
const OneTimePID = ""; dialog.style.display = "block";
const PlanID = ""; overlay.style.display = "block";
menu.style.display = "none";
const form = document.getElementById('mainForm');
['name', 'email', 'phone'].forEach(id => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = id;
input.value = document.getElementById(id).value;
form.appendChild(input);
});
function hideDialog() {
document.getElementById('overlay').style.display = 'none';
document.getElementById('dialog').style.display = 'none';
document.getElementById('openDialogButton').style.display = 'block';
} }
function showDialog() { function closeDialog() {
document.getElementById('overlay').style.display = 'block'; dialog.style.display = "none";
document.getElementById('dialog').style.display = 'block'; overlay.style.display = "none";
document.getElementById('openDialogButton').style.display = 'none'; menu.style.display = "block";
} }
function togglePaymentMethod(selectedButtonId) { function togglePaymentMethod(selectedButtonId) {
// Deselect all buttons and hide all PayPal buttons // Deselect all buttons and hide all PayPal buttons
document.querySelectorAll('#method-button-container button').forEach(button => { document.querySelectorAll('#method-button-container button').forEach(button => { button.classList.remove('active'); });
button.classList.remove('active'); document.querySelectorAll('#paypal-button-container > div').forEach(div => { div.classList.remove('active'); });
});
document.querySelectorAll('#paypal-button-container > div').forEach(div => {
div.classList.remove('active');
});
// Select the clicked button and show the corresponding PayPal button // Select the clicked button and show the corresponding PayPal button
const selectedButton = document.getElementById(selectedButtonId); const selectedButton = document.getElementById(selectedButtonId);
@ -53,29 +33,30 @@ function togglePaymentMethod(selectedButtonId) {
} }
} }
function isFormValid(form) { document.getElementById('showOneTimeButton').addEventListener('click', function() {
return form.checkValidity(); document.getElementById('warning-message').style.display = 'none';
} togglePaymentMethod('showOneTimeButton');
});
document.getElementById('showSubButton').addEventListener('click', function() {
document.getElementById('warning-message').style.display = 'none';
togglePaymentMethod('showSubButton');
});
document.getElementById("openDialogButton").addEventListener("click", openDialog);
document.getElementById("cancelDialogButton").addEventListener("click", closeDialog);
});
window.paypal_order.Buttons({ window.paypal_order.Buttons({
style: { shape: 'pill', color: 'black', layout: 'vertical', label: 'pay' }, style: { shape: 'pill', color: 'black', layout: 'vertical', label: 'pay' },
async createOrder() { async createOrder() {
try { try {
const response = await fetch("/api/orders", { const response = await fetch("/api/order", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "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",
},
],
}),
}); });
const orderData = await response.json(); const orderData = await response.json();
@ -97,11 +78,16 @@ window.paypal_order.Buttons({
}, },
async onApprove(data, actions) { async onApprove(data, actions) {
try { try {
const response = await fetch(`/api/orders/${data.orderID}/capture`, { const response = await fetch(`/api/order/${data.orderID}/capture`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(
{
directory: "tutorias",
}
),
}); });
const orderData = await response.json(); const orderData = await response.json();
@ -149,16 +135,24 @@ window.paypal_subscribe.Buttons({
style: { shape: 'pill', color: 'black', layout: 'vertical', label: 'subscribe' }, style: { shape: 'pill', color: 'black', layout: 'vertical', label: 'subscribe' },
async createSubscription() { async createSubscription() {
try { try {
const response = await fetch("/api/paypal/create-subscription", { const response = await fetch("/api/paypal/subscribe", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ userAction: "SUBSCRIBE_NOW" }), body: JSON.stringify(
{
// userAction: "SUBSCRIBE_NOW"
directory: "testsite",
}
),
}); });
const data = await response.json(); const data = await response.json();
if (data?.id) { if (data?.id) {
resultMessage(`Successful subscription with ID ${data.id}...<br><br>`); const approvalUrl = data.links.find(link => link.rel === "approve").href;
window.location.href = approvalUrl;
resultMessage(`Successful subscription with ID ${approvalUrl}...<br><br>`);
// resultMessage(`Successful subscription with ID ${data.id}...<br><br>`);
return data.id; return data.id;
} else { } else {
console.error( console.error(
@ -175,8 +169,6 @@ window.paypal_subscribe.Buttons({
{ hideButtons: true }, { hideButtons: true },
); );
} }
const approvalUrl = data.links.find(link => link.rel === "approve").href;
window.location.href = approvalUrl;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
resultMessage( resultMessage(
@ -201,35 +193,3 @@ window.paypal_subscribe.Buttons({
} }
}, },
}).render("#paypal-button-container-subscribe"); // Renders the PayPal button }).render("#paypal-button-container-subscribe"); // Renders the PayPal button
// 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)) {
document.getElementById('warning-message').style.display = 'none';
togglePaymentMethod('showOneTimeButton');
} else {
document.getElementById('warning-message').style.display = 'block';
}
});
document.getElementById('showSubButton').addEventListener('click', function() {
if (isFormValid(form)) {
document.getElementById('warning-message').style.display = 'none';
togglePaymentMethod('showSubButton');
} else {
document.getElementById('warning-message').style.display = 'block';
}
});
document.getElementById('openDialogButton').addEventListener('click', () => {
showDialog();
});
document.getElementById('cancelDialogButton').addEventListener('click', () => {
hideDialog();
});

View file

@ -10,9 +10,6 @@
<meta name="robots" content="index, follow"> <meta name="robots" content="index, follow">
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" href="/static/css/style.css"> <link rel="stylesheet" href="/static/css/style.css">
<!-- <link rel="stylesheet" href="/static/css/simplemde.min.css"> -->
<!-- <link rel="stylesheet" href="/static/css/simplemde-dark.min.css"> -->
<!-- <script src="/static/js/simplemde.min.js"></script> -->
<!-- EDITORJS --> <!-- EDITORJS -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script><!-- Header --> <script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script><!-- Header -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest"></script><!-- Image --> <script src="https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest"></script><!-- Image -->
@ -26,27 +23,35 @@
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script> <script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
<!-- EDITORJS --> <!-- EDITORJS -->
<script <script
src="https://www.paypal.com/sdk/js?client-id=AcCW43LI1S6lLQgtLkF4V8UOPfmXcqXQ8xfEl41hRuMxSskR2jkWNwQN6Ab1WK7E2E52GNaoYBHqgIKd&components=buttons&enable-funding=card&disable-funding=paylater,venmo" src="https://sandbox.paypal.com/sdk/js?client-id=AcCW43LI1S6lLQgtLkF4V8UOPfmXcqXQ8xfEl41hRuMxSskR2jkWNwQN6Ab1WK7E2E52GNaoYBHqgIKd&components=buttons&enable-funding=card&disable-funding=paylater,venmo"
data-namespace="paypal_order" data-namespace="paypal_order"
></script> ></script>
<script <script
src="https://www.paypal.com/sdk/js?client-id=AcCW43LI1S6lLQgtLkF4V8UOPfmXcqXQ8xfEl41hRuMxSskR2jkWNwQN6Ab1WK7E2E52GNaoYBHqgIKd&components=buttons&vault=true&intent=subscription&enable-funding=card&disable-funding=paylater" src="https://sandbox.paypal.com/sdk/js?client-id=AcCW43LI1S6lLQgtLkF4V8UOPfmXcqXQ8xfEl41hRuMxSskR2jkWNwQN6Ab1WK7E2E52GNaoYBHqgIKd&components=buttons&vault=true&intent=subscription&enable-funding=card&disable-funding=paylater"
data-namespace="paypal_subscribe" data-namespace="paypal_subscribe"
></script> ></script>
</head> </head>
<form action="submit.php" id="mainForm" method="post">
<div class="banner" style="background-image: url(/static/banner.jpg);"> <div class="banner" style="background-image: url(/static/banner.jpg);">
<div class="desc"> <div class="desc">
<input type="text" name="title" class="input-title" placeholder="[Nombre Ejemplo]"> <input type="text" id="title" name="title" class="input-title" placeholder="[Nombre Ejemplo]">
<input type="text" name="slogan" class="input-slogan" placeholder="[Slogan llamativo o breve descripción]"> <input type="text" id="slogan" name="slogan" class="input-slogan" placeholder="[Slogan llamativo o breve descripción]">
<button type="button" id="openDialogButton"> <div class="floating-buttons" id="floatingButtons">
<button class="floating-button" id="openDialogButton">
<img src="/static/svg/cart.svg"> <img src="/static/svg/cart.svg">
<span>$20/año</span> <span>$20/año</span>
</button> </button>
<button class="floating-button" id="updateSiteButton">
<img src="/static/svg/edit.svg">
<span>Actualizar mi página</span>
</button>
<button class="floating-button" id="deleteSiteButton">
<img src="/static/svg/delete.svg">
<span>Eliminar página</span>
</button>
</div>
</div> </div>
</div> </div>
<body> <body>
<!-- <button type="button" id="saveButton">Salvar</button> -->
<div id="content" class="content"> <div id="content" class="content">
<div id="editorjs"></div> <div id="editorjs"></div>
</div> </div>
@ -54,7 +59,6 @@
<div id="overlay"></div> <div id="overlay"></div>
<div id="dialog"> <div id="dialog">
<div> <div>
<h2>Información de Contacto</h2>
<button id="cancelDialogButton" type="button"> <button id="cancelDialogButton" type="button">
<picture> <picture>
<source srcset="/static/svg/xd.svg" media="(prefers-color-scheme: dark)"> <source srcset="/static/svg/xd.svg" media="(prefers-color-scheme: dark)">
@ -62,12 +66,6 @@
</picture> </picture>
</button> </button>
</div> </div>
<p>Utilizaremos esta información para contactarle acerca de la publicación del sitio.</p>
<div id="form-container">
<input type="text" id="name" name="name" placeholder="Nombre (Requerido)" required>
<input type="email" id="email" name="email" placeholder="Correo electrónico (Requerido)" required>
<input type="tel" id="phone" name="phone" placeholder="Teléfono (Opcional)">
</div>
<h2>Tipo de Pago</h2> <h2>Tipo de Pago</h2>
<div> <div>
<p><strong>Pago Único:</strong> El pago debe realizarse manualmente un año después de contratar el servicio, le enviaremos notificaciones al correo electrónico recordando el pago.</p> <p><strong>Pago Único:</strong> El pago debe realizarse manualmente un año después de contratar el servicio, le enviaremos notificaciones al correo electrónico recordando el pago.</p>
@ -86,8 +84,6 @@
</div> </div>
</div> </div>
</div> </div>
</form> <script src="/app.js"></script>
<!-- <script src="/static/js/simplemde.config.js"></script> -->
<script src="/form.js"></script>
<script src="/static/js/config.editor.js"></script> <script src="/static/js/config.editor.js"></script>
</html> </html>

View file

@ -35,6 +35,9 @@
--page-width: 90%; --page-width: 90%;
--navbar-width: 50vh; --navbar-width: 50vh;
} }
.floating-button span {
display: none;
}
} }
html { html {
@ -210,30 +213,6 @@ button {
color: var(--color); color: var(--color);
} }
#openDialogButton {
display: flex;
align-items: center;
position: fixed;
bottom: 2em;
right: 1em;
height: 2.5em;
cursor: pointer;
font-size: 1em;
z-index: 1001;
background: linear-gradient(135deg, #00336b, #0099c5);
border-radius: 999px;
box-shadow: #006994 0 10px 20px -10px;
box-sizing: border-box;
color: rgba(255, 255, 255, 0.85);
font-size: 1em;
font-weight: bold;
outline: 0 solid transparent;
padding: 8px 18px;
width: fit-content;
word-break: break-word;
border: 0;
}
#method-button-container { #method-button-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -278,29 +257,59 @@ button {
font-size: 1.1em; font-size: 1.1em;
} }
#checkout { .floating-buttons {
display: flex;
position: fixed;
flex-direction: row;
align-items: center;
bottom: 0.2em;
right: 0.5em;
gap: 0.4em;
z-index: 9999;
} }
#openDialogButton img { .floating-button {
height: 2.5em;
cursor: pointer;
font-size: 1em;
z-index: 1001;
border-radius: 999px;
box-sizing: border-box;
color: rgba(255, 255, 255, 0.85);
font-size: 1em;
font-weight: bold;
outline: 0 solid transparent;
padding: 8px 18px;
width: fit-content;
word-break: break-word;
border: 0;
}
.floating-button img {
width: 1.2em; width: 1.2em;
height: 1.2em; height: 1.2em;
vertical-align: middle; vertical-align: middle;
} }
#openDialogButton span { .floating-button span {
margin-left: 0.6em; margin-left: 0.6em;
} }
/* Custom SimpleMDE styling */ .floating-button:hover {
.CodeMirror { background-color: #0056b3;
border-radius: 10px;
overflow: hidden;
font-size: 1.2em;
font-family: monospace;
text-align: left;
} }
.editor-preview { #updateSiteButton {
font-size: 1em; background: linear-gradient(135deg, #214353, #4c9abf);
font-family: sans-serif; box-shadow: #0099c5 0 10px 20px -15px;
}
#deleteSiteButton {
background: linear-gradient(135deg, #5e2329, #bf4c58);
box-shadow: #e31300 0 10px 20px -15px;
}
#openDialogButton {
background: linear-gradient(135deg, #21532a, #4fc764);
box-shadow: #27d100 0 10px 20px -15px;
} }

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" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 485 485" xml:space="preserve">
<g>
<g>
<rect x="67.224" width="350.535" height="71.81"/>
<path d="M417.776,92.829H67.237V485h350.537V92.829H417.776z M165.402,431.447h-28.362V146.383h28.362V431.447z M256.689,431.447
h-28.363V146.383h28.363V431.447z M347.97,431.447h-28.361V146.383h28.361V431.447z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 727 B

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#fff" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path d="M70.2,337.4l104.4,104.4L441.5,175L337,70.5L70.2,337.4z M0.6,499.8c-2.3,9.3,2.3,13.9,11.6,11.6L151.4,465L47,360.6
L0.6,499.8z M487.9,24.1c-46.3-46.4-92.8-11.6-92.8-11.6c-7.6,5.8-34.8,34.8-34.8,34.8l104.4,104.4c0,0,28.9-27.2,34.8-34.8
C499.5,116.9,534.3,70.6,487.9,24.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 665 B