-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from ocknamo/feature/pos
Feature/pos
- Loading branch information
Showing
6 changed files
with
395 additions
and
7 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -113,7 +113,7 @@ | |
</svg> | ||
</button> | ||
<input class="currency-input" type="text" id="sats" aria-label="サッツの金額" | ||
oninput="calculateValues('sats')" pattern="[0-9]+([\.,][0-9]+)?" inputmode="decimal"> | ||
oninput="window.satsRate.calculateValues('sats')" pattern="[0-9]+([\.,][0-9]+)?" inputmode="decimal"> | ||
<button class="currency-units" id="paste-sats" data-currency="sats" aria-label="サッツの数値をペースト"> | ||
sats | ||
</button> | ||
|
@@ -124,7 +124,7 @@ | |
</svg> | ||
</button> | ||
<input class="currency-input" type="text" id="btc" aria-label="ビットコインの金額" | ||
oninput="calculateValues('btc')" pattern="[0-9]+([\.,][0-9]+)?" inputmode="decimal"> | ||
oninput="window.satsRate.calculateValues('btc')" pattern="[0-9]+([\.,][0-9]+)?" inputmode="decimal"> | ||
<button class="currency-units" id="paste-btc" data-currency="btc" aria-label="ビットコインの数値をペースト"> | ||
BTC | ||
</button> | ||
|
@@ -135,7 +135,7 @@ | |
</svg> | ||
</button> | ||
<input class="currency-input" type="text" id="jpy" aria-label="日本円の金額" | ||
oninput="calculateValues('jpy')" pattern="[0-9]+([\.,][0-9]+)?" inputmode="decimal"> | ||
oninput="window.satsRate.calculateValues('jpy')" pattern="[0-9]+([\.,][0-9]+)?" inputmode="decimal"> | ||
<button class="currency-units" id="paste-jpy" aria-label="円の数値をペースト"> | ||
JPY | ||
</button> | ||
|
@@ -146,7 +146,7 @@ | |
</svg> | ||
</button> | ||
<input class="currency-input" type="text" id="usd" aria-label="アメリカドルの金額" | ||
oninput="calculateValues('usd')" pattern="[0-9]+([\.,][0-9]+)?" inputmode="decimal"> | ||
oninput="window.satsRate.calculateValues('usd')" pattern="[0-9]+([\.,][0-9]+)?" inputmode="decimal"> | ||
<button class="currency-units" id="paste-usd" data-currency="usd" aria-label="USドルの数値をペースト"> | ||
USD | ||
</button> | ||
|
@@ -157,7 +157,7 @@ | |
</svg> | ||
</button> | ||
<input class="currency-input" type="text" id="eur" aria-label="ユーロの金額" | ||
oninput="calculateValues('eur')" pattern="[0-9]+([\.,][0-9]+)?" inputmode="decimal"> | ||
oninput="window.satsRate.calculateValues('eur')" pattern="[0-9]+([\.,][0-9]+)?" inputmode="decimal"> | ||
<button class="currency-units" id="paste-eur" data-currency="eur" aria-label="ユーロの数値をペースト"> | ||
EUR | ||
</button> | ||
|
@@ -174,6 +174,48 @@ | |
</div> | ||
</div> | ||
|
||
<div class="bgcolor pos-wrapper"> | ||
<div class="subject"> | ||
POS | ||
</div> | ||
<div class="pos"> | ||
<button id="pos-pay-button">⚡請求書を表示</button> | ||
<br /> | ||
<dialog id="update-lightning-address-dialog"> | ||
<form id="lightning-address-form"> | ||
<label class="lightning-address-label" for="lightning-address">受取用ライトニングアドレス | ||
<input | ||
type="text" | ||
name="lightning-address" | ||
id="lightning-address-input" | ||
class="lightning-address-input" | ||
pattern="^[a-z0-9._%+\-]+@[a-z0-9\.\-]+\.[a-z]{2,3}$" | ||
maxlength="200" | ||
minlength="4" | ||
required | ||
title="ライトニングアドレスの形式で入力してください" | ||
/> | ||
<div> | ||
<button id="lightning-address-submit-button" type="submit" formmethod="dialog">設定する</button> | ||
<button id="lightning-address-clear-button" value="clear" formmethod="dialog">削除</button> | ||
<button id="lightning-address-close-button" value="cancel" formmethod="dialog">閉じる</button> | ||
</div> | ||
</label> | ||
</form> | ||
</dialog> | ||
<p class="lightning-address"> | ||
<output id="lightning-address-output"></output> | ||
<button class="setting-btn" id="show-lightning-address-dialog" aria-label="受取ライトニングアドレスの設定"> | ||
<svg width="24px" height="24px"> | ||
<use xlink:href="./images/settings-solid.svg#a"></use> | ||
</svg> | ||
</button> | ||
</p> | ||
<div id="lightning-pos-qr-box"></div> | ||
<div id="pos-message"></div> | ||
</div> | ||
</div> | ||
|
||
<div class="bgcolor"> | ||
<div class="subject"> | ||
SNSに計算結果を共有 | ||
|
@@ -337,7 +379,8 @@ | |
</div> | ||
|
||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script> | ||
<script src="./main.js"></script> | ||
<script src="https://unpkg.com/[email protected]/lib/qr-code-styling.js"></script> | ||
<script type="module" src="./main.js"></script> | ||
</body> | ||
|
||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
// ライトニングアドレスの正規表現(メールの正規表現を流用) | ||
const addressRegExp = /^[A-Za-z0-9]{1}[A-Za-z0-9_.-]*@{1}[A-Za-z0-9_.-]{1,}\.[A-Za-z0-9]{1,}$/ | ||
|
||
export class LightningAddress { | ||
addressView = window.document.getElementById('lightning-address-output'); | ||
addressInput = window.document.getElementById('lightning-address-input'); | ||
|
||
#domain = ''; | ||
#userName = ''; | ||
#LightningAddressStr = ''; | ||
|
||
data = {}; | ||
|
||
constructor(lightningAddressString) { | ||
this.#LightningAddressStr = addressRegExp.test(lightningAddressString) ? lightningAddressString : ''; | ||
const [userName, domain] = this.#LightningAddressStr.split('@'); | ||
this.#domain = domain; | ||
this.#userName = userName; | ||
|
||
this.#updateView() | ||
} | ||
|
||
async fetchAddressData() { | ||
if(!this.#domain || !this.#userName) { | ||
return; | ||
} | ||
|
||
// Document: https://github.com/lnurl/luds/blob/luds/16.md | ||
this.data = await fetch(`https://${this.#domain}/.well-known/lnurlp/${this.#userName}`).then(async (res) => { | ||
return res.json(); | ||
}) | ||
|
||
return this.data; | ||
} | ||
|
||
/** | ||
* 入力値の数量でインボイスを生成する | ||
* @param {number} amount ミリサトシ | ||
* @returns void | ||
*/ | ||
async getInvoice(amount) { | ||
if(!this.data || this.data.tag !== 'payRequest' || !this.data.callback) { | ||
throw new Error('支払い可能なライトニングアドレスではないようです') | ||
} | ||
|
||
if(this.data.status === 'ERROR') { | ||
// 仕様上this.data.reasonが存在するはずなのでエラー理由をスローする | ||
// https://github.com/lnurl/luds/blob/luds/06.md | ||
if(this.data.reason) { | ||
throw new Error(this.data.reason) | ||
} else { | ||
throw new Error(`Invalid lnurlp response: ${JSON.stringify(this.data)}`) | ||
} | ||
} | ||
|
||
// 数量のバリデーション | ||
if( amount > this.data.maxSendable && amount < (this.data.minSendable ?? 0)) { | ||
return; | ||
} | ||
|
||
const callbackUrl = new URL(this.data.callback) | ||
callbackUrl.searchParams.append('amount', amount); | ||
|
||
return fetch(callbackUrl).then(async (res) => { | ||
return res.json(); | ||
}); | ||
} | ||
|
||
toString() { | ||
return this.#LightningAddressStr; | ||
} | ||
|
||
hasValidAddress() { | ||
return !!this.#LightningAddressStr; | ||
} | ||
|
||
#updateView() { | ||
this.addressView.value = this.#LightningAddressStr || 'ライトニングアドレス未設定' | ||
this.addressInput.value = this.#LightningAddressStr || ''; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import { LightningAddress } from './lightning-address.js'; | ||
|
||
/** | ||
* POS機能 | ||
* ライトニングアドレスを保管して入力された金額のインボイスのQRコードの表示を行う。 | ||
* またインプットの値とインボイスの値が乖離することを避けるため入力値を監視し変更された場合は直ちに破棄する。 | ||
*/ | ||
export class Pos { | ||
localStorageKey = 'POS:LnAddress'; | ||
#getQrCodeConfig(data) { | ||
return { | ||
width: 340, | ||
height: 340, | ||
type: 'svg', | ||
data, | ||
image: './images/icon_x192.png', | ||
margin: 2, | ||
dotsOptions: { | ||
color: '#4D4D4D', | ||
type: 'rounded' | ||
}, | ||
backgroundOptions: { | ||
color: '#e9ebee', | ||
}, | ||
imageOptions: { | ||
crossOrigin: 'anonymous', | ||
margin: 2 | ||
} | ||
} | ||
}; | ||
|
||
// LightningAddressクラスのインスタンス | ||
#lnAddress; | ||
|
||
#qrWrapper = window.document.getElementById('lightning-pos-qr-box'); | ||
#satsInput = window.document.getElementById('sats'); | ||
#messageArea = window.document.getElementById('pos-message'); | ||
#otherUnits = ['btc', 'jpy', 'usd', 'eur'] | ||
|
||
// 連打による連続的なリクエストを制限するためのフラグ | ||
#isRequesting = false; | ||
|
||
initialize() { | ||
// ローカルストレージからアドレスを取得 | ||
this.#lnAddress = new LightningAddress(window.localStorage.getItem(this.localStorageKey) ?? ''); | ||
|
||
// 値が変更されたら請求書QRコードは削除しなければいけないため入力値を監視する | ||
this.#handleInputChange(); | ||
} | ||
|
||
setLnAddress(form) { | ||
const formData = new FormData(form); | ||
|
||
this.#lnAddress = new LightningAddress(formData.get('lightning-address') ?? ''); | ||
|
||
if(!this.#lnAddress.hasValidAddress) { | ||
console.warn(`Pos: invalid address: ${formData.get('lightning-address')}`); | ||
|
||
return; | ||
} | ||
|
||
window.localStorage.setItem(this.localStorageKey, this.#lnAddress.toString()); | ||
} | ||
|
||
getCurrntSatsAmount() { | ||
const satsAmount = this.#satsInput.value.replaceAll(',', ''); | ||
|
||
return satsAmount || null; | ||
} | ||
|
||
// 支払いボタン押下時 | ||
async generateInvoice() { | ||
if(this.#isRequesting) { | ||
return; | ||
} | ||
|
||
this.#isRequesting = true; | ||
|
||
if(!this.#lnAddress.hasValidAddress()) { | ||
return; | ||
} | ||
|
||
const amount = this.getCurrntSatsAmount(); | ||
if(!amount) { | ||
return; | ||
} | ||
|
||
this.#clearQrCode(); | ||
|
||
try { | ||
await this.#lnAddress.fetchAddressData(); | ||
const invoice = await this.#lnAddress.getInvoice(amount * 1000); | ||
this.#showQrCode(invoice.pr); | ||
} catch (error) { | ||
this.#setMessage(error.message ?? JSON.stringify(error)); | ||
} finally { | ||
this.#isRequesting = false; | ||
} | ||
} | ||
|
||
clearLnAddress() { | ||
this.#lnAddress = new LightningAddress(''); | ||
window.localStorage.clear(this.localStorageKey); | ||
this.#clearQrCode(); | ||
} | ||
|
||
#clearQrCode() { | ||
this.#qrWrapper.innerHTML = ''; | ||
} | ||
|
||
#showQrCode(data) { | ||
const qrCode = new QRCodeStyling(this.#getQrCodeConfig(data)) | ||
qrCode.append(this.#qrWrapper); | ||
} | ||
|
||
// どの通貨が変更された場合もQRコードをクリアする | ||
#handleInputChange() { | ||
let inputs = this.#otherUnits.map(u => window.document.getElementById(u)) | ||
inputs = [ ...inputs, this.#satsInput ]; | ||
|
||
inputs.forEach(input => { | ||
input.addEventListener('input', () => { | ||
this.#clearQrCode(); | ||
}); | ||
}); | ||
} | ||
|
||
// エラーメッセージなどを表示する | ||
#setMessage(message) { | ||
this.#messageArea.innerHTML = ''; | ||
const p = window.document.createElement('p') | ||
p.innerText = message; | ||
|
||
this.#messageArea.appendChild(p) | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.