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 7b9c6c8db..000000000 --- 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 72d1ae6b3..000000000 --- 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 94dcd4783..000000000 --- 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 1fa63f3e0..000000000 --- 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 86a653aa2..000000000 --- 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 ca4e6bac5..000000000 --- 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 4e668668a..000000000 --- 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 01044cb0f..000000000 --- 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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 diff --git a/lnbits/extensions/offlineshop/static/js/index.js b/lnbits/extensions/offlineshop/static/js/index.js deleted file mode 100644 index 7ade1bb9a..000000000 --- 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 0a4b9df8c..000000000 --- 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 80a7bbb8b..000000000 --- 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 a3bf5861b..000000000 --- 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 ebde17629..000000000 --- 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 22dca69b1..000000000 --- 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 fa0e574d2..000000000 --- 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", -]