Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
A
aisbf
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nexlab
aisbf
Commits
13b80367
Commit
13b80367
authored
Apr 22, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Payment systems...
parent
946dfd79
Changes
8
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
371 additions
and
230 deletions
+371
-230
database.py
aisbf/database.py
+17
-14
monitor.py
aisbf/payments/crypto/monitor.py
+212
-121
service.py
aisbf/payments/service.py
+15
-19
main.py
main.py
+45
-24
docs.html
templates/dashboard/docs.html
+6
-0
profile.html
templates/dashboard/profile.html
+32
-4
prompts.html
templates/dashboard/prompts.html
+5
-34
wallet.html
templates/dashboard/wallet.html
+39
-14
No files found.
aisbf/database.py
View file @
13b80367
...
...
@@ -1375,7 +1375,7 @@ class DatabaseManager:
'''
,
(
new_email
,
user_id
))
conn
.
commit
()
def
update_user_profile
(
self
,
user_id
:
int
,
username
:
str
,
email
:
str
,
display_name
:
str
=
None
):
def
update_user_profile
(
self
,
user_id
:
int
,
username
:
str
,
email
:
str
,
display_name
:
str
=
None
,
profile_pic
:
str
=
None
):
"""
Update user profile (username and display_name, email is read-only).
...
...
@@ -1384,23 +1384,25 @@ class DatabaseManager:
username: New username
email: Email (ignored, kept for backward compatibility)
display_name: New display name (optional)
profile_pic: Base64-encoded profile picture data URL (optional)
"""
with
self
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
placeholder
=
'?'
if
self
.
db_type
==
'sqlite'
else
'
%
s'
fields
=
[
'username = '
+
placeholder
]
params
=
[
username
]
if
display_name
is
not
None
:
cursor
.
execute
(
f
'''
UPDATE users
SET username = {placeholder}, display_name = {placeholder}
WHERE id = {placeholder}
'''
,
(
username
,
display_name
,
user_id
))
else
:
cursor
.
execute
(
f
'''
UPDATE users
SET username = {placeholder}
WHERE id = {placeholder}
'''
,
(
username
,
user_id
))
fields
.
append
(
'display_name = '
+
placeholder
)
params
.
append
(
display_name
)
if
profile_pic
is
not
None
:
fields
.
append
(
'profile_pic = '
+
placeholder
)
params
.
append
(
profile_pic
)
params
.
append
(
user_id
)
cursor
.
execute
(
f
'''
UPDATE users SET {', '.join(fields)} WHERE id = {placeholder}
'''
,
tuple
(
params
))
conn
.
commit
()
def
sanitize_username
(
self
,
input_str
:
str
)
->
str
:
...
...
@@ -4315,7 +4317,8 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
(
'subscription_expires'
,
'TIMESTAMP NULL'
,
'tier_id'
),
(
'stripe_customer_id'
,
'VARCHAR(100)'
,
'subscription_expires'
),
(
'reset_password_token'
,
'VARCHAR(255)'
,
'stripe_customer_id'
),
(
'reset_password_token_expires'
,
'TIMESTAMP NULL'
,
'reset_password_token'
)
(
'reset_password_token_expires'
,
'TIMESTAMP NULL'
,
'reset_password_token'
),
(
'profile_pic'
,
'TEXT'
,
'reset_password_token_expires'
)
]
for
col_name
,
col_def
,
after_col
in
required_columns
:
...
...
aisbf/payments/crypto/monitor.py
View file @
13b80367
This diff is collapsed.
Click to expand it.
aisbf/payments/service.py
View file @
13b80367
...
...
@@ -55,25 +55,21 @@ class PaymentService:
async
def
get_user_crypto_addresses
(
self
,
user_id
:
int
)
->
dict
:
"""Get or create crypto addresses for user"""
addresses
=
{}
# Get enabled crypto types
with
self
.
db
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
placeholder
=
'?'
if
self
.
db
.
db_type
==
'sqlite'
else
'
%
s'
cursor
.
execute
(
f
"""
SELECT crypto_type FROM crypto_consolidation_settings
WHERE enabled = {placeholder}
"""
,
(
True
,))
enabled_cryptos
=
cursor
.
fetchall
()
for
crypto_config
in
enabled_cryptos
:
crypto_type
=
crypto_config
[
0
]
address
=
await
self
.
wallet_manager
.
get_or_create_user_address
(
user_id
,
crypto_type
)
addresses
[
crypto_type
]
=
address
# Get enabled crypto gateways from payment gateway settings
gateways
=
self
.
db
.
get_payment_gateway_settings
()
crypto_types
=
{
'bitcoin'
:
'btc'
,
'ethereum'
:
'eth'
,
'usdt'
:
'usdt'
,
'usdc'
:
'usdc'
}
for
gateway_name
,
crypto_type
in
crypto_types
.
items
():
gw
=
gateways
.
get
(
gateway_name
,
{})
if
not
gw
.
get
(
'enabled'
,
False
):
continue
try
:
address
=
await
self
.
wallet_manager
.
get_or_create_user_address
(
user_id
,
crypto_type
)
addresses
[
gateway_name
]
=
address
except
Exception
as
e
:
logger
.
warning
(
f
"Could not get/create {crypto_type} address for user {user_id}: {e}"
)
return
addresses
async
def
get_user_wallet_balances
(
self
,
user_id
:
int
)
->
dict
:
...
...
main.py
View file @
13b80367
...
...
@@ -59,6 +59,7 @@ from datetime import datetime, timedelta
from
collections
import
defaultdict
from
pathlib
import
Path
import
json
import
re
import
markdown
from
urllib.parse
import
urljoin
,
urlencode
from
cryptography.fernet
import
Fernet
...
...
@@ -3198,7 +3199,7 @@ async def dashboard_profile(request: Request):
@
app
.
post
(
"/dashboard/profile"
)
async
def
dashboard_profile_save
(
request
:
Request
,
username
:
str
=
Form
(
...
),
display_name
:
str
=
Form
(
""
)):
async
def
dashboard_profile_save
(
request
:
Request
,
username
:
str
=
Form
(
...
),
display_name
:
str
=
Form
(
""
)
,
profile_pic
:
UploadFile
=
File
(
None
)
):
"""Save user profile changes (username and display_name)"""
auth_check
=
require_dashboard_auth
(
request
)
if
isinstance
(
auth_check
,
RedirectResponse
):
...
...
@@ -3208,7 +3209,18 @@ async def dashboard_profile_save(request: Request, username: str = Form(...), di
db
=
DatabaseRegistry
.
get_config_database
()
try
:
db
.
update_user_profile
(
user_id
,
username
,
None
,
display_name
if
display_name
else
None
)
profile_pic_data
=
None
if
profile_pic
and
profile_pic
.
filename
:
content_type
=
profile_pic
.
content_type
or
''
if
not
content_type
.
startswith
(
'image/'
):
return
RedirectResponse
(
url
=
url_for
(
request
,
"/dashboard/profile?error=Invalid file type. Please upload an image."
),
status_code
=
303
)
data
=
await
profile_pic
.
read
(
1024
*
1024
+
1
)
# read up to 1MB+1 to detect oversized
if
len
(
data
)
>
1024
*
1024
:
return
RedirectResponse
(
url
=
url_for
(
request
,
"/dashboard/profile?error=Image too large. Maximum size is 1MB."
),
status_code
=
303
)
import
base64
profile_pic_data
=
f
"data:{content_type};base64,{base64.b64encode(data).decode()}"
db
.
update_user_profile
(
user_id
,
username
,
None
,
display_name
if
display_name
else
None
,
profile_pic_data
)
# Update session with new username and display_name
request
.
session
[
'username'
]
=
username
request
.
session
[
'display_name'
]
=
display_name
or
''
...
...
@@ -8205,12 +8217,18 @@ async def dashboard_wallet_topup(request: Request):
if
not
gw
.
get
(
'enabled'
,
False
):
return
JSONResponse
({
"error"
:
f
"Payment method '{method}' is not enabled"
},
status_code
=
400
)
# Crypto:
return deposit address (manual transfer)
crypto_methods
=
{
'bitcoin'
,
'ethereum'
,
'usdt'
,
'usdc'
}
# Crypto:
generate per-user HD wallet address
crypto_methods
=
{
'bitcoin'
:
'btc'
,
'ethereum'
:
'eth'
,
'usdt'
:
'usdt'
,
'usdc'
:
'usdc'
}
if
method
in
crypto_methods
:
address
=
gw
.
get
(
'address'
,
''
)
if
not
address
:
return
JSONResponse
({
"error"
:
"Crypto address not configured"
},
status_code
=
503
)
crypto_type
=
crypto_methods
[
method
]
ps
=
getattr
(
request
.
app
.
state
,
'payment_service'
,
None
)
if
ps
is
None
:
return
JSONResponse
({
"error"
:
"Payment service unavailable"
},
status_code
=
503
)
try
:
address
=
await
ps
.
wallet_manager
.
get_or_create_user_address
(
user_id
,
crypto_type
)
except
Exception
as
e
:
logger
.
error
(
f
"Crypto address generation error: {e}"
)
return
JSONResponse
({
"error"
:
"Could not generate deposit address"
},
status_code
=
503
)
return
JSONResponse
({
"type"
:
"crypto"
,
"method"
:
method
,
...
...
@@ -8257,22 +8275,14 @@ async def dashboard_wallet_topup(request: Request):
if
method
==
'paypal'
:
try
:
payment_service
=
getattr
(
request
.
app
.
state
,
'payment_service'
,
None
)
if
payment_service
and
hasattr
(
payment_service
,
'paypal_handler'
):
from
decimal
import
Decimal
order
=
await
payment_service
.
paypal_handler
.
create_order
(
user_id
,
Decimal
(
str
(
amount
)),
metadata
=
{
"type"
:
"wallet_topup"
}
)
return
JSONResponse
({
"type"
:
"paypal"
,
"order_id"
:
order
.
id
})
# Fallback: direct PayPal redirect
client_id
=
gw
.
get
(
'client_id'
,
''
)
sandbox
=
gw
.
get
(
'sandbox'
,
True
)
paypal_base
=
"https://www.sandbox.paypal.com"
if
sandbox
else
"https://www.paypal.com"
return
JSONResponse
({
"type"
:
"paypal"
,
"paypal_base"
:
paypal_base
,
"client_id"
:
client_id
,
"amount"
:
amount
,
})
if
not
payment_service
or
not
hasattr
(
payment_service
,
'paypal_handler'
):
return
JSONResponse
({
"error"
:
"PayPal payment service unavailable"
},
status_code
=
503
)
from
decimal
import
Decimal
result
=
await
payment_service
.
paypal_handler
.
create_topup_order
(
user_id
,
Decimal
(
str
(
amount
)))
if
not
result
.
get
(
'success'
):
logger
.
error
(
f
"PayPal top-up error: {result.get('error')}"
)
return
JSONResponse
({
"error"
:
"PayPal checkout failed. Please try again."
},
status_code
=
502
)
return
JSONResponse
({
"type"
:
"paypal"
,
"approval_url"
:
result
[
'approval_url'
]})
except
Exception
as
e
:
logger
.
error
(
f
"PayPal top-up error: {e}"
)
return
JSONResponse
({
"error"
:
"PayPal checkout failed. Please try again."
},
status_code
=
502
)
...
...
@@ -8994,7 +9004,7 @@ async def dashboard_docs(request: Request):
# Convert markdown to HTML with extensions for better formatting
html_content
=
markdown
.
markdown
(
markdown_content
,
extensions
=
[
'fenced_code'
,
'tables'
,
'nl2br'
,
'sane_lists'
]
extensions
=
[
'fenced_code'
,
'tables'
,
'nl2br'
,
'sane_lists'
,
'toc'
]
)
else
:
html_content
=
"<p>Documentation file not found.</p>"
...
...
@@ -9039,6 +9049,17 @@ async def dashboard_about(request: Request):
markdown_content
,
extensions
=
[
'fenced_code'
,
'tables'
,
'nl2br'
,
'sane_lists'
]
)
# Rewrite DOCUMENTATION.md links to /dashboard/docs
html_content
=
re
.
sub
(
r'href="DOCUMENTATION\.md#([^"]*)"'
,
r'href="/dashboard/docs#\1"'
,
html_content
)
html_content
=
re
.
sub
(
r'href="DOCUMENTATION\.md"'
,
'href="/dashboard/docs"'
,
html_content
)
else
:
html_content
=
"<p>README file not found.</p>"
...
...
templates/dashboard/docs.html
View file @
13b80367
...
...
@@ -135,4 +135,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div
class=
"markdown-content"
>
{{ content|safe }}
</div>
<script>
if
(
location
.
hash
)
{
const
el
=
document
.
querySelector
(
location
.
hash
);
if
(
el
)
el
.
scrollIntoView
();
}
</script>
{% endblock %}
templates/dashboard/profile.html
View file @
13b80367
...
...
@@ -18,7 +18,7 @@
<div
class=
"card"
>
<h2>
Account Information
</h2>
<form
method=
"POST"
action=
"{{ url_for(request, '/dashboard/profile') }}"
>
<form
method=
"POST"
action=
"{{ url_for(request, '/dashboard/profile') }}"
enctype=
"multipart/form-data"
>
<div
class=
"form-group"
>
<label
for=
"username"
>
Username
</label>
<input
type=
"text"
id=
"username"
name=
"username"
value=
"{{ session.username }}"
required
>
...
...
@@ -48,9 +48,22 @@
<div
class=
"form-group"
>
<label>
Profile Picture
</label>
<div
style=
"display: flex; align-items: center; gap: 1rem;"
>
<img
src=
"https://www.gravatar.com/avatar/{{ session.email|md5 }}?s=96&d=identicon"
alt=
"Current avatar"
style=
"border-radius: 8px;"
>
<p
style=
"color: #a0a0a0;"
>
Profile pictures are managed via
<a
href=
"https://gravatar.com"
target=
"_blank"
>
Gravatar
</a>
using your email address
</p>
<div
style=
"display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;"
>
<div
style=
"position: relative; cursor: pointer;"
onclick=
"document.getElementById('profile_pic').click()"
>
{% if user.profile_pic %}
<img
id=
"avatar-preview"
src=
"{{ user.profile_pic }}"
alt=
"Profile picture"
style=
"width: 96px; height: 96px; border-radius: 8px; object-fit: cover; display: block;"
>
{% else %}
<img
id=
"avatar-preview"
src=
"https://www.gravatar.com/avatar/{{ session.email|md5 }}?s=96&d=identicon"
alt=
"Profile picture"
style=
"width: 96px; height: 96px; border-radius: 8px; object-fit: cover; display: block;"
>
{% endif %}
<div
style=
"position: absolute; inset: 0; background: rgba(0,0,0,0.45); border-radius: 8px; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.2s;"
id=
"avatar-overlay"
>
<span
style=
"color: #fff; font-size: 0.8rem; text-align: center;"
>
Change
</span>
</div>
</div>
<div>
<input
type=
"file"
id=
"profile_pic"
name=
"profile_pic"
accept=
"image/*"
style=
"display: none;"
onchange=
"previewAvatar(this)"
>
<button
type=
"button"
class=
"btn"
style=
"background: #1a1a2e; border: 1px solid #0f3460; color: #e0e0e0;"
onclick=
"document.getElementById('profile_pic').click()"
>
Upload Image
</button>
<small
style=
"color: #a0a0a0; display: block; margin-top: 0.5rem;"
>
Max 1 MB. JPG, PNG, GIF, WebP.
</small>
</div>
</div>
</div>
...
...
@@ -106,4 +119,19 @@
border-color
:
#667eea
;
}
</style>
<script>
function
previewAvatar
(
input
)
{
if
(
!
input
.
files
||
!
input
.
files
[
0
])
return
;
const
reader
=
new
FileReader
();
reader
.
onload
=
e
=>
{
document
.
getElementById
(
'avatar-preview'
).
src
=
e
.
target
.
result
;
};
reader
.
readAsDataURL
(
input
.
files
[
0
]);
}
const
avatarWrap
=
document
.
querySelector
(
'[onclick="document.getElementById(
\'
profile_pic
\'
).click()"]'
);
const
overlay
=
document
.
getElementById
(
'avatar-overlay'
);
if
(
avatarWrap
&&
overlay
)
{
avatarWrap
.
addEventListener
(
'mouseenter'
,
()
=>
overlay
.
style
.
opacity
=
'1'
);
avatarWrap
.
addEventListener
(
'mouseleave'
,
()
=>
overlay
.
style
.
opacity
=
'0'
);
}
</script>
{% endblock %}
\ No newline at end of file
templates/dashboard/prompts.html
View file @
13b80367
...
...
@@ -31,7 +31,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div
style=
"margin-bottom: 20px;"
>
<label
style=
"font-weight: 500; margin-bottom: 10px; display: block;"
>
Select Prompt File:
</label>
<select
id=
"prompt-selector"
onchange=
"switchPrompt()"
style=
"width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 3px; font-size: 14px;"
>
<select
id=
"prompt-selector"
onchange=
"switchPrompt()"
>
{% for prompt in prompts %}
<option
value=
"{{ prompt.key }}"
{%
if
loop
.
first
%}
selected
{%
endif
%}
>
{{ prompt.name }}
</option>
{% endfor %}
...
...
@@ -42,9 +42,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<input
type=
"hidden"
name=
"prompt_key"
id=
"prompt_key"
value=
""
>
<div
class=
"form-group"
>
<label
for=
"prompt_content"
style=
"font-weight: 500; margin-bottom: 10px; display: block;"
>
Prompt Content:
</label>
<textarea
id=
"prompt_content"
name=
"prompt_content"
style=
"
width: 100%; min-height: 400px; padding: 10px; border: 1px solid #ddd; border-radius: 3px; font-family: monospace; font-size: 13px; line-height: 1.5
;"
></textarea>
<small
style=
"color: #
666
; display: block; margin-top: 5px;"
>
Edit the prompt template. Use markdown formatting as needed.
</small>
<label
for=
"prompt_content"
>
Prompt Content:
</label>
<textarea
id=
"prompt_content"
name=
"prompt_content"
style=
"
min-height: 400px
;"
></textarea>
<small
style=
"color: #
a0a0a0
; display: block; margin-top: 5px;"
>
Edit the prompt template. Use markdown formatting as needed.
</small>
</div>
<div
style=
"display: flex; gap: 10px; margin-top: 20px;"
>
...
...
@@ -52,7 +52,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% if not is_admin %}
<button
type=
"button"
class=
"btn btn-secondary"
onclick=
"resetPrompt()"
>
Reset to Default
</button>
{% endif %}
<a
href=
"
/dashboard
"
class=
"btn btn-secondary"
>
Cancel
</a>
<a
href=
"
{{ url_for(request, '/dashboard') }}
"
class=
"btn btn-secondary"
>
Cancel
</a>
</div>
</form>
...
...
@@ -96,33 +96,4 @@ if (prompts.length > 0) {
document
.
getElementById
(
'prompt_key'
).
value
=
prompts
[
0
].
key
;
}
</script>
<style>
.form-group
{
margin-bottom
:
20px
;
}
textarea
{
resize
:
vertical
;
}
.btn
{
padding
:
10px
20px
;
border
:
none
;
border-radius
:
3px
;
cursor
:
pointer
;
font-size
:
14px
;
text-decoration
:
none
;
display
:
inline-block
;
}
.btn-secondary
{
background
:
#6c757d
;
color
:
white
;
}
.btn-secondary
:hover
{
background
:
#5a6268
;
}
</style>
{% endblock %}
templates/dashboard/wallet.html
View file @
13b80367
...
...
@@ -94,8 +94,7 @@
<div
style=
"display: flex; flex-wrap: wrap; gap: 8px;"
>
{% for name in crypto_gateways %}
{% set cfg = enabled_gateways[name] %}
{% set address = cfg.get('wallet_address') or cfg.get('address') or '' %}
<button
onclick=
"openCryptoModal('{{ name }}','{{ address }}')"
<button
onclick=
"openCryptoModal('{{ name }}')"
style=
"background:#1a1a2e; border:1px solid #0f3460; color:#e0e0e0; padding:8px 16px; border-radius:6px; cursor:pointer; font-size:13px; font-weight:600; display:flex; align-items:center; gap:6px; transition:border-color .15s;"
onmouseover=
"this.style.borderColor='#4a9eff'"
onmouseout=
"this.style.borderColor='#0f3460'"
>
{% if name == 'bitcoin' %}
<i
class=
"fab fa-bitcoin"
style=
"color:#f7931a;"
></i>
...
...
@@ -243,9 +242,10 @@
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
// ── Amount buttons ──────────────────────────────────────────
let
selectedAmount
=
null
;
let
selectedAmount
=
15
;
document
.
querySelectorAll
(
'.amount-btn'
).
forEach
(
btn
=>
{
if
(
parseFloat
(
btn
.
dataset
.
amount
)
===
selectedAmount
)
btn
.
classList
.
add
(
'active'
);
btn
.
addEventListener
(
'click'
,
function
()
{
document
.
querySelectorAll
(
'.amount-btn'
).
forEach
(
b
=>
b
.
classList
.
remove
(
'active'
));
this
.
classList
.
add
(
'active'
);
...
...
@@ -423,25 +423,50 @@ function copyAddress(addr, btn) {
let
_cryptoAddr
=
''
;
function
openCryptoModal
(
name
,
address
)
{
function
openCryptoModal
(
name
)
{
const
icons
=
{
bitcoin
:
'₿ Bitcoin (BTC)'
,
ethereum
:
'Ξ Ethereum (ETH)'
,
usdt
:
'₮ USDT'
,
usdc
:
'◎ USDC'
};
document
.
getElementById
(
'cryptoModalTitle'
).
textContent
=
icons
[
name
]
||
name
.
toUpperCase
();
document
.
getElementById
(
'cryptoAddress'
).
textContent
=
address
||
'Address not configured
'
;
_cryptoAddr
=
address
;
document
.
getElementById
(
'cryptoAddress'
).
textContent
=
'Loading…
'
;
_cryptoAddr
=
''
;
const
qrEl
=
document
.
getElementById
(
'cryptoQR'
);
qrEl
.
innerHTML
=
'<i class="fas fa-spinner fa-spin fa-2x" style="color:#0f3460;"></i>'
;
document
.
getElementById
(
'cryptoModal'
).
classList
.
add
(
'active'
);
if
(
address
)
{
const
qrUrl
=
`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=
${
encodeURIComponent
(
address
)}
&bgcolor=ffffff&color=000000&margin=8`
;
const
img
=
new
Image
();
img
.
onload
=
()
=>
{
qrEl
.
innerHTML
=
''
;
qrEl
.
appendChild
(
img
);
};
img
.
onerror
=
()
=>
{
qrEl
.
innerHTML
=
'<span style="color:#888;font-size:12px;">QR unavailable</span>'
;
};
img
.
src
=
qrUrl
;
img
.
style
.
cssText
=
'width:200px;height:200px;border-radius:8px;'
;
}
// Fetch a per-user deposit address from the server
const
amount
=
getAmount
()
||
15
;
fetch
(
'/dashboard/wallet/topup'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
},
body
:
JSON
.
stringify
({
amount
,
payment_method
:
name
})
})
.
then
(
r
=>
r
.
json
())
.
then
(
data
=>
{
if
(
data
.
error
)
{
document
.
getElementById
(
'cryptoAddress'
).
textContent
=
data
.
error
;
qrEl
.
innerHTML
=
'<span style="color:#f87171;font-size:12px;">Error</span>'
;
return
;
}
const
address
=
data
.
address
||
''
;
_cryptoAddr
=
address
;
document
.
getElementById
(
'cryptoAddress'
).
textContent
=
address
||
'Address unavailable'
;
if
(
address
)
{
const
qrUrl
=
`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=
${
encodeURIComponent
(
address
)}
&bgcolor=ffffff&color=000000&margin=8`
;
const
img
=
new
Image
();
img
.
onload
=
()
=>
{
qrEl
.
innerHTML
=
''
;
qrEl
.
appendChild
(
img
);
};
img
.
onerror
=
()
=>
{
qrEl
.
innerHTML
=
'<span style="color:#888;font-size:12px;">QR unavailable</span>'
;
};
img
.
src
=
qrUrl
;
img
.
style
.
cssText
=
'width:200px;height:200px;border-radius:8px;'
;
}
else
{
qrEl
.
innerHTML
=
'<span style="color:#888;font-size:12px;">No address</span>'
;
}
})
.
catch
(()
=>
{
document
.
getElementById
(
'cryptoAddress'
).
textContent
=
'Failed to load address'
;
qrEl
.
innerHTML
=
'<span style="color:#f87171;font-size:12px;">Error</span>'
;
});
}
function
closeCryptoModal
()
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment