Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa6d64c18 | ||
|
|
936a8ec876 | ||
|
|
f6ca5d43fa | ||
|
|
684c3c80f8 | ||
|
|
a66e571c50 | ||
|
|
a0bb77cd4a | ||
|
|
31ab7372d7 | ||
|
|
3f154058bd | ||
|
|
279280acde | ||
|
|
1c1da1403b | ||
|
|
058d646d01 | ||
|
|
b6a95ad0f8 | ||
|
|
ba2a94a1da | ||
|
|
56f3d84cca | ||
|
|
33e7d825ad | ||
|
|
909b9ea803 | ||
|
|
bacd7b3e78 | ||
|
|
9718b8e6b7 | ||
|
|
393bcb3799 | ||
|
|
3e8dd361ce | ||
|
|
8f22811900 | ||
|
|
0d4ca742d3 | ||
|
|
0ee89c2568 | ||
|
|
82f4037f38 | ||
|
|
22f699826d | ||
|
|
56d8c94eec | ||
|
|
fddd5d25b6 | ||
|
|
bbdb18e8ca | ||
|
|
810348879b | ||
|
|
d908a0d646 | ||
|
|
8e897b7e51 | ||
|
|
9b526c7a45 | ||
|
|
f20d69924e |
@ -1,6 +0,0 @@
|
||||
DATABASE_URL=file:/app/data/database.sqlite
|
||||
SHOPIFY_APP_URL=http://localhost:3000
|
||||
SHOPIFY_API_KEY=your_client_id
|
||||
SHOPIFY_API_SECRET=your_client_secret
|
||||
DOMAIN=YOUR_HOSTED_APP_URL.COM
|
||||
LETSENCRYPT_EMAIL=johndoe@example.com
|
||||
@ -1,6 +0,0 @@
|
||||
node_modules
|
||||
build
|
||||
public/build
|
||||
shopify-app-remix
|
||||
*/*.yml
|
||||
.shopify
|
||||
@ -1,13 +0,0 @@
|
||||
/** @type {import('@types/eslint').Linter.BaseConfig} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"@remix-run/eslint-config",
|
||||
"@remix-run/eslint-config/node",
|
||||
"@remix-run/eslint-config/jest-testing-library",
|
||||
"prettier",
|
||||
],
|
||||
globals: {
|
||||
shopify: "readonly"
|
||||
},
|
||||
};
|
||||
17
.gitattributes
vendored
Normal file
17
.gitattributes
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text=auto
|
||||
|
||||
# Explicitly declare text files you want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.c text
|
||||
*.h text
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sln text eol=crlf
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sh text eol=lf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
26
.github/workflows/CI.yml
vendored
Normal file
26
.github/workflows/CI.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Publishing docker image
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
run: ./.github/workflows/publish-docker.sh
|
||||
11
.github/workflows/publish-docker.sh
vendored
Executable file
11
.github/workflows/publish-docker.sh
vendored
Executable file
@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
|
||||
sudo docker login --username=$DOCKERHUB_USERNAME --password=$DOCKERHUB_TOKEN
|
||||
sudo docker buildx create --use
|
||||
sudo docker buildx build \
|
||||
-t "btcpayserver/shopify-app-deployer:${TAG}" \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--push .
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
import fs from "fs";
|
||||
import { LATEST_API_VERSION } from "@shopify/shopify-api";
|
||||
import { shopifyApiProject, ApiType } from "@shopify/api-codegen-preset";
|
||||
function getConfig() {
|
||||
const config = {
|
||||
projects: {
|
||||
default: shopifyApiProject({
|
||||
apiType: ApiType.Admin,
|
||||
apiVersion: LATEST_API_VERSION,
|
||||
documents: [
|
||||
"./app/**/*.{js,ts,jsx,tsx}",
|
||||
"./app/.server/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
outputDir: "./app/types",
|
||||
}),
|
||||
},
|
||||
};
|
||||
let extensions = [];
|
||||
try {
|
||||
extensions = fs.readdirSync("./extensions");
|
||||
} catch {
|
||||
// ignore if no extensions
|
||||
}
|
||||
for (const entry of extensions) {
|
||||
const extensionPath = `./extensions/${entry}`;
|
||||
const schema = `${extensionPath}/schema.graphql`;
|
||||
if (!fs.existsSync(schema)) {
|
||||
continue;
|
||||
}
|
||||
config.projects[entry] = {
|
||||
schema,
|
||||
documents: [`${extensionPath}/**/*.graphql`],
|
||||
};
|
||||
}
|
||||
return config;
|
||||
}
|
||||
module.exports = getConfig();
|
||||
4
.npmrc
4
.npmrc
@ -1,4 +0,0 @@
|
||||
engine-strict=true
|
||||
auto-install-peers=true
|
||||
shamefully-hoist=true
|
||||
enable-pre-post-scripts=true
|
||||
@ -1,7 +0,0 @@
|
||||
package.json
|
||||
.shadowenv.d
|
||||
.vscode
|
||||
node_modules
|
||||
prisma
|
||||
public
|
||||
.shopify
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@ -1,25 +0,0 @@
|
||||
# @shopify/shopify-app-template-remix
|
||||
|
||||
## 2024.10.02
|
||||
|
||||
- [863](https://github.com/Shopify/shopify-app-template-remix/pull/863) Update to Shopify App API v2024-10 and shopify-app-remix v3.3.2
|
||||
|
||||
## 2024.09.18
|
||||
|
||||
- [850](https://github.com/Shopify/shopify-app-template-remix/pull/850) Removed "~" import alias
|
||||
|
||||
## 2024.09.17
|
||||
|
||||
- [842](https://github.com/Shopify/shopify-app-template-remix/pull/842)Move webhook processing to individual routes
|
||||
|
||||
## 2024.08.19
|
||||
|
||||
Replaced deprecated `productVariantUpdate` with `productVariantsBulkUpdate`
|
||||
|
||||
## v2024.08.06
|
||||
|
||||
Allow `SHOP_REDACT` webhook to process without admin context
|
||||
|
||||
## v2024.07.16
|
||||
|
||||
Started tracking changes and releases using calver
|
||||
17
Dockerfile
17
Dockerfile
@ -1,9 +1,9 @@
|
||||
FROM node:18-alpine3.20
|
||||
FROM node:20-alpine3.20
|
||||
|
||||
# Install xdg-utils (BTCPay Server mod)
|
||||
RUN apk add --no-cache xdg-utils
|
||||
RUN apk add --no-cache bash xdg-utils git
|
||||
# Install Shopify CLI globally (BTCPay Server mod)
|
||||
RUN npm install -g @shopify/cli@latest
|
||||
RUN npm install -g @shopify/cli@3.92.1
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
@ -13,13 +13,8 @@ ENV NODE_ENV=production
|
||||
|
||||
COPY package.json package-lock.json* ./
|
||||
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
# Remove CLI packages since we don't need them in production by default.
|
||||
# Remove this line if you want to run CLI commands in your container.
|
||||
###RUN npm remove @shopify/cli
|
||||
RUN npm install && npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
CMD ["npm", "run", "docker-start"]
|
||||
RUN npm install && npm cache clean --force
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
|
||||
23
README.md
23
README.md
@ -2,6 +2,27 @@
|
||||
|
||||
This is an app that provides a checkout extension to make it possible for Shopify merchants to accept Bitcoin payments using BTCPay Server.
|
||||
|
||||
The docker image run a lightweight API in `deploy.sh` running on port `5000`.
|
||||
|
||||
This host a single route on `/deploy`, which will build and deploy on shopify server the app.
|
||||
|
||||
```json
|
||||
{
|
||||
"cliToken": "PARTNER CLI TOKEN",
|
||||
"clientId": "APP_CLIENT_ID",
|
||||
"pluginUrl": "BTCPAY_PLUGIN_URL",
|
||||
"appName": "APP NAME"
|
||||
}
|
||||
```
|
||||
|
||||
It streams back the logs of the deployment process to the client.
|
||||
|
||||
This server is used by the [BTCPay Server Shopify plugin](https://github.com/btcpayserver/btcpayserver-shopify-plugin) in order to deploy the app.
|
||||
|
||||
You can find installation instructions on our official documentation page: [BTCPay Server for Shopify](https://docs.btcpayserver.org/ShopifyV2/)
|
||||
|
||||
This app is based on the official [Shopify Remix app template](https://github.com/Shopify/shopify-app-template-remix). Other than the original template we have a docker-compose.yml file in place that makes sure nginx and Let's Encrypt companion is deployed and can be used on any cheap VPS.
|
||||
## Maintainers
|
||||
|
||||
The image is hosted on [docker hub](https://hub.docker.com/r/btcpayserver/shopify-app-deployer), to publish a new one, push a new tag to the repository. The github action is set up to create the image and upload it.
|
||||
|
||||
Increment `VERSION` when you want the plugin to be able to notify the user that a new app deployment is needed.
|
||||
@ -1,11 +0,0 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (!global.prisma) {
|
||||
global.prisma = new PrismaClient();
|
||||
}
|
||||
}
|
||||
|
||||
const prisma = global.prisma || new PrismaClient();
|
||||
|
||||
export default prisma;
|
||||
@ -1,53 +0,0 @@
|
||||
import { PassThrough } from "stream";
|
||||
import { renderToPipeableStream } from "react-dom/server";
|
||||
import { RemixServer } from "@remix-run/react";
|
||||
import { createReadableStreamFromReadable } from "@remix-run/node";
|
||||
import { isbot } from "isbot";
|
||||
import { addDocumentResponseHeaders } from "./shopify.server";
|
||||
|
||||
const ABORT_DELAY = 5000;
|
||||
|
||||
export default async function handleRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext,
|
||||
) {
|
||||
addDocumentResponseHeaders(request, responseHeaders);
|
||||
const userAgent = request.headers.get("user-agent");
|
||||
const callbackName = isbot(userAgent ?? "") ? "onAllReady" : "onShellReady";
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
abortDelay={ABORT_DELAY}
|
||||
/>,
|
||||
{
|
||||
[callbackName]: () => {
|
||||
const body = new PassThrough();
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
resolve(
|
||||
new Response(stream, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
}),
|
||||
);
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(error) {
|
||||
reject(error);
|
||||
},
|
||||
onError(error) {
|
||||
responseStatusCode = 500;
|
||||
console.error(error);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
});
|
||||
}
|
||||
30
app/root.jsx
30
app/root.jsx
@ -1,30 +0,0 @@
|
||||
import {
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from "@remix-run/react";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="preconnect" href="https://cdn.shopify.com/" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.shopify.com/static/fonts/inter/v4/styles.css"
|
||||
/>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
import { json, redirect } from "@remix-run/node";
|
||||
import { Form, useLoaderData } from "@remix-run/react";
|
||||
import { login } from "../../shopify.server";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.searchParams.get("shop")) {
|
||||
throw redirect(`/app?${url.searchParams.toString()}`);
|
||||
}
|
||||
|
||||
return json({ showForm: Boolean(login) });
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const { showForm } = useLoaderData();
|
||||
|
||||
return (
|
||||
<div className={styles.index}>
|
||||
<div className={styles.content}>
|
||||
<h1 className={styles.heading}>Accept Bitcoin in Shopify with BTCPay Server</h1>
|
||||
<p className={styles.text}>
|
||||
Add Bitcoin as a payment option to your store with{" "}
|
||||
<a
|
||||
href="https://btcpayserver.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "#3498db", textDecoration: "underline" }}
|
||||
>
|
||||
BTCPay Server
|
||||
</a>
|
||||
. It's free, simple, secure, and puts you in full control.
|
||||
</p>
|
||||
|
||||
<ul className={styles.list}>
|
||||
<li>
|
||||
<strong>Zero fees: </strong> Enjoy a payment gateway with no fees. Yes, You saw that right. Zero fees!
|
||||
</li>
|
||||
<li>
|
||||
<strong>No middlemen: </strong> Say goodbye to intermediaries and tedious paperwork, and get your money directly in your wallet
|
||||
</li>
|
||||
<li>
|
||||
<strong>Community-driven support: </strong> Get responsive assistance from our dedicated community
|
||||
</li>
|
||||
</ul>
|
||||
<div>
|
||||
<a className={styles.button}
|
||||
target="_blank"
|
||||
href="https://docs.btcpayserver.org/Shopify/">
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,131 +0,0 @@
|
||||
/* Main container for the landing page */
|
||||
.index {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background-color: #f9fafb; /* Soft neutral background */
|
||||
}
|
||||
|
||||
/* Headings */
|
||||
.heading {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: #2c3e50; /* Dark, modern text color */
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 1.2rem;
|
||||
color: #7f8c8d; /* Subtle gray for supporting text */
|
||||
line-height: 1.6;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Main content container */
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 800px;
|
||||
padding: 2rem;
|
||||
background-color: #ffffff; /* Clean white card background */
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Unordered list for feature highlights */
|
||||
.list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 2rem 0;
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.list > li {
|
||||
font-size: 1rem;
|
||||
color: #34495e; /* Slightly darker gray for readability */
|
||||
line-height: 1.4;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.list > li::before {
|
||||
content: "✔";
|
||||
color: #27ae60; /* Green checkmark for positivity */
|
||||
margin-right: 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Links section for documentation */
|
||||
.docs {
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.docs a {
|
||||
text-decoration: none;
|
||||
color: #3498db; /* Shopify-inspired blue for links */
|
||||
font-weight: 500;
|
||||
margin: 0.5rem 0;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.docs a:hover {
|
||||
color: #2c3e50; /* Darker on hover */
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Call-to-action button */
|
||||
.button {
|
||||
display: inline-block;
|
||||
background-color: #5cb85c; /* Shopify green */
|
||||
color: #ffffff;
|
||||
padding: 0.8rem 2rem;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #4cae4c; /* Slightly darker green on hover */
|
||||
}
|
||||
|
||||
/* Responsive design for smaller screens */
|
||||
@media only screen and (max-width: 768px) {
|
||||
.content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.list {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 0.6rem 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
import { json } from '@remix-run/node';
|
||||
import { cors } from 'remix-utils/cors'
|
||||
import db from '../db.server';
|
||||
|
||||
export async function loader({ request }) {
|
||||
const url = new URL(request.url);
|
||||
const shopName = url.searchParams.get("shopName");
|
||||
|
||||
if (request.method === "OPTIONS") {
|
||||
return json({ status: 204 });
|
||||
}
|
||||
|
||||
if (!shopName) {
|
||||
return json({
|
||||
success: false,
|
||||
message: "shop name is missing",
|
||||
data: null
|
||||
});
|
||||
}
|
||||
|
||||
const shop = `${shopName}.myshopify.com`;
|
||||
const btcpayServerRecord = await db.bTCPayServerStore.findFirst({
|
||||
where: { shop }
|
||||
});
|
||||
if (!btcpayServerRecord) {
|
||||
return json({
|
||||
success: false,
|
||||
data: null,
|
||||
message: `No record found for shop: ${shopName}`
|
||||
});
|
||||
}
|
||||
|
||||
return json({
|
||||
ok: true,
|
||||
message: `Record found for shop: ${shopName}`,
|
||||
data: btcpayServerRecord
|
||||
});
|
||||
}
|
||||
@ -1,162 +0,0 @@
|
||||
import db from "../db.server";
|
||||
import { json } from "@remix-run/node";
|
||||
import { useEffect, useState } from "react";
|
||||
import { authenticate } from "../shopify.server";
|
||||
import { useFetcher, useLoaderData } from "@remix-run/react";
|
||||
import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
|
||||
import { Page, Text, Card, Button, BlockStack, InlineGrid, TextField, Box } from "@shopify/polaris";
|
||||
|
||||
export async function loader({ request }) {
|
||||
await authenticate.admin(request);
|
||||
const url = new URL(request.url);
|
||||
const shop = url.searchParams.get('shop');
|
||||
const btcpayServerRecord = await db.bTCPayServerStore.findFirst({
|
||||
where: { shop }
|
||||
});
|
||||
return { btcpayUrl: btcpayServerRecord?.btcpayUrl, btcpayStoreId: btcpayServerRecord?.btcpayStoreId };
|
||||
}
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
await authenticate.admin(request);
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const shop = url.searchParams.get('shop');
|
||||
const shopId = shop.split('.myshopify.com')[0];
|
||||
const formData = Object.fromEntries(await request.formData());
|
||||
let { btcpayUrl, btcpayStoreId } = formData;
|
||||
if (!btcpayUrl || !btcpayStoreId) {
|
||||
return json({ success: false, message: `Please input your BTCPay server domain url and store Id` }, { status: 400 });
|
||||
}
|
||||
btcpayUrl = btcpayUrl.endsWith('/') ? btcpayUrl.slice(0, -1) : btcpayUrl;
|
||||
const isValidBTCPayStore = await validateBTCPayStoreInstance(btcpayUrl, btcpayStoreId, shopId);
|
||||
if (!isValidBTCPayStore) {
|
||||
return json({ success: false, message: 'Failed to validate BTCPay store. Kindly ensure you have the plugin installed on your BTCPay Server instance.' }, { status: 400 });
|
||||
}
|
||||
const shopInfo = await getShopInfo(shop);
|
||||
var data = {
|
||||
btcpayUrl,
|
||||
shopName: shopInfo.name,
|
||||
shop,
|
||||
btcpayStoreId,
|
||||
shopId,
|
||||
shopOwner: shopInfo.name,
|
||||
currency: shopInfo.currency,
|
||||
country: shopInfo.country
|
||||
};
|
||||
const user = await db.bTCPayServerStore.upsert({
|
||||
where: { shop },
|
||||
update: data,
|
||||
create: data
|
||||
});
|
||||
return json({
|
||||
btcpayUrl, success: true,
|
||||
message: "BTCPay URL saved successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Error encountered', error.message)
|
||||
return json({ success: false, message: `Error: ${error.message}` }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
const getShopInfo = async (shopDomain) => {
|
||||
let session = await findSessionByShop(shopDomain);
|
||||
const response = await fetch(`https://${shopDomain}/admin/api/2023-04/shop.json`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Shopify-Access-Token': session.accessToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
return data.shop;
|
||||
};
|
||||
|
||||
const validateBTCPayStoreInstance = async (btcpayUrl, storeId, shopName) => {
|
||||
try {
|
||||
const response = await fetch(`${btcpayUrl}/stores/${storeId}/plugins/shopify/validate/${shopName}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const findSessionByShop = async (shop) => {
|
||||
return await db.session.findFirst({ where: { shop } });
|
||||
};
|
||||
|
||||
export default function Index() {
|
||||
const fetcher = useFetcher();
|
||||
const shopify = useAppBridge();
|
||||
const settings = useLoaderData();
|
||||
const [formState, setFormState] = useState(settings);
|
||||
const [errorMessage, setErrorMessage] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetcher.data?.success === false) {
|
||||
shopify.toast.show(fetcher.data.message || "Error saving BTCPay URL");
|
||||
setErrorMessage(fetcher.data.message);
|
||||
} else if (fetcher.data?.success === true) {
|
||||
setErrorMessage(null);
|
||||
shopify.toast.show("BTCPay URL saved successfully");
|
||||
}
|
||||
}, [fetcher.data, shopify]);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<TitleBar title="BTCPay Server - Shopify plugin" />
|
||||
<BlockStack gap={{ xs: "800", sm: "400" }}>
|
||||
<InlineGrid columns={{ xs: "1fr", md: "2fr 5fr" }} gap="400">
|
||||
<Box
|
||||
as="section"
|
||||
paddingInlineStart={{ xs: 400, sm: 0 }}
|
||||
paddingInlineEnd={{ xs: 400, sm: 0 }}
|
||||
>
|
||||
<BlockStack gap="400">
|
||||
<Text as="h3" variant="headingMd">
|
||||
BTCPay Server
|
||||
</Text>
|
||||
<Text as="p" variant="bodyMd">
|
||||
Please enter your BTCPay server Url and the Store ID (where the plugin is installed)
|
||||
</Text>
|
||||
</BlockStack>
|
||||
</Box>
|
||||
<Card roundedAbove="sm">
|
||||
<fetcher.Form method="POST">
|
||||
<BlockStack gap="400">
|
||||
<TextField
|
||||
label="BTCPay URL"
|
||||
name="btcpayUrl"
|
||||
value={formState?.btcpayUrl}
|
||||
onChange={(v) =>
|
||||
setFormState({ ...formState, btcpayUrl: v })
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
label="BTCPay Store Id"
|
||||
name="btcpayStoreId"
|
||||
value={formState?.btcpayStoreId}
|
||||
onChange={(v) =>
|
||||
setFormState({ ...formState, btcpayStoreId: v })
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
submit
|
||||
primary
|
||||
loading={fetcher.state === "submitting"}
|
||||
disabled={fetcher.state === "submitting"}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</BlockStack>
|
||||
</fetcher.Form>
|
||||
</Card>
|
||||
</InlineGrid>
|
||||
</BlockStack>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Layout,
|
||||
Link,
|
||||
List,
|
||||
Page,
|
||||
Text,
|
||||
BlockStack,
|
||||
} from "@shopify/polaris";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
|
||||
export default function AdditionalPage() {
|
||||
return (
|
||||
<Page>
|
||||
<TitleBar title="Additional page" />
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<Card>
|
||||
<BlockStack gap="300">
|
||||
<Text as="p" variant="bodyMd">
|
||||
The app template comes with an additional page which
|
||||
demonstrates how to create multiple pages within app navigation
|
||||
using{" "}
|
||||
<Link
|
||||
url="https://shopify.dev/docs/apps/tools/app-bridge"
|
||||
target="_blank"
|
||||
removeUnderline
|
||||
>
|
||||
App Bridge
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
<Text as="p" variant="bodyMd">
|
||||
To create your own page and have it show up in the app
|
||||
navigation, add a page inside <Code>app/routes</Code>, and a
|
||||
link to it in the <Code><NavMenu></Code> component found
|
||||
in <Code>app/routes/app.jsx</Code>.
|
||||
</Text>
|
||||
</BlockStack>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
<Layout.Section variant="oneThird">
|
||||
<Card>
|
||||
<BlockStack gap="200">
|
||||
<Text as="h2" variant="headingMd">
|
||||
Resources
|
||||
</Text>
|
||||
<List>
|
||||
<List.Item>
|
||||
<Link
|
||||
url="https://shopify.dev/docs/apps/design-guidelines/navigation#app-nav"
|
||||
target="_blank"
|
||||
removeUnderline
|
||||
>
|
||||
App nav best practices
|
||||
</Link>
|
||||
</List.Item>
|
||||
</List>
|
||||
</BlockStack>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return (
|
||||
<Box
|
||||
as="span"
|
||||
padding="025"
|
||||
paddingInlineStart="100"
|
||||
paddingInlineEnd="100"
|
||||
background="bg-surface-active"
|
||||
borderWidth="025"
|
||||
borderColor="border"
|
||||
borderRadius="100"
|
||||
>
|
||||
<code>{children}</code>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
import { json } from "@remix-run/node";
|
||||
import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
|
||||
import { boundary } from "@shopify/shopify-app-remix/server";
|
||||
import { AppProvider } from "@shopify/shopify-app-remix/react";
|
||||
import { NavMenu } from "@shopify/app-bridge-react";
|
||||
import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
await authenticate.admin(request);
|
||||
|
||||
return json({ apiKey: process.env.SHOPIFY_API_KEY || "" });
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const { apiKey } = useLoaderData();
|
||||
|
||||
return (
|
||||
<AppProvider isEmbeddedApp apiKey={apiKey}>
|
||||
<NavMenu>
|
||||
<Link to="/app" rel="home">
|
||||
Home
|
||||
</Link>
|
||||
</NavMenu>
|
||||
<Outlet />
|
||||
</AppProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Shopify needs Remix to catch some thrown responses, so that their headers are included in the response.
|
||||
export function ErrorBoundary() {
|
||||
return boundary.error(useRouteError());
|
||||
}
|
||||
|
||||
export const headers = (headersArgs) => {
|
||||
return boundary.headers(headersArgs);
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
await authenticate.admin(request);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,11 +0,0 @@
|
||||
import { LoginErrorType } from "@shopify/shopify-app-remix/server";
|
||||
|
||||
export function loginErrorMessage(loginErrors) {
|
||||
if (loginErrors?.shop === LoginErrorType.MissingShop) {
|
||||
return { shop: "Please enter your shop domain to log in" };
|
||||
} else if (loginErrors?.shop === LoginErrorType.InvalidShop) {
|
||||
return { shop: "Please enter a valid shop domain to log in" };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { json } from "@remix-run/node";
|
||||
import { Form, useActionData, useLoaderData } from "@remix-run/react";
|
||||
import {
|
||||
AppProvider as PolarisAppProvider,
|
||||
Button,
|
||||
Card,
|
||||
FormLayout,
|
||||
Page,
|
||||
Text,
|
||||
TextField,
|
||||
} from "@shopify/polaris";
|
||||
import polarisTranslations from "@shopify/polaris/locales/en.json";
|
||||
import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
|
||||
import { login } from "../../shopify.server";
|
||||
import { loginErrorMessage } from "./error.server";
|
||||
|
||||
export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const errors = loginErrorMessage(await login(request));
|
||||
|
||||
return json({ errors, polarisTranslations });
|
||||
};
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const errors = loginErrorMessage(await login(request));
|
||||
|
||||
return json({
|
||||
errors,
|
||||
});
|
||||
};
|
||||
|
||||
export default function Auth() {
|
||||
const loaderData = useLoaderData();
|
||||
const actionData = useActionData();
|
||||
const [shop, setShop] = useState("");
|
||||
const { errors } = actionData || loaderData;
|
||||
|
||||
return (
|
||||
<PolarisAppProvider i18n={loaderData.polarisTranslations}>
|
||||
<Page>
|
||||
<Card>
|
||||
<Form method="post">
|
||||
<FormLayout>
|
||||
<Text variant="headingMd" as="h2">
|
||||
Log in
|
||||
</Text>
|
||||
<TextField
|
||||
type="text"
|
||||
name="shop"
|
||||
label="Shop domain"
|
||||
helpText="example.myshopify.com"
|
||||
value={shop}
|
||||
onChange={setShop}
|
||||
autoComplete="on"
|
||||
error={errors.shop}
|
||||
/>
|
||||
<Button submit>Log in</Button>
|
||||
</FormLayout>
|
||||
</Form>
|
||||
</Card>
|
||||
</Page>
|
||||
</PolarisAppProvider>
|
||||
);
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import { authenticate } from "../shopify.server";
|
||||
import db from "../db.server";
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const { shop, session, topic } = await authenticate.webhook(request);
|
||||
|
||||
console.log(`Received ${topic} webhook for ${shop}`);
|
||||
|
||||
// Webhook requests can trigger multiple times and after an app has already been uninstalled.
|
||||
// If this webhook already ran, the session may have been deleted previously.
|
||||
if (session) {
|
||||
await db.session.deleteMany({ where: { shop } });
|
||||
}
|
||||
|
||||
return new Response();
|
||||
};
|
||||
@ -1,12 +0,0 @@
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const { shop, payload, topic } = await authenticate.webhook(request);
|
||||
|
||||
// Implement handling of mandatory compliance topics
|
||||
// See: https://shopify.dev/docs/apps/build/privacy-law-compliance
|
||||
console.log(`Received ${topic} webhook for ${shop}`);
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
|
||||
return new Response();
|
||||
};
|
||||
@ -1,12 +0,0 @@
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const { shop, payload, topic } = await authenticate.webhook(request);
|
||||
|
||||
// Implement handling of mandatory compliance topics
|
||||
// See: https://shopify.dev/docs/apps/build/privacy-law-compliance
|
||||
console.log(`Received ${topic} webhook for ${shop}`);
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
|
||||
return new Response();
|
||||
};
|
||||
@ -1,12 +0,0 @@
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const { shop, payload, topic } = await authenticate.webhook(request);
|
||||
|
||||
// Implement handling of mandatory compliance topics
|
||||
// See: https://shopify.dev/docs/apps/build/privacy-law-compliance
|
||||
console.log(`Received ${topic} webhook for ${shop}`);
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
|
||||
return new Response();
|
||||
};
|
||||
@ -1,36 +0,0 @@
|
||||
import "@shopify/shopify-app-remix/adapters/node";
|
||||
import {
|
||||
ApiVersion,
|
||||
AppDistribution,
|
||||
shopifyApp,
|
||||
} from "@shopify/shopify-app-remix/server";
|
||||
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
|
||||
import { restResources } from "@shopify/shopify-api/rest/admin/2024-07";
|
||||
import prisma from "./db.server";
|
||||
|
||||
const shopify = shopifyApp({
|
||||
apiKey: process.env.SHOPIFY_API_KEY,
|
||||
apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
|
||||
apiVersion: ApiVersion.October24,
|
||||
scopes: process.env.SCOPES?.split(","),
|
||||
appUrl: process.env.SHOPIFY_APP_URL || "",
|
||||
authPathPrefix: "/auth",
|
||||
sessionStorage: new PrismaSessionStorage(prisma),
|
||||
distribution: AppDistribution.AppStore,
|
||||
restResources,
|
||||
future: {
|
||||
unstable_newEmbeddedAuthStrategy: true,
|
||||
},
|
||||
...(process.env.SHOP_CUSTOM_DOMAIN
|
||||
? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
|
||||
: {}),
|
||||
});
|
||||
|
||||
export default shopify;
|
||||
export const apiVersion = ApiVersion.October24;
|
||||
export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
|
||||
export const authenticate = shopify.authenticate;
|
||||
export const unauthenticated = shopify.unauthenticated;
|
||||
export const login = shopify.login;
|
||||
export const registerWebhooks = shopify.registerWebhooks;
|
||||
export const sessionStorage = shopify.sessionStorage;
|
||||
27
deploy.sh
Normal file
27
deploy.sh
Normal file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
COMMIT="$(git log -1 --format=%H)"
|
||||
pushd .
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
echo "COMMIT=${COMMIT}"
|
||||
echo "VERSION=$(git describe --tags --abbrev=0)"
|
||||
cd "$TEMP_DIR"
|
||||
echo "Creating plugin in directory: ${TEMP_DIR}"
|
||||
cp -rf /app/* "${TEMP_DIR}"
|
||||
|
||||
cp shopify.app.toml.example shopify.app.toml
|
||||
sed -i "s|APP_NAME|${APP_NAME}|g" shopify.app.toml
|
||||
sed -i "s|CLIENT_ID|${CLIENT_ID}|g" shopify.app.toml
|
||||
sed -i "s|PLUGIN_URL|${PLUGIN_URL}|g" shopify.app.toml
|
||||
sed -i "s|PLUGIN_URL|${PLUGIN_URL}|g" extensions/btcpaycheckout/src/Checkout.jsx
|
||||
echo "Settings saved"
|
||||
|
||||
if shopify app deploy -f --no-color; then
|
||||
echo "SUCCESS=true"
|
||||
else
|
||||
echo "SUCCESS=false"
|
||||
fi
|
||||
|
||||
popd
|
||||
|
||||
rm -rf "$TEMP_DIR"
|
||||
3
docker-entrypoint.sh
Executable file
3
docker-entrypoint.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
node server.js
|
||||
2
env.d.ts
vendored
2
env.d.ts
vendored
@ -1,2 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="@remix-run/node" />
|
||||
@ -1,3 +0,0 @@
|
||||
export const config = {
|
||||
appUrl: process.env.DOMAIN
|
||||
};
|
||||
10
extensions/btcpaycheckout/locales/cs.json
Normal file
10
extensions/btcpaycheckout/locales/cs.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Název obchodu",
|
||||
"reviewAndPay": "Zkontrolujte a zaplaťte pomocí BTCpay Server!",
|
||||
"reviewOrderMessage": "Prosím, zkontrolujte si objednávku a vyplňte platbu pomocí BTCpay Server.",
|
||||
"complete_payment": "Kompletní platba",
|
||||
"error": {
|
||||
"fetch_invoice": "Chyba BTCpay Serveru. Nepodařilo se získat fakturu. {{error}}",
|
||||
"general": "Chyba BTCpay Serveru. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/da.json
Normal file
10
extensions/btcpaycheckout/locales/da.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Handelsnavn",
|
||||
"reviewAndPay": "Gennemgå og betale ved hjælp af BTCPAy Server!",
|
||||
"reviewOrderMessage": "Gennemgå din ordre og fuldføre betalingen ved hjælp af BTCPAy Server.",
|
||||
"complete_payment": "Fuldstændig betaling",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay Server Fejl. Kunne ikke hente faktura. {{error}}",
|
||||
"general": "BTCPay Server Fejl. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/de.json
Normal file
10
extensions/btcpaycheckout/locales/de.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Name des Shops",
|
||||
"reviewAndPay": "Bewerten und bezahlen Sie mit dem BTCPay-Server!",
|
||||
"reviewOrderMessage": "Bitte überprüfen Sie Ihre Bestellung und schließen Sie die Zahlung mit dem BTCPay-Server ab.",
|
||||
"complete_payment": "Zahlung abschließen",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay-Serverfehler. Rechnung konnte nicht abgerufen werden. {{error}}",
|
||||
"general": "BTCPay-Serverfehler. {{error}}"
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,10 @@
|
||||
{
|
||||
"welcome": "Welcome to the {{target}} extension!",
|
||||
"iWouldLikeAFreeGiftWithMyOrder": "I would like to receive a free gift with my order",
|
||||
"attributeChangesAreNotSupported": "Attribute changes are not supported in this checkout"
|
||||
}
|
||||
"shop_name": "Shop name",
|
||||
"reviewAndPay": "Review and pay using BTCPay Server!",
|
||||
"reviewOrderMessage": "Please review your order and complete the payment using BTCPay Server.",
|
||||
"complete_payment": "Complete Payment",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay Server Error. Failed to fetch invoice. {{error}}",
|
||||
"general": "BTCPay Server Error. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/es.json
Normal file
10
extensions/btcpaycheckout/locales/es.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Nombre de la tienda",
|
||||
"reviewAndPay": "¡Revisa y paga usando BTCPay Server!",
|
||||
"reviewOrderMessage": "Por favor revisa tu pedido y completa el pago usando BTCPay Server.",
|
||||
"complete_payment": "Completar Pago",
|
||||
"error": {
|
||||
"fetch_invoice": "Error del Servidor BTCPay. Error al obtener la factura. {{error}}",
|
||||
"general": "Error del Servidor BTCPay. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/fi.json
Normal file
10
extensions/btcpaycheckout/locales/fi.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Kaupan nimi",
|
||||
"reviewAndPay": "Tarkista ja maksa käyttäen BTCPay Server!",
|
||||
"reviewOrderMessage": "Tarkista tilauksesi ja suorita maksu BTCPay-palvelimella.",
|
||||
"complete_payment": "Täydellinen maksu",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay-palvelinvirhe. Laskun nouto epäonnistui. {{error}}",
|
||||
"general": "BTCPay-palvelinvirhe. {{error}}"
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,10 @@
|
||||
{
|
||||
"welcome": "Bienvenue dans l'extension {{target}}!",
|
||||
"iWouldLikeAFreeGiftWithMyOrder": "Je souhaite recevoir un cadeau gratuit avec ma commande",
|
||||
"attributeChangesAreNotSupported": "Les modifications d'attribut ne sont pas prises en charge dans cette commande"
|
||||
}
|
||||
"shop_name": "Nom du magasin",
|
||||
"reviewAndPay": "Vérifiez et payez en utilisant BTCPay Server !",
|
||||
"reviewOrderMessage": "Veuillez vérifier votre commande et effectuer le paiement via le serveur BTCPay.",
|
||||
"complete_payment": "Terminer le Paiement",
|
||||
"error": {
|
||||
"fetch_invoice": "Erreur du serveur BTCPay. Échec de la récupération de la facture. {{error}}",
|
||||
"general": "Erreur du serveur BTCPay. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/hi.json
Normal file
10
extensions/btcpaycheckout/locales/hi.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "दुकान का नाम",
|
||||
"reviewAndPay": "BTCPay सर्वर का उपयोग करके समीक्षा और भुगतान करें!",
|
||||
"reviewOrderMessage": "कृपया अपने ऑर्डर की समीक्षा करें और BTCPay सर्वर का उपयोग करके भुगतान पूरा करें।.",
|
||||
"complete_payment": "पूर्ण भुगतान",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay सर्वर त्रुटि। चालान प्राप्त करने में विफल रहा।. {{error}}",
|
||||
"general": "BTCPay सर्वर त्रुटि।. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/hu.json
Normal file
10
extensions/btcpaycheckout/locales/hu.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "A bolt neve",
|
||||
"reviewAndPay": "Áttekintés és fizetés segítségével BTCPay Server!",
|
||||
"reviewOrderMessage": "Kérjük, olvassa el megrendelését, és töltse ki a kifizetést a BTCPay Server használatával.",
|
||||
"complete_payment": "Teljes kifizetés",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay szerver hiba. Nem sikerült számlát beszerezni. {{error}}",
|
||||
"general": "BTCPay szerver hiba. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/it.json
Normal file
10
extensions/btcpaycheckout/locales/it.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Nome negozio",
|
||||
"reviewAndPay": "Rivedere e pagare usando BTCPay Server!",
|
||||
"reviewOrderMessage": "Per favore controlla il tuo ordine e completa il pagamento usando BTCPay Server.",
|
||||
"complete_payment": "Completa Pagamento",
|
||||
"error": {
|
||||
"fetch_invoice": "Errore BTCPay Server. Impossibile recuperare la fattura. {{error}}",
|
||||
"general": "Errore BTCPay Server. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/ja.json
Normal file
10
extensions/btcpaycheckout/locales/ja.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "店舗名",
|
||||
"reviewAndPay": "BTCPay Server でのレビューと支払い!",
|
||||
"reviewOrderMessage": "BTCPay Server を使用して注文を確認し、支払いを完了してください.",
|
||||
"complete_payment": "完全な支払",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay Server エラー。 請求書を受け取りませんでした. {{error}}",
|
||||
"general": "BTCPay Server エラー. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/ko.json
Normal file
10
extensions/btcpaycheckout/locales/ko.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "가게 이름",
|
||||
"reviewAndPay": "BTCPay Server를 사용하여 검토 및 지불!",
|
||||
"reviewOrderMessage": "주문을 검토하고 BTCPay Server를 사용하여 지불을 완료하십시오.",
|
||||
"complete_payment": "결제 완료",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay 서버 오류. 청구서에 실패. {{error}}",
|
||||
"general": "BTCPay 서버 오류. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/nb.json
Normal file
10
extensions/btcpaycheckout/locales/nb.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Butikknavn",
|
||||
"reviewAndPay": "Se over og betal med BTCPay Server!",
|
||||
"reviewOrderMessage": "Vennligst se over bestillingen din og fullfør betalingen med BTCPay Server.",
|
||||
"complete_payment": "Fullfør betaling",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay Server-feil. Klarte ikke hente faktura. {{error}}",
|
||||
"general": "BTCPay Server-feil. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/nl.json
Normal file
10
extensions/btcpaycheckout/locales/nl.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Winkelnaam",
|
||||
"reviewAndPay": "Bekijk en betaal met BTCPay Server!",
|
||||
"reviewOrderMessage": "Bekijk uw bestelling en vul de betaling met BTCPay Server.",
|
||||
"complete_payment": "Volledige betaling",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay-serverfout Ophalen van factuur is mislukt. {{error}}",
|
||||
"general": "BTCPay-serverfout. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/pl.json
Normal file
10
extensions/btcpaycheckout/locales/pl.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Nazwa sklepu",
|
||||
"reviewAndPay": "Przegląd i zapłacić za pomocą serwera BTCPAy!",
|
||||
"reviewOrderMessage": "Proszę przejrzeć zamówienie i dokonać płatności za pomocą serwera BTCPAy.",
|
||||
"complete_payment": "Płatność pełna",
|
||||
"error": {
|
||||
"fetch_invoice": "Błąd serwera BTCPAy. Nie udało się pobrać faktury. {{error}}",
|
||||
"general": "Błąd serwera BTCPAy. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/pt-BR.json
Normal file
10
extensions/btcpaycheckout/locales/pt-BR.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Nome da loja",
|
||||
"reviewAndPay": "Reveja e pague usando o BTCPay Server!",
|
||||
"reviewOrderMessage": "Por favor, reveja seu pedido e complete o pagamento usando o BTCPay Server.",
|
||||
"complete_payment": "Pagamento completo",
|
||||
"error": {
|
||||
"fetch_invoice": "Erro no servidor BTCPay. Não consegui pegar a fatura. {{error}}",
|
||||
"general": "Erro no servidor BTCPay. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/pt.json
Normal file
10
extensions/btcpaycheckout/locales/pt.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Nome da loja",
|
||||
"reviewAndPay": "Revise e pague usando BTCPay Server!",
|
||||
"reviewOrderMessage": "Por favor revise seu pedido e complete o pagamento usando BTCPay Server.",
|
||||
"complete_payment": "Completar Pagamento",
|
||||
"error": {
|
||||
"fetch_invoice": "Erro do BTCPay Server. Falha ao buscar fatura. {{error}}",
|
||||
"general": "Erro do BTCPay Server. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/ru.json
Normal file
10
extensions/btcpaycheckout/locales/ru.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Название магазина",
|
||||
"reviewAndPay": "Обзор и оплата с помощью BTCPay Server!",
|
||||
"reviewOrderMessage": "Пожалуйста, просмотрите ваш заказ и заполните оплату с помощью BTCPay Server.",
|
||||
"complete_payment": "Полный платеж",
|
||||
"error": {
|
||||
"fetch_invoice": "Ошибка BTCPay Server. Не удалось получить счет. {{error}}",
|
||||
"general": "Ошибка BTCPay Server. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/sv.json
Normal file
10
extensions/btcpaycheckout/locales/sv.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Shop name",
|
||||
"reviewAndPay": "Granska och betala med BTCPay Server!",
|
||||
"reviewOrderMessage": "Vänligen granska din beställning och slutföra betalningen med hjälp av BTCPay Server.",
|
||||
"complete_payment": "Fullständig betalning",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay Server Fel. Misslyckades med att hämta faktura. {{error}}",
|
||||
"general": "BTCPay Server Fel. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/th.json
Normal file
10
extensions/btcpaycheckout/locales/th.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "ชื่อร้านค้า",
|
||||
"reviewAndPay": "ทบทวนและจ่ายจากเซิร์ฟเวอร์บีทีซี!",
|
||||
"reviewOrderMessage": "กรุณาทบทวนคําสั่งของคุณ และชําระเงินด้วยเซิร์ฟเวอร์บีทีซี.",
|
||||
"complete_payment": "การชําระเงินที่สมบูรณ์",
|
||||
"error": {
|
||||
"fetch_invoice": "เซิร์ฟเวอร์ BTC มีข้อผิดพลาด ล้มเหลวในการเรียกใบแจ้งหนี้. {{error}}",
|
||||
"general": "เซิร์ฟเวอร์ BTC มีข้อผิดพลาด. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/tr.json
Normal file
10
extensions/btcpaycheckout/locales/tr.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Dükkan adı",
|
||||
"reviewAndPay": "BTCPay Server kullanarak yorum ve ödeme!",
|
||||
"reviewOrderMessage": "Lütfen siparişinizi gözden geçirin ve BTCPay Server kullanarak ödeme tamamlayın.",
|
||||
"complete_payment": "Tamam Ödeme",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay Server Hatası. Fatura getirmek için başarısız oldu. {{error}}",
|
||||
"general": "BTCPay Server Hatası. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/vi.json
Normal file
10
extensions/btcpaycheckout/locales/vi.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "Tên cửa hàng",
|
||||
"reviewAndPay": "Xem lại và trả tiền bằng máy chủ trả tiền BTC!",
|
||||
"reviewOrderMessage": "Vui lòng xem lại đơn đặt hàng và hoàn tất việc thanh toán bằng máy phục vụ trả tiền BTC.",
|
||||
"complete_payment": "Trả tiền đầy đủ",
|
||||
"error": {
|
||||
"fetch_invoice": "Lỗi trình phục vụ trả tiền BTC. Không lấy được hóa đơn. {{error}}",
|
||||
"general": "Lỗi trình phục vụ trả tiền BTC. {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/zh-CN.json
Normal file
10
extensions/btcpaycheckout/locales/zh-CN.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "商店名称",
|
||||
"reviewAndPay": "用 BTCPay 服务器审查并支付!",
|
||||
"reviewOrderMessage": "请审查您的订单并使用 BTCPay 服务器完成支付 .",
|
||||
"complete_payment": "全额付款",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay 服务器出错 。 获取发票失败 . {{error}}",
|
||||
"general": "BTCPay 服务器出错 . {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/zh-TW.json
Normal file
10
extensions/btcpaycheckout/locales/zh-TW.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "店名",
|
||||
"reviewAndPay": "使用 BTCPay 伺服器去審查并支付!",
|
||||
"reviewOrderMessage": "請回復您的訂單并用 BTCPay 伺服器完成支付 .",
|
||||
"complete_payment": "全部付款",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay 伺服器出錯 。 取回收據失敗 . {{error}}",
|
||||
"general": "BTCPay 伺服器出錯 . {{error}}"
|
||||
}
|
||||
}
|
||||
10
extensions/btcpaycheckout/locales/zh.json
Normal file
10
extensions/btcpaycheckout/locales/zh.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"shop_name": "商店名称",
|
||||
"reviewAndPay": "用 BTCPay 服务器审查并支付!",
|
||||
"reviewOrderMessage": "请审查您的订单并使用 BTCPay 服务器完成支付 .",
|
||||
"complete_payment": "全额付款",
|
||||
"error": {
|
||||
"fetch_invoice": "BTCPay 服务器出错 。 获取发票失败 . {{error}}",
|
||||
"general": "BTCPay 服务器出错 . {{error}}"
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
# The version of APIs your extension will receive. Learn more:
|
||||
# https://shopify.dev/docs/api/usage/versioning
|
||||
api_version = "2024-10"
|
||||
api_version = "2025-07"
|
||||
|
||||
[[extensions]]
|
||||
name = "BTCPay Checkout"
|
||||
@ -22,7 +22,7 @@ target = "purchase.thank-you.block.render"
|
||||
[extensions.capabilities]
|
||||
# Gives your extension access to directly query Shopify’s storefront API.
|
||||
# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/configuration#api-access
|
||||
api_access = true
|
||||
api_access = false
|
||||
|
||||
# Gives your extension access to make external network calls, using the
|
||||
# JavaScript `fetch()` API. Learn more:
|
||||
|
||||
@ -1,21 +1,14 @@
|
||||
import {
|
||||
reactExtension,
|
||||
Banner,
|
||||
BlockStack,
|
||||
Button,
|
||||
Text,
|
||||
useApi,
|
||||
Link,
|
||||
Modal,
|
||||
Spinner,
|
||||
TextBlock,
|
||||
useTotalAmount,
|
||||
useInstructions,
|
||||
useTranslate,
|
||||
useSelectedPaymentOptions
|
||||
} from "@shopify/ui-extensions-react/checkout";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { config } from '../config';
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// 1. Choose an extension target
|
||||
export default reactExtension(
|
||||
@ -25,220 +18,57 @@ export default reactExtension(
|
||||
|
||||
function Extension() {
|
||||
const translate = useTranslate();
|
||||
const { shop, ui, checkoutToken } = useApi();
|
||||
const { currencyCode, amount } = useTotalAmount();
|
||||
const instructions = useInstructions();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [orderId, setOrderId] = useState(null);
|
||||
const [btcPayUrl, setBtcPayUrl] = useState(null);
|
||||
const [btcPayStoreId, setBtcPayStoreId] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [modalContent, setModalContent] = useState(null);
|
||||
const [isTokenValid, setIsTokenValid] = useState(false);
|
||||
const [isPaid, setIsPaid] = useState(false);
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
const [modalTitle, setModalTitle] = useState('Pay with Bitcoin/Lightning Network');
|
||||
const shopName = shop.myshopifyDomain.split('.myshopify.com')[0];
|
||||
const options = useSelectedPaymentOptions();
|
||||
let { appUrl } = config;
|
||||
const { shop, checkoutToken } = useApi();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const hasManualPayment = options.some((option) => option.type.toLowerCase() === 'manualpayment');
|
||||
const appUrl = `PLUGIN_URL/checkout?checkout_token=${checkoutToken.current}`;
|
||||
|
||||
useEffect(() => {
|
||||
const hasManualPaymentOption = options.some((option) => option.type.toLowerCase() === 'manualpayment');
|
||||
if (hasManualPaymentOption) {
|
||||
const timer = setTimeout(async () => {
|
||||
await validateToken();
|
||||
}, 5000);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}
|
||||
}, [options]);
|
||||
|
||||
const validateToken = async () => {
|
||||
try {
|
||||
const storeData = await retrieveBTCPayUrl(shopName);
|
||||
if (!storeData.btcpayUrl || !storeData.btcpayStoreId) {
|
||||
setError('Failed to retrieve BTCPay URL or Store ID');
|
||||
}
|
||||
setBtcPayStoreId(storeData.btcpayStoreId);
|
||||
setBtcPayUrl(storeData.btcpayUrl);
|
||||
await setCheckTokenValidity(storeData.btcpayUrl, storeData.btcpayStoreId, shopName);
|
||||
} catch (error) {
|
||||
setError(`Failed to validate token: ${error.message}`);
|
||||
setIsTokenValid(false);
|
||||
}
|
||||
};
|
||||
|
||||
const retrieveBTCPayUrl = async (shopName) => {
|
||||
appUrl = appUrl.endsWith('/') ? appUrl.slice(0, -1) : appUrl;
|
||||
const response = await fetch(
|
||||
`https://${appUrl}/api/btcpaystores?shopName=${shopName}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
if (!hasManualPayment) return;
|
||||
const fetchInvoice = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${appUrl}&redirect=false`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (response.ok) {
|
||||
setIsSuccess(true);
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const { btcpayUrl, btcpayStoreId } = data.data;
|
||||
return { btcpayUrl, btcpayStoreId };
|
||||
}
|
||||
};
|
||||
|
||||
const setCheckTokenValidity = async (btcpayurl, btcpaystoreId, shopName) => {
|
||||
try {
|
||||
const validationResponse = await validateCheckoutToken(btcpayurl, btcpaystoreId, shopName, checkoutToken.current);
|
||||
if (validationResponse.success) {
|
||||
if(validationResponse.data.financialStatus === "success"){
|
||||
setIsTokenValid(false);
|
||||
setIsPaid(true);
|
||||
else if (response.status !== 404) {
|
||||
const errorText = await response.text();
|
||||
setErrorMessage(translate("error.fetch_invoice", { error: errorText || response.statusText }));
|
||||
}
|
||||
else{
|
||||
setIsTokenValid(true);
|
||||
setOrderId(validationResponse.data.orderId);
|
||||
setModalTitle(validationResponse.data.paymentMethodDescription);
|
||||
setRetryCount(0);
|
||||
}
|
||||
} else {
|
||||
retryTokenValidation();
|
||||
} catch (error) {
|
||||
setErrorMessage(translate("error.general", { error: error.message }));
|
||||
}
|
||||
} catch (error) {
|
||||
retryTokenValidation();
|
||||
}
|
||||
};
|
||||
|
||||
const retryTokenValidation = () => {
|
||||
if (retryCount >= 3) {
|
||||
setError("Failed to connect to BTCPay Server instance. Please contact support or try again later.");
|
||||
setIsTokenValid(false);
|
||||
} else {
|
||||
setRetryCount(retryCount + 1);
|
||||
setIsTokenValid(false);
|
||||
setTimeout(async () => {
|
||||
await validateToken();
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const validateCheckoutToken = async (btcpayUrl, btcpayStoreId, shopName, token) => {
|
||||
try {
|
||||
const response = await fetch(`${btcpayUrl}/stores/${btcpayStoreId}/plugins/shopify/order/${shopName}/${token}`, {
|
||||
method: 'GET',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
});
|
||||
if (response.ok) {
|
||||
return {
|
||||
success: true,
|
||||
data: await response.json()
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: response.statusText
|
||||
};
|
||||
finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
const timer = setTimeout(fetchInvoice, 1000);
|
||||
return () => clearTimeout(timer)
|
||||
}, [hasManualPayment]);
|
||||
|
||||
const CreateBTCPayOrder = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const createOrderPayload = {
|
||||
shopName: shopName,
|
||||
orderId: orderId,
|
||||
currency: currencyCode,
|
||||
total: amount
|
||||
};
|
||||
console.log('Creating BTCPay order...');
|
||||
const createOrderResponse = await fetch(`${btcPayUrl}/stores/${btcPayStoreId}/plugins/shopify/${shopName}/create-order`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(createOrderPayload)
|
||||
});
|
||||
if (!createOrderResponse.ok) {
|
||||
setError(`Failed to create order: ${createOrderResponse.statusText}`);
|
||||
}
|
||||
const orderData = await createOrderResponse.json();
|
||||
console.log('Order created successfully');
|
||||
setModalContent(orderData);
|
||||
} catch (error) {
|
||||
setError(`Failed to load BTC Pay content: ${error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
if (!hasManualPayment) return null;
|
||||
|
||||
// 2. Check instructions for feature availability, see https://shopify.dev/docs/api/checkout-ui-extensions/apis/cart-instructions for details
|
||||
if (!instructions.attributes.canUpdateAttributes) {
|
||||
// For checkouts such as draft order invoices, cart attributes may not be allowed
|
||||
// Consider rendering a fallback UI or nothing at all, if the feature is unavailable
|
||||
return (
|
||||
<Banner title="btcpaytextshopext" status="warning">
|
||||
{translate("attributeChangesAreNotSupported")}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Render a UI
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<Banner status="critical">
|
||||
<Text>{error}</Text>
|
||||
</Banner>
|
||||
)}
|
||||
{isTokenValid ? (
|
||||
<BlockStack>
|
||||
<Text>Shop name: {shop.name}</Text>
|
||||
<Text size="large" alignment="center" bold>Review and pay using BTCPay Server!</Text>
|
||||
<Text>Please review your order and complete the payment using BTCPay Server.</Text>
|
||||
<Button onPress={async () => {
|
||||
await CreateBTCPayOrder();
|
||||
ui.overlay.open('btc-pay-modal');
|
||||
}}
|
||||
overlay={
|
||||
<Modal
|
||||
id="btc-pay-modal"
|
||||
padding
|
||||
title={modalTitle}
|
||||
onClose={async () => {
|
||||
await setCheckTokenValidity(btcPayUrl, btcPayStoreId, shopName);
|
||||
}}>
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : error ? (
|
||||
<TextBlock>{error}</TextBlock>
|
||||
) : modalContent ? (
|
||||
<Link to={`${modalContent.externalPaymentLink}`} external>
|
||||
Click to pay invoice
|
||||
</Link>
|
||||
) : (
|
||||
<TextBlock>No content available</TextBlock>
|
||||
)}
|
||||
</Modal>
|
||||
}>Complete Payment</Button>
|
||||
</BlockStack>
|
||||
) : isPaid ? (
|
||||
<BlockStack>
|
||||
<Text>Shop name: {shop.name}</Text>
|
||||
<Text size="large" alignment="center" bold>Thank you! Your payment was acknowledged.</Text>
|
||||
</BlockStack>
|
||||
) : (
|
||||
<BlockStack>
|
||||
<Text>Shop name: {shop.name}</Text>
|
||||
<Text size="large" alignment="center" bold>Review and pay using BTCPay Server!</Text>
|
||||
<Text>Kindly ignore if the payment method selected was not payment with BTCPay Server</Text>
|
||||
<Spinner />
|
||||
<Text>Validating your payment options...</Text>
|
||||
</BlockStack>
|
||||
<BlockStack>
|
||||
{isLoading && <Spinner />}
|
||||
{!isLoading && errorMessage && (
|
||||
<Text size="large" appearance="critical">{errorMessage}</Text>
|
||||
)}
|
||||
</>
|
||||
{!isLoading && isSuccess && (
|
||||
<>
|
||||
<Text>{translate("shop_name")}: {shop.name}</Text>
|
||||
<Text size="large" alignment="center" bold>{translate("reviewAndPay")}</Text>
|
||||
<Text>{translate("reviewOrderMessage")}.</Text>
|
||||
<Button to={appUrl} external>{translate("complete_payment")}</Button>
|
||||
</>
|
||||
)}
|
||||
</BlockStack>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
# add CORS headers
|
||||
add_header Access-Control-Allow-Origin '*' always;
|
||||
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, DELETE, PUT' always;
|
||||
add_header Access-Control-Allow-Headers 'Authorization, Content-Type, X-Shopify-Hmac-Sha256' always;
|
||||
|
||||
# Preflight request handling
|
||||
if ($request_method = OPTIONS) {
|
||||
add_header Access-Control-Allow-Origin '*' always;
|
||||
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, DELETE, PUT' always;
|
||||
add_header Access-Control-Allow-Headers 'Authorization, Content-Type, X-Shopify-Hmac-Sha256' always;
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 204;
|
||||
}
|
||||
15349
package-lock.json
generated
15349
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
@ -1,66 +1,8 @@
|
||||
{
|
||||
"name": "btcpayserverdemoplugin",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "prisma generate && prisma migrate deploy && remix vite:build",
|
||||
"dev": "shopify app dev",
|
||||
"config:link": "shopify app config link",
|
||||
"generate": "shopify app generate",
|
||||
"deploy": "shopify app deploy",
|
||||
"config:use": "shopify app config use",
|
||||
"env": "shopify app env",
|
||||
"start": "remix-serve ./build/server/index.js",
|
||||
"docker-start": "npm run setup && npm run start",
|
||||
"setup": "prisma generate && prisma migrate deploy",
|
||||
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
|
||||
"shopify": "shopify",
|
||||
"prisma": "prisma",
|
||||
"graphql-codegen": "graphql-codegen",
|
||||
"vite": "vite"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^18.20 || ^20.10 || >=21.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.11.0",
|
||||
"@remix-run/dev": "^2.7.1",
|
||||
"@remix-run/node": "^2.7.1",
|
||||
"@remix-run/react": "^2.7.1",
|
||||
"@remix-run/serve": "^2.7.1",
|
||||
"@shopify/app-bridge-react": "^4.1.2",
|
||||
"@shopify/polaris": "^12.0.0",
|
||||
"@shopify/shopify-app-remix": "^3.3.2",
|
||||
"@shopify/shopify-app-session-storage-prisma": "^5.0.2",
|
||||
"@shopify/ui-extensions-react": "2024.10.x",
|
||||
"dotenv": "^16.4.7",
|
||||
"isbot": "^5.1.0",
|
||||
"prisma": "^5.11.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"remix-utils": "^7.7.0",
|
||||
"vite-tsconfig-paths": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/eslint-config": "^2.7.1",
|
||||
"@shopify/api-codegen-preset": "^1.1.1",
|
||||
"@types/eslint": "^8.40.0",
|
||||
"@types/node": "^22.2.0",
|
||||
"@types/react": "^18.2.31",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"prettier": "^3.2.4",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.1.3"
|
||||
},
|
||||
"workspaces": [
|
||||
"extensions/*"
|
||||
],
|
||||
"trustedDependencies": [
|
||||
"@shopify/plugin-cloudflare"
|
||||
],
|
||||
"resolutions": {},
|
||||
"overrides": {},
|
||||
"author": "PC"
|
||||
"dependencies": {
|
||||
"express": "^4.21.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"shop" TEXT NOT NULL,
|
||||
"state" TEXT NOT NULL,
|
||||
"isOnline" BOOLEAN NOT NULL DEFAULT false,
|
||||
"scope" TEXT,
|
||||
"expires" DATETIME,
|
||||
"accessToken" TEXT NOT NULL,
|
||||
"userId" BIGINT,
|
||||
"firstName" TEXT,
|
||||
"lastName" TEXT,
|
||||
"email" TEXT,
|
||||
"accountOwner" BOOLEAN NOT NULL DEFAULT false,
|
||||
"locale" TEXT,
|
||||
"collaborator" BOOLEAN DEFAULT false,
|
||||
"emailVerified" BOOLEAN DEFAULT false
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BTCPayServerStore" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"btcpayUrl" TEXT NOT NULL,
|
||||
"shopName" TEXT,
|
||||
"shop" TEXT NOT NULL,
|
||||
"btcpayStoreId" TEXT NOT NULL,
|
||||
"shopId" TEXT,
|
||||
"shopOwner" TEXT,
|
||||
"currency" TEXT,
|
||||
"country" TEXT
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BTCPayServerStore_shop_key" ON "BTCPayServerStore"("shop");
|
||||
@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
||||
@ -1,44 +0,0 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
// Note that some adapters may set a maximum length for the String type by default, please ensure your strings are long
|
||||
// enough when changing adapters.
|
||||
// See https://www.prisma.io/docs/orm/reference/prisma-schema-reference#string for more information
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id
|
||||
shop String
|
||||
state String
|
||||
isOnline Boolean @default(false)
|
||||
scope String?
|
||||
expires DateTime?
|
||||
accessToken String
|
||||
userId BigInt?
|
||||
firstName String?
|
||||
lastName String?
|
||||
email String?
|
||||
accountOwner Boolean @default(false)
|
||||
locale String?
|
||||
collaborator Boolean? @default(false)
|
||||
emailVerified Boolean? @default(false)
|
||||
}
|
||||
|
||||
model BTCPayServerStore {
|
||||
id Int @id @default(autoincrement())
|
||||
btcpayUrl String
|
||||
shopName String?
|
||||
shop String @unique
|
||||
btcpayStoreId String
|
||||
shopId String?
|
||||
shopOwner String?
|
||||
currency String?
|
||||
country String?
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@ -1,20 +0,0 @@
|
||||
// Related: https://github.com/remix-run/remix/issues/2835#issuecomment-1144102176
|
||||
// Replace the HOST env var with SHOPIFY_APP_URL so that it doesn't break the remix server. The CLI will eventually
|
||||
// stop passing in HOST, so we can remove this workaround after the next major release.
|
||||
if (
|
||||
process.env.HOST &&
|
||||
(!process.env.SHOPIFY_APP_URL ||
|
||||
process.env.SHOPIFY_APP_URL === process.env.HOST)
|
||||
) {
|
||||
process.env.SHOPIFY_APP_URL = process.env.HOST;
|
||||
delete process.env.HOST;
|
||||
}
|
||||
|
||||
/** @type {import('@remix-run/dev').AppConfig} */
|
||||
module.exports = {
|
||||
ignoredRouteFiles: ["**/.*"],
|
||||
appDirectory: "app",
|
||||
serverModuleFormat: "cjs",
|
||||
dev: { port: process.env.HMR_SERVER_PORT || 8002 },
|
||||
future: {},
|
||||
};
|
||||
31
server.js
Normal file
31
server.js
Normal file
@ -0,0 +1,31 @@
|
||||
const express = require('express');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.post('/deploy', (req, res) => {
|
||||
const process = spawn('bash', ['/app/deploy.sh'],
|
||||
{
|
||||
env:
|
||||
{
|
||||
"SHOPIFY_CLI_PARTNERS_TOKEN": req.body.cliToken,
|
||||
"CLIENT_ID": req.body.clientId,
|
||||
"PLUGIN_URL": req.body.pluginUrl,
|
||||
"APP_NAME": req.body.appName
|
||||
}
|
||||
});
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
process.stdout.on('data', (data) => {
|
||||
res.write(data.toString()); // Stream stdout
|
||||
});
|
||||
|
||||
process.stderr.on('data', (data) => {
|
||||
res.write(data.toString()); // Stream stderr
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
res.end(`\nProcess exited with code ${code}\n`);
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(5000, () => console.log('Server running on port 5000'));
|
||||
@ -1,45 +1,18 @@
|
||||
|
||||
# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration
|
||||
name = "APP_NAME"
|
||||
|
||||
client_id = "YOURCLIENT_ID"
|
||||
name = "BTCPay Server APPNAME"
|
||||
handle = "btcpay-server-APPNAME"
|
||||
application_url = "https://YOUR_HOSTED_APP_URL.COM/"
|
||||
client_id = "CLIENT_ID"
|
||||
application_url = "PLUGIN_URL"
|
||||
embedded = true
|
||||
|
||||
[build]
|
||||
automatically_update_urls_on_dev = false
|
||||
dev_store_url = "YOURDEV_STORE.myshopify.com"
|
||||
include_config_on_deploy = true
|
||||
|
||||
[access_scopes]
|
||||
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
|
||||
scopes = ""
|
||||
scopes = "read_orders,write_orders,read_customers"
|
||||
|
||||
[auth]
|
||||
redirect_urls = [
|
||||
"https://YOUR_HOSTED_APP_URL.COM/auth/callback",
|
||||
"https://YOUR_HOSTED_APP_URL.COM/auth/shopify/callback",
|
||||
"https://YOUR_HOSTED_APP_URL.COM/api/auth/callback"
|
||||
]
|
||||
redirect_urls = ["PLUGIN_URL/checkout"]
|
||||
|
||||
[webhooks]
|
||||
api_version = "2024-10"
|
||||
api_version = "2025-07"
|
||||
|
||||
[[webhooks.subscriptions]]
|
||||
uri = "/webhooks/customers/data_request"
|
||||
compliance_topics = [ "customers/data_request" ]
|
||||
|
||||
[[webhooks.subscriptions]]
|
||||
uri = "/webhooks/customers/redact"
|
||||
compliance_topics = [ "customers/redact" ]
|
||||
|
||||
[[webhooks.subscriptions]]
|
||||
uri = "/webhooks/shop/redact"
|
||||
compliance_topics = [ "shop/redact" ]
|
||||
|
||||
[[webhooks.subscriptions]]
|
||||
topics = [ "app/uninstalled" ]
|
||||
uri = "/webhooks/app/uninstalled"
|
||||
|
||||
[pos]
|
||||
embedded = false
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
name = "remix"
|
||||
roles = ["frontend", "backend"]
|
||||
webhooks_path = "/webhooks/app/uninstalled"
|
||||
|
||||
[commands]
|
||||
predev = "npx prisma generate"
|
||||
dev = "npx prisma migrate deploy && npm exec remix vite:dev"
|
||||
@ -1,55 +0,0 @@
|
||||
import { vitePlugin as remix } from "@remix-run/dev";
|
||||
import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
// Related: https://github.com/remix-run/remix/issues/2835#issuecomment-1144102176
|
||||
// Replace the HOST env var with SHOPIFY_APP_URL so that it doesn't break the remix server. The CLI will eventually
|
||||
// stop passing in HOST, so we can remove this workaround after the next major release.
|
||||
if (
|
||||
process.env.HOST &&
|
||||
(!process.env.SHOPIFY_APP_URL ||
|
||||
process.env.SHOPIFY_APP_URL === process.env.HOST)
|
||||
) {
|
||||
process.env.SHOPIFY_APP_URL = process.env.HOST;
|
||||
delete process.env.HOST;
|
||||
}
|
||||
|
||||
const host = new URL(process.env.SHOPIFY_APP_URL || "http://localhost")
|
||||
.hostname;
|
||||
let hmrConfig;
|
||||
|
||||
if (host === "localhost") {
|
||||
hmrConfig = {
|
||||
protocol: "ws",
|
||||
host: "localhost",
|
||||
port: 64999,
|
||||
clientPort: 64999,
|
||||
};
|
||||
} else {
|
||||
hmrConfig = {
|
||||
protocol: "wss",
|
||||
host: host,
|
||||
port: parseInt(process.env.FRONTEND_PORT) || 8002,
|
||||
clientPort: 443,
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: Number(process.env.PORT || 3000),
|
||||
hmr: hmrConfig,
|
||||
fs: {
|
||||
// See https://vitejs.dev/config/server-options.html#server-fs-allow for more information
|
||||
allow: ["app", "node_modules"],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
remix({
|
||||
ignoredRouteFiles: ["**/.*"],
|
||||
}),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
build: {
|
||||
assetsInlineLimit: 0,
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user