From 755b032f17593012773b59db9ea3a47fb8fadf69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Wed, 15 Feb 2023 10:46:39 +0100 Subject: [PATCH] remove offlineshop --- lnbits/extensions/offlineshop/README.md | 36 -- lnbits/extensions/offlineshop/__init__.py | 26 -- lnbits/extensions/offlineshop/config.json | 8 - lnbits/extensions/offlineshop/crud.py | 117 ------ lnbits/extensions/offlineshop/helpers.py | 17 - lnbits/extensions/offlineshop/lnurl.py | 88 ----- lnbits/extensions/offlineshop/migrations.py | 39 -- lnbits/extensions/offlineshop/models.py | 138 ------- .../offlineshop/static/image/offlineshop.png | Bin 13689 -> 0 bytes .../extensions/offlineshop/static/js/index.js | 230 ------------ .../templates/offlineshop/_api_docs.html | 154 -------- .../templates/offlineshop/index.html | 348 ------------------ .../templates/offlineshop/print.html | 28 -- lnbits/extensions/offlineshop/views.py | 89 ----- lnbits/extensions/offlineshop/views_api.py | 136 ------- lnbits/extensions/offlineshop/wordlists.py | 28 -- 16 files changed, 1482 deletions(-) delete mode 100644 lnbits/extensions/offlineshop/README.md delete mode 100644 lnbits/extensions/offlineshop/__init__.py delete mode 100644 lnbits/extensions/offlineshop/config.json delete mode 100644 lnbits/extensions/offlineshop/crud.py delete mode 100644 lnbits/extensions/offlineshop/helpers.py delete mode 100644 lnbits/extensions/offlineshop/lnurl.py delete mode 100644 lnbits/extensions/offlineshop/migrations.py delete mode 100644 lnbits/extensions/offlineshop/models.py delete mode 100644 lnbits/extensions/offlineshop/static/image/offlineshop.png delete mode 100644 lnbits/extensions/offlineshop/static/js/index.js delete mode 100644 lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html delete mode 100644 lnbits/extensions/offlineshop/templates/offlineshop/index.html delete mode 100644 lnbits/extensions/offlineshop/templates/offlineshop/print.html delete mode 100644 lnbits/extensions/offlineshop/views.py delete mode 100644 lnbits/extensions/offlineshop/views_api.py delete mode 100644 lnbits/extensions/offlineshop/wordlists.py diff --git a/lnbits/extensions/offlineshop/README.md b/lnbits/extensions/offlineshop/README.md deleted file mode 100644 index 7b9c6c8d..00000000 --- a/lnbits/extensions/offlineshop/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Offline Shop - -## Create QR codes for each product and display them on your store for receiving payments Offline - -[![video tutorial offline shop](http://img.youtube.com/vi/_XAvM_LNsoo/0.jpg)](https://youtu.be/_XAvM_LNsoo 'video tutorial offline shop') - -LNbits Offline Shop allows for merchants to receive Bitcoin payments while offline and without any electronic device. - -Merchant will create items and associate a QR code ([a LNURLp](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) with a price. He can then print the QR codes and display them on their shop. When a customer chooses an item, scans the QR code, gets the description and price. After payment, the customer gets a confirmation code that the merchant can validate to be sure the payment was successful. - -Customers must use an LNURL pay capable wallet. - -[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets) - -## Usage - -1. Entering the Offline shop extension you'll see an Items list, the Shop wallet and a Wordslist\ - ![offline shop back office](https://i.imgur.com/Ei7cxj9.png) -2. Begin by creating an item, click "ADD NEW ITEM" - - set the item name and a small description - - you can set an optional, preferably square image, that will show up on the customer wallet - _depending on wallet_ - - set the item price, if you choose a fiat currency the bitcoin conversion will happen at the time customer scans to pay\ - ![add new item](https://i.imgur.com/pkZqRgj.png) -3. After creating some products, click on "PRINT QR CODES"\ - ![print qr codes](https://i.imgur.com/2GAiSTe.png) -4. You'll see a QR code for each product in your LNbits Offline Shop with a title and price ready for printing\ - ![qr codes sheet](https://i.imgur.com/faEqOcd.png) -5. Place the printed QR codes on your shop, or at the fair stall, or have them as a menu style laminated sheet -6. Choose what type of confirmation do you want customers to report to merchant after a successful payment\ - ![wordlist](https://i.imgur.com/9aM6NUL.png) - - - Wordlist is the default option: after a successful payment the customer will receive a word from this list, **sequentially**. Starting in _albatross_ as customers pay for the items they will get the next word in the list until _zebra_, then it starts at the top again. The list can be changed, for example if you think A-Z is a big list to track, you can use _apple_, _banana_, _coconut_\ - ![totp authenticator](https://i.imgur.com/MrJXFxz.png) - - TOTP (time-based one time password) can be used instead. If you use Google Authenticator just scan the presented QR with the app and after a successful payment the user will get the password that you can check with GA\ - ![disable confirmations](https://i.imgur.com/2OFs4yi.png) - - Nothing, disables the need for confirmation of payment, click the "DISABLE CONFIRMATION CODES" diff --git a/lnbits/extensions/offlineshop/__init__.py b/lnbits/extensions/offlineshop/__init__.py deleted file mode 100644 index 72d1ae6b..00000000 --- a/lnbits/extensions/offlineshop/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from fastapi import APIRouter -from fastapi.staticfiles import StaticFiles - -from lnbits.db import Database -from lnbits.helpers import template_renderer - -db = Database("ext_offlineshop") - -offlineshop_static_files = [ - { - "path": "/offlineshop/static", - "app": StaticFiles(packages=[("lnbits", "extensions/offlineshop/static")]), - "name": "offlineshop_static", - } -] - -offlineshop_ext: APIRouter = APIRouter(prefix="/offlineshop", tags=["Offlineshop"]) - - -def offlineshop_renderer(): - return template_renderer(["lnbits/extensions/offlineshop/templates"]) - - -from .lnurl import * # noqa: F401,F403 -from .views import * # noqa: F401,F403 -from .views_api import * # noqa: F401,F403 diff --git a/lnbits/extensions/offlineshop/config.json b/lnbits/extensions/offlineshop/config.json deleted file mode 100644 index 94dcd478..00000000 --- a/lnbits/extensions/offlineshop/config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "OfflineShop", - "short_description": "Receive payments for products offline!", - "tile": "/offlineshop/static/image/offlineshop.png", - "contributors": [ - "fiatjaf" - ] -} diff --git a/lnbits/extensions/offlineshop/crud.py b/lnbits/extensions/offlineshop/crud.py deleted file mode 100644 index 1fa63f3e..00000000 --- a/lnbits/extensions/offlineshop/crud.py +++ /dev/null @@ -1,117 +0,0 @@ -from typing import List, Optional - -from lnbits.db import SQLITE - -from . import db -from .models import Item, Shop -from .wordlists import animals - - -async def create_shop(*, wallet_id: str) -> int: - returning = "" if db.type == SQLITE else "RETURNING ID" - method = db.execute if db.type == SQLITE else db.fetchone - - result = await (method)( - f""" - INSERT INTO offlineshop.shops (wallet, wordlist, method) - VALUES (?, ?, 'wordlist') - {returning} - """, - (wallet_id, "\n".join(animals)), - ) - if db.type == SQLITE: - return result._result_proxy.lastrowid - else: - return result[0] # type: ignore - - -async def get_shop(id: int) -> Optional[Shop]: - row = await db.fetchone("SELECT * FROM offlineshop.shops WHERE id = ?", (id,)) - return Shop(**row) if row else None - - -async def get_or_create_shop_by_wallet(wallet: str) -> Optional[Shop]: - row = await db.fetchone( - "SELECT * FROM offlineshop.shops WHERE wallet = ?", (wallet,) - ) - - if not row: - # create on the fly - ls_id = await create_shop(wallet_id=wallet) - return await get_shop(ls_id) - - return Shop(**row) if row else None - - -async def set_method(shop: int, method: str, wordlist: str = "") -> Optional[Shop]: - await db.execute( - "UPDATE offlineshop.shops SET method = ?, wordlist = ? WHERE id = ?", - (method, wordlist, shop), - ) - return await get_shop(shop) - - -async def add_item( - shop: int, - name: str, - description: str, - image: Optional[str], - price: int, - unit: str, - fiat_base_multiplier: int, -) -> int: - result = await db.execute( - """ - INSERT INTO offlineshop.items (shop, name, description, image, price, unit, fiat_base_multiplier) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, - (shop, name, description, image, price, unit, fiat_base_multiplier), - ) - return result._result_proxy.lastrowid - - -async def update_item( - shop: int, - item_id: int, - name: str, - description: str, - image: Optional[str], - price: int, - unit: str, - fiat_base_multiplier: int, -) -> int: - await db.execute( - """ - UPDATE offlineshop.items SET - name = ?, - description = ?, - image = ?, - price = ?, - unit = ?, - fiat_base_multiplier = ? - WHERE shop = ? AND id = ? - """, - (name, description, image, price, unit, fiat_base_multiplier, shop, item_id), - ) - return item_id - - -async def get_item(id: int) -> Optional[Item]: - row = await db.fetchone( - "SELECT * FROM offlineshop.items WHERE id = ? LIMIT 1", (id,) - ) - return Item.from_row(row) if row else None - - -async def get_items(shop: int) -> List[Item]: - rows = await db.fetchall("SELECT * FROM offlineshop.items WHERE shop = ?", (shop,)) - return [Item.from_row(row) for row in rows] - - -async def delete_item_from_shop(shop: int, item_id: int): - await db.execute( - """ - DELETE FROM offlineshop.items WHERE shop = ? AND id = ? - """, - (shop, item_id), - ) diff --git a/lnbits/extensions/offlineshop/helpers.py b/lnbits/extensions/offlineshop/helpers.py deleted file mode 100644 index 86a653aa..00000000 --- a/lnbits/extensions/offlineshop/helpers.py +++ /dev/null @@ -1,17 +0,0 @@ -import base64 -import hmac -import struct -import time - - -def hotp(key, counter, digits=6, digest="sha1"): - key = base64.b32decode(key.upper() + "=" * ((8 - len(key)) % 8)) - counter = struct.pack(">Q", counter) - mac = hmac.new(key, counter, digest).digest() - offset = mac[-1] & 0x0F - binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7FFFFFFF - return str(binary)[-digits:].zfill(digits) - - -def totp(key, time_step=30, digits=6, digest="sha1"): - return hotp(key, int(time.time() / time_step), digits, digest) diff --git a/lnbits/extensions/offlineshop/lnurl.py b/lnbits/extensions/offlineshop/lnurl.py deleted file mode 100644 index ca4e6bac..00000000 --- a/lnbits/extensions/offlineshop/lnurl.py +++ /dev/null @@ -1,88 +0,0 @@ -from fastapi import Query -from lnurl import LnurlErrorResponse, LnurlPayActionResponse, LnurlPayResponse -from lnurl.models import ClearnetUrl, LightningInvoice, MilliSatoshi -from starlette.requests import Request - -from lnbits.core.services import create_invoice -from lnbits.utils.exchange_rates import fiat_amount_as_satoshis - -from . import offlineshop_ext -from .crud import get_item, get_shop - - -@offlineshop_ext.get("/lnurl/{item_id}", name="offlineshop.lnurl_response") -async def lnurl_response(req: Request, item_id: int = Query(...)) -> dict: - item = await get_item(item_id) - if not item: - return {"status": "ERROR", "reason": "Item not found."} - - if not item.enabled: - return {"status": "ERROR", "reason": "Item disabled."} - - price_msat = ( - await fiat_amount_as_satoshis(item.price, item.unit) - if item.unit != "sat" - else item.price - ) * 1000 - - resp = LnurlPayResponse( - callback=ClearnetUrl( - req.url_for("offlineshop.lnurl_callback", item_id=item.id), scheme="https" - ), - minSendable=MilliSatoshi(price_msat), - maxSendable=MilliSatoshi(price_msat), - metadata=await item.lnurlpay_metadata(), - ) - - return resp.dict() - - -@offlineshop_ext.get("/lnurl/cb/{item_id}", name="offlineshop.lnurl_callback") -async def lnurl_callback(request: Request, item_id: int): - item = await get_item(item_id) - if not item: - return {"status": "ERROR", "reason": "Couldn't find item."} - - if item.unit == "sat": - min = item.price * 1000 - max = item.price * 1000 - else: - price = await fiat_amount_as_satoshis(item.price, item.unit) - # allow some fluctuation (the fiat price may have changed between the calls) - min = price * 995 - max = price * 1010 - - amount_received = int(request.query_params.get("amount") or 0) - if amount_received < min: - return LnurlErrorResponse( - reason=f"Amount {amount_received} is smaller than minimum {min}." - ).dict() - elif amount_received > max: - return LnurlErrorResponse( - reason=f"Amount {amount_received} is greater than maximum {max}." - ).dict() - - shop = await get_shop(item.shop) - assert shop - - try: - payment_hash, payment_request = await create_invoice( - wallet_id=shop.wallet, - amount=int(amount_received / 1000), - memo=item.name, - unhashed_description=(await item.lnurlpay_metadata()).encode(), - extra={"tag": "offlineshop", "item": item.id}, - ) - except Exception as exc: - return LnurlErrorResponse(reason=str(exc)).dict() - - if shop.method: - success_action = item.success_action(shop, payment_hash, request) - assert success_action - resp = LnurlPayActionResponse( - pr=LightningInvoice(payment_request), - successAction=success_action, - routes=[], - ) - - return resp.dict() diff --git a/lnbits/extensions/offlineshop/migrations.py b/lnbits/extensions/offlineshop/migrations.py deleted file mode 100644 index 4e668668..00000000 --- a/lnbits/extensions/offlineshop/migrations.py +++ /dev/null @@ -1,39 +0,0 @@ -async def m001_initial(db): - """ - Initial offlineshop tables. - """ - await db.execute( - f""" - CREATE TABLE offlineshop.shops ( - id {db.serial_primary_key}, - wallet TEXT NOT NULL, - method TEXT NOT NULL, - wordlist TEXT - ); - """ - ) - - await db.execute( - f""" - CREATE TABLE offlineshop.items ( - shop INTEGER NOT NULL REFERENCES {db.references_schema}shops (id), - id {db.serial_primary_key}, - name TEXT NOT NULL, - description TEXT NOT NULL, - image TEXT, -- image/png;base64,... - enabled BOOLEAN NOT NULL DEFAULT true, - price {db.big_int} NOT NULL, - unit TEXT NOT NULL DEFAULT 'sat' - ); - """ - ) - - -async def m002_fiat_base_multiplier(db): - """ - Store the multiplier for fiat prices. We store the price in cents and - remember to multiply by 100 when we use it to convert to Dollars. - """ - await db.execute( - "ALTER TABLE offlineshop.items ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;" - ) diff --git a/lnbits/extensions/offlineshop/models.py b/lnbits/extensions/offlineshop/models.py deleted file mode 100644 index 01044cb0..00000000 --- a/lnbits/extensions/offlineshop/models.py +++ /dev/null @@ -1,138 +0,0 @@ -import base64 -import hashlib -import json -from collections import OrderedDict -from sqlite3 import Row -from typing import Dict, List, Optional - -from lnurl import encode as lnurl_encode -from lnurl.models import ClearnetUrl, Max144Str, UrlAction -from lnurl.types import LnurlPayMetadata -from pydantic import BaseModel -from starlette.requests import Request - -from .helpers import totp - -shop_counters: Dict = {} - - -class ShopCounter: - wordlist: List[str] - fulfilled_payments: OrderedDict - counter: int - - @classmethod - def invoke(cls, shop: "Shop"): - shop_counter = shop_counters.get(shop.id) - if not shop_counter: - shop_counter = cls(wordlist=shop.wordlist.split("\n")) - shop_counters[shop.id] = shop_counter - return shop_counter - - @classmethod - def reset(cls, shop: "Shop"): - shop_counter = cls.invoke(shop) - shop_counter.counter = -1 - shop_counter.wordlist = shop.wordlist.split("\n") - - def __init__(self, wordlist: List[str]): - self.wordlist = wordlist - self.fulfilled_payments = OrderedDict() - self.counter = -1 - - def get_word(self, payment_hash): - if payment_hash in self.fulfilled_payments: - return self.fulfilled_payments[payment_hash] - - # get a new word - self.counter += 1 - word = self.wordlist[self.counter % len(self.wordlist)] - self.fulfilled_payments[payment_hash] = word - - # cleanup confirmation words cache - to_remove = len(self.fulfilled_payments) - 23 - if to_remove > 0: - for _ in range(to_remove): - self.fulfilled_payments.popitem(False) - - return word - - -class Shop(BaseModel): - id: int - wallet: str - method: str - wordlist: str - - @classmethod - def from_row(cls, row: Row): - return cls(**dict(row)) - - @property - def otp_key(self) -> str: - return base64.b32encode( - hashlib.sha256( - ("otpkey" + str(self.id) + self.wallet).encode("ascii") - ).digest() - ).decode("ascii") - - def get_code(self, payment_hash: str) -> str: - if self.method == "wordlist": - sc = ShopCounter.invoke(self) - return sc.get_word(payment_hash) - elif self.method == "totp": - return totp(self.otp_key) - return "" - - -class Item(BaseModel): - shop: int - id: int - name: str - description: str - image: Optional[str] - enabled: bool - price: float - unit: str - fiat_base_multiplier: int - - @classmethod - def from_row(cls, row: Row) -> "Item": - data = dict(row) - if data["unit"] != "sat" and data["fiat_base_multiplier"]: - data["price"] /= data["fiat_base_multiplier"] - return cls(**data) - - def lnurl(self, req: Request) -> str: - return lnurl_encode(req.url_for("offlineshop.lnurl_response", item_id=self.id)) - - def values(self, req: Request): - values = self.dict() - values["lnurl"] = lnurl_encode( - req.url_for("offlineshop.lnurl_response", item_id=self.id) - ) - return values - - async def lnurlpay_metadata(self) -> LnurlPayMetadata: - metadata = [["text/plain", self.description]] - - if self.image: - metadata.append(self.image.split(":")[1].split(",")) - - return LnurlPayMetadata(json.dumps(metadata)) - - def success_action( - self, shop: Shop, payment_hash: str, req: Request - ) -> Optional[UrlAction]: - if not shop.wordlist: - return None - - return UrlAction( - url=ClearnetUrl( - req.url_for("offlineshop.confirmation_code", p=payment_hash), - scheme="https", - ), - description=Max144Str( - "Open to get the confirmation code for your purchase." - ), - ) diff --git a/lnbits/extensions/offlineshop/static/image/offlineshop.png b/lnbits/extensions/offlineshop/static/image/offlineshop.png deleted file mode 100644 index 24241d4fa366286546766570fd95e021fb63aaa5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13689 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%0DIeEoa6}C!XbFK1^!3Zj%k|2Q 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 diff --git a/lnbits/extensions/offlineshop/static/js/index.js b/lnbits/extensions/offlineshop/static/js/index.js deleted file mode 100644 index 7ade1bb9..00000000 --- a/lnbits/extensions/offlineshop/static/js/index.js +++ /dev/null @@ -1,230 +0,0 @@ -/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ - -Vue.component(VueQrcode.name, VueQrcode) - -const pica = window.pica() - -function imgSizeFit(img, maxWidth = 1024, maxHeight = 768) { - let ratio = Math.min( - 1, - maxWidth / img.naturalWidth, - maxHeight / img.naturalHeight - ) - return {width: img.naturalWidth * ratio, height: img.naturalHeight * ratio} -} - -const defaultItemData = { - unit: 'sat' -} - -new Vue({ - el: '#vue', - mixins: [windowMixin], - data() { - return { - selectedWallet: null, - confirmationMethod: 'wordlist', - wordlistTainted: false, - offlineshop: { - method: null, - wordlist: [], - items: [] - }, - itemDialog: { - show: false, - urlImg: true, - data: {...defaultItemData}, - units: ['sat'] - } - } - }, - computed: { - printItems() { - return this.offlineshop.items.filter(({enabled}) => enabled) - } - }, - methods: { - openNewDialog() { - this.itemDialog.show = true - this.itemDialog.data = {...defaultItemData} - }, - openUpdateDialog(itemId) { - this.itemDialog.show = true - let item = this.offlineshop.items.find(item => item.id === itemId) - if (item.image.startsWith('data:')) { - this.itemDialog.urlImg = false - } - this.itemDialog.data = item - }, - imageAdded(file) { - let blobURL = URL.createObjectURL(file) - let image = new Image() - image.src = blobURL - image.onload = async () => { - let fit = imgSizeFit(image, 100, 100) - let canvas = document.createElement('canvas') - canvas.setAttribute('width', fit.width) - canvas.setAttribute('height', fit.height) - output = await pica.resize(image, canvas) - this.itemDialog.data.image = output.toDataURL('image/jpeg', 0.4) - this.itemDialog = {...this.itemDialog} - } - }, - imageCleared() { - this.itemDialog.data.image = null - this.itemDialog = {...this.itemDialog} - }, - disabledAddItemButton() { - return ( - !this.itemDialog.data.name || - this.itemDialog.data.name.length === 0 || - !this.itemDialog.data.price || - !this.itemDialog.data.description || - !this.itemDialog.data.unit || - this.itemDialog.data.unit.length === 0 - ) - }, - changedWallet(wallet) { - this.selectedWallet = wallet - this.loadShop() - }, - loadShop() { - LNbits.api - .request( - 'GET', - '/offlineshop/api/v1/offlineshop', - this.selectedWallet.inkey - ) - .then(response => { - this.offlineshop = response.data - this.confirmationMethod = response.data.method - this.wordlistTainted = false - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, - async setMethod() { - try { - await LNbits.api.request( - 'PUT', - '/offlineshop/api/v1/offlineshop/method', - this.selectedWallet.inkey, - {method: this.confirmationMethod, wordlist: this.offlineshop.wordlist} - ) - } catch (err) { - LNbits.utils.notifyApiError(err) - return - } - - this.$q.notify({ - message: - `Method set to ${this.confirmationMethod}.` + - (this.confirmationMethod === 'wordlist' ? ' Counter reset.' : ''), - timeout: 700 - }) - this.loadShop() - }, - async sendItem() { - let {id, name, image, description, price, unit} = this.itemDialog.data - const data = { - name, - description, - image, - price, - unit, - fiat_base_multiplier: unit == 'sat' ? 1 : 100 - } - - try { - if (id) { - await LNbits.api.request( - 'PUT', - '/offlineshop/api/v1/offlineshop/items/' + id, - this.selectedWallet.inkey, - data - ) - } else { - await LNbits.api.request( - 'POST', - '/offlineshop/api/v1/offlineshop/items', - this.selectedWallet.inkey, - data - ) - this.$q.notify({ - message: `Item '${this.itemDialog.data.name}' added.`, - timeout: 700 - }) - } - } catch (err) { - LNbits.utils.notifyApiError(err) - return - } - - this.loadShop() - this.itemDialog.show = false - this.itemDialog.urlImg = true - this.itemDialog.data = {...defaultItemData} - }, - toggleItem(itemId) { - let item = this.offlineshop.items.find(item => item.id === itemId) - item.enabled = !item.enabled - - LNbits.api - .request( - 'PUT', - '/offlineshop/api/v1/offlineshop/items/' + itemId, - this.selectedWallet.inkey, - item - ) - .then(response => { - this.$q.notify({ - message: `Item ${item.enabled ? 'enabled' : 'disabled'}.`, - timeout: 700 - }) - this.offlineshop.items = this.offlineshop.items - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, - deleteItem(itemId) { - LNbits.utils - .confirmDialog('Are you sure you want to delete this item?') - .onOk(() => { - LNbits.api - .request( - 'DELETE', - '/offlineshop/api/v1/offlineshop/items/' + itemId, - this.selectedWallet.inkey - ) - .then(response => { - this.$q.notify({ - message: `Item deleted.`, - timeout: 700 - }) - this.offlineshop.items.splice( - this.offlineshop.items.findIndex(item => item.id === itemId), - 1 - ) - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }) - } - }, - created() { - this.selectedWallet = this.g.user.wallets[0] - this.loadShop() - - LNbits.api - .request('GET', '/offlineshop/api/v1/currencies') - .then(response => { - this.itemDialog = {...this.itemDialog, units: ['sat', ...response.data]} - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - } -}) diff --git a/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html b/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html deleted file mode 100644 index 0a4b9df8..00000000 --- a/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html +++ /dev/null @@ -1,154 +0,0 @@ - - - -
    -
  1. Register items.
  2. -
  3. - Print QR codes and paste them on your store, your menu, somewhere, - somehow. -
  4. -
  5. - Clients scan the QR codes and get information about the items plus the - price on their phones directly (they must have internet) -
  6. -
  7. - Once they decide to pay, they'll get an invoice on their phones - automatically -
  8. -
  9. - When the payment is confirmed, a confirmation code will be issued for - them. -
  10. -
-

- The confirmation codes are words from a predefined sequential word list. - Each new payment bumps the words sequence by 1. So you can check the - confirmation codes manually by just looking at them. -

-

- For example, if your wordlist is - [apple, banana, coconut] the first purchase will be - apple, the second banana and so on. When it - gets to the end it starts from the beginning again. -

-

Powered by LNURL-pay.

-
-
-
- - - - - - - POST -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
Returns 201 OK
-
Curl example
- curl -X GET {{ request.base_url - }}offlineshop/api/v1/offlineshop/items -H "Content-Type: - application/json" -H "X-Api-Key: {{ user.wallets[0].inkey }}" -d - '{"name": <string>, "description": <string>, "image": - <data-uri string>, "price": <integer>, "unit": <"sat" - or "USD">}' - -
-
-
- - - - GET -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- {"id": <integer>, "wallet": <string>, "wordlist": - <string>, "items": [{"id": <integer>, "name": - <string>, "description": <string>, "image": - <string>, "enabled": <boolean>, "price": <integer>, - "unit": <string>, "lnurl": <string>}, ...]}< -
Curl example
- curl -X GET {{ request.base_url }}offlineshop/api/v1/offlineshop -H - "X-Api-Key: {{ user.wallets[0].inkey }}" - -
-
-
- - - - PUT -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
Returns 200 OK
-
Curl example
- curl -X GET {{ request.base_url - }}offlineshop/api/v1/offlineshop/items/<item_id> -H - "Content-Type: application/json" -H "X-Api-Key: {{ - user.wallets[0].inkey }}" -d '{"name": <string>, "description": - <string>, "image": <data-uri string>, "price": - <integer>, "unit": <"sat" or "USD">}' - -
-
-
- - - - DELETE -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
Returns 200 OK
-
Curl example
- curl -X GET {{ request.base_url - }}offlineshop/api/v1/offlineshop/items/<item_id> -H "X-Api-Key: - {{ user.wallets[0].inkey }}" - -
-
-
-
diff --git a/lnbits/extensions/offlineshop/templates/offlineshop/index.html b/lnbits/extensions/offlineshop/templates/offlineshop/index.html deleted file mode 100644 index 80a7bbb8..00000000 --- a/lnbits/extensions/offlineshop/templates/offlineshop/index.html +++ /dev/null @@ -1,348 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - -
-
-
Items
-
-
- Add new item -
-
- {% raw %} - - - - - {% endraw %} -
-
- - - -
-
Wallet Shop
-
- - - - - - -
- Print QR Codes -
-
-
- - - - - - - - - - -
-
- -
-
- - Update Wordlist - - Reset -
-
-
- -
-
-
- - - -
-
- - Set TOTP - -
-
-
- -
-

- Setting this option disables the confirmation code message that - appears in the consumer wallet after a purchase is paid for. It's ok - if the consumer is to be trusted when they claim to have paid. -

- - - Disable Confirmation Codes - -
-
-
-
- -
- - -
- {{SITE_TITLE}} OfflineShop extension -
-
- - - {% include "offlineshop/_api_docs.html" %} - -
-
- - - - -
-
Adding a new item
- - - - - -
- Copy LNURL -
- - - - - - - - - - - - -
-
- - {% raw %}{{ itemDialog.data.id ? 'Update' : 'Add' }}{% endraw %} - Item - -
-
- Cancel -
-
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - - -{% endblock %} diff --git a/lnbits/extensions/offlineshop/templates/offlineshop/print.html b/lnbits/extensions/offlineshop/templates/offlineshop/print.html deleted file mode 100644 index a3bf5861..00000000 --- a/lnbits/extensions/offlineshop/templates/offlineshop/print.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "print.html" %} {% block page %} {% raw %} -
-
-
{{ item.name }}
- -
{{ item.price }}
-
-
-{% endraw %} {% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/offlineshop/views.py b/lnbits/extensions/offlineshop/views.py deleted file mode 100644 index ebde1762..00000000 --- a/lnbits/extensions/offlineshop/views.py +++ /dev/null @@ -1,89 +0,0 @@ -import time -from datetime import datetime -from http import HTTPStatus - -from fastapi import Depends, HTTPException, Query, Request -from starlette.responses import HTMLResponse - -from lnbits.core.crud import get_standalone_payment -from lnbits.core.models import User -from lnbits.core.views.api import api_payment -from lnbits.decorators import check_user_exists - -from . import offlineshop_ext, offlineshop_renderer -from .crud import get_item, get_shop - - -@offlineshop_ext.get("/", response_class=HTMLResponse) -async def index(request: Request, user: User = Depends(check_user_exists)): - return offlineshop_renderer().TemplateResponse( - "offlineshop/index.html", {"request": request, "user": user.dict()} - ) - - -@offlineshop_ext.get("/print", response_class=HTMLResponse) -async def print_qr_codes(request: Request): - items = [] - for item_id in request.query_params.get("items").split(","): - item = await get_item(item_id) - if item: - items.append( - { - "lnurl": item.lnurl(request), - "name": item.name, - "price": f"{item.price} {item.unit}", - } - ) - - return offlineshop_renderer().TemplateResponse( - "offlineshop/print.html", {"request": request, "items": items} - ) - - -@offlineshop_ext.get( - "/confirmation/{p}", - name="offlineshop.confirmation_code", - response_class=HTMLResponse, -) -async def confirmation_code(p: str = Query(...)): - style = "" - - payment_hash = p - await api_payment(payment_hash) - - payment = await get_standalone_payment(payment_hash) - if not payment: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, - detail=f"Couldn't find the payment {payment_hash}." + style, - ) - if payment.pending: - raise HTTPException( - status_code=HTTPStatus.PAYMENT_REQUIRED, - detail=f"Payment {payment_hash} wasn't received yet. Please try again in a minute." - + style, - ) - - if payment.time + 60 * 15 < time.time(): - raise HTTPException( - status_code=HTTPStatus.REQUEST_TIMEOUT, - detail="Too much time has passed." + style, - ) - - assert payment.extra - item_id = payment.extra.get("item") - assert item_id - item = await get_item(item_id) - assert item - shop = await get_shop(item.shop) - assert shop - - return ( - f""" -[{shop.get_code(payment_hash)}]
-{item.name}
-{item.price} {item.unit}
-{datetime.utcfromtimestamp(payment.time).strftime('%Y-%m-%d %H:%M:%S')} - """ - + style - ) diff --git a/lnbits/extensions/offlineshop/views_api.py b/lnbits/extensions/offlineshop/views_api.py deleted file mode 100644 index 22dca69b..00000000 --- a/lnbits/extensions/offlineshop/views_api.py +++ /dev/null @@ -1,136 +0,0 @@ -from http import HTTPStatus -from typing import Optional - -from fastapi import Depends, HTTPException, Query, Request, Response -from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl -from pydantic import BaseModel - -from lnbits.decorators import WalletTypeInfo, get_key_type -from lnbits.utils.exchange_rates import currencies - -from . import offlineshop_ext -from .crud import ( - add_item, - delete_item_from_shop, - get_items, - get_or_create_shop_by_wallet, - set_method, - update_item, -) -from .models import ShopCounter - - -@offlineshop_ext.get("/api/v1/currencies") -async def api_list_currencies_available(): - return list(currencies.keys()) - - -@offlineshop_ext.get("/api/v1/offlineshop") -async def api_shop_from_wallet( - r: Request, wallet: WalletTypeInfo = Depends(get_key_type) -): - shop = await get_or_create_shop_by_wallet(wallet.wallet.id) - assert shop - items = await get_items(shop.id) - try: - return { - **shop.dict(), - **{"otp_key": shop.otp_key, "items": [item.values(r) for item in items]}, - } - except LnurlInvalidUrl: - raise HTTPException( - status_code=HTTPStatus.UPGRADE_REQUIRED, - detail="LNURLs need to be delivered over a publically accessible `https` domain or Tor.", - ) - - -class CreateItemsData(BaseModel): - name: str - description: str - image: Optional[str] - price: float - unit: str - fiat_base_multiplier: int = Query(100, ge=1) - - -@offlineshop_ext.post("/api/v1/offlineshop/items") -@offlineshop_ext.put("/api/v1/offlineshop/items/{item_id}") -async def api_add_or_update_item( - data: CreateItemsData, item_id=None, wallet: WalletTypeInfo = Depends(get_key_type) -): - shop = await get_or_create_shop_by_wallet(wallet.wallet.id) - assert shop - if data.image: - image_is_url = data.image.startswith("https://") or data.image.startswith( - "http://" - ) - - if not image_is_url: - - def size(b64string): - return int((len(b64string) * 3) / 4 - b64string.count("=", -2)) - - image_size = size(data.image) / 1024 - if image_size > 100: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail=f"Image size is too big, {int(image_size)}Kb. Max: 100kb, Compress the image at https://tinypng.com, or use an URL.", - ) - if data.unit != "sat": - data.price = data.price * 100 - if item_id is None: - - await add_item( - shop.id, - data.name, - data.description, - data.image, - int(data.price), - data.unit, - data.fiat_base_multiplier, - ) - return Response(status_code=HTTPStatus.CREATED) - else: - await update_item( - shop.id, - item_id, - data.name, - data.description, - data.image, - int(data.price), - data.unit, - data.fiat_base_multiplier, - ) - - -@offlineshop_ext.delete("/api/v1/offlineshop/items/{item_id}") -async def api_delete_item(item_id, wallet: WalletTypeInfo = Depends(get_key_type)): - shop = await get_or_create_shop_by_wallet(wallet.wallet.id) - assert shop - await delete_item_from_shop(shop.id, item_id) - return "", HTTPStatus.NO_CONTENT - - -class CreateMethodData(BaseModel): - method: str - wordlist: Optional[str] - - -@offlineshop_ext.put("/api/v1/offlineshop/method") -async def api_set_method( - data: CreateMethodData, wallet: WalletTypeInfo = Depends(get_key_type) -): - method = data.method - - wordlist = data.wordlist.split("\n") if data.wordlist else [] - wordlist = [word.strip() for word in wordlist if word.strip()] - - shop = await get_or_create_shop_by_wallet(wallet.wallet.id) - if not shop: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND) - - updated_shop = await set_method(shop.id, method, "\n".join(wordlist)) - if not updated_shop: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND) - - ShopCounter.reset(updated_shop) diff --git a/lnbits/extensions/offlineshop/wordlists.py b/lnbits/extensions/offlineshop/wordlists.py deleted file mode 100644 index fa0e574d..00000000 --- a/lnbits/extensions/offlineshop/wordlists.py +++ /dev/null @@ -1,28 +0,0 @@ -animals = [ - "albatross", - "bison", - "chicken", - "duck", - "eagle", - "flamingo", - "gorilla", - "hamster", - "iguana", - "jaguar", - "koala", - "llama", - "macaroni penguin", - "numbat", - "octopus", - "platypus", - "quetzal", - "rabbit", - "salmon", - "tuna", - "unicorn", - "vulture", - "wolf", - "xenops", - "yak", - "zebra", -]