0.99.49

parent f65694ef
......@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.99.48] - 2026-04-21
## [0.99.49] - 2026-04-21
### Added
- **Unified Wallet System**
......@@ -29,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Clarified token access patterns (global vs user tokens)
### Changed
- Updated all version numbers to 0.99.48
- Updated all version numbers to 0.99.49
- Added wallet package to pyproject.toml
## [0.99.26] - 2026-04-16
......
......@@ -41,8 +41,8 @@ python -m build
```
This creates:
- `dist/aisbf-0.99.48.tar.gz` - Source distribution
- `dist/aisbf-0.99.48-py3-none-any.whl` - Wheel distribution
- `dist/aisbf-0.99.49.tar.gz` - Source distribution
- `dist/aisbf-0.99.49-py3-none-any.whl` - Wheel distribution
## Testing the Package
......@@ -50,7 +50,7 @@ This creates:
```bash
# Install from the built wheel
pip install dist/aisbf-0.99.48-py3-none-any.whl
pip install dist/aisbf-0.99.49-py3-none-any.whl
# Test the installation
aisbf status
......
......@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.99.48"
__version__ = "0.99.49"
__all__ = [
# Config
"config",
......
......@@ -8171,6 +8171,10 @@ async def dashboard_wallet(request: Request):
all_gateways = db.get_payment_gateway_settings()
enabled_gateways = {k: v for k, v in all_gateways.items() if v.get('enabled', False)}
# Get user's saved Stripe credit cards for auto top-up
stripe_cards = [m for m in db.get_user_payment_methods(user_id)
if m.get('type') == 'stripe' or m.get('gateway') == 'stripe']
return templates.TemplateResponse(
request=request,
name="dashboard/wallet.html",
......@@ -8179,6 +8183,7 @@ async def dashboard_wallet(request: Request):
"wallet": wallet,
"enabled_gateways": enabled_gateways,
"currency_symbol": db.get_currency_settings().get('currency_symbol', '$'),
"stripe_cards": stripe_cards,
}
)
except ImportError:
......@@ -8348,6 +8353,11 @@ async def dashboard_billing(request: Request):
except Exception:
wallet = {'balance': '0.00', 'currency_code': currency_code, 'auto_topup_enabled': False}
# Get Stripe publishable key
stripe_publishable_key = ""
if 'stripe' in gateways and gateways['stripe'].get('enabled'):
stripe_publishable_key = gateways['stripe'].get('publishable_key', '')
return templates.TemplateResponse(
request=request,
name="dashboard/billing.html",
......@@ -8358,7 +8368,8 @@ async def dashboard_billing(request: Request):
"transactions": transactions,
"enabled_gateways": enabled_gateways,
"wallet": wallet,
"currency_symbol": currency_settings.get('currency_symbol', '$')
"currency_symbol": currency_settings.get('currency_symbol', '$'),
"stripe_publishable_key": stripe_publishable_key,
}
)
......
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aisbf"
version = "0.99.48"
version = "0.99.49"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md"
license = "GPL-3.0-or-later"
......
......@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.99.48",
version="0.99.49",
author="AISBF Contributors",
author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......@@ -227,7 +227,6 @@ setup(
'templates/dashboard/pricing.html',
'templates/dashboard/subscription.html',
'templates/dashboard/billing.html',
'templates/dashboard/add_payment_method.html',
'templates/dashboard/paypal_connect.html',
'templates/dashboard/cache_settings.html',
'templates/dashboard/wallet.html',
......
This diff is collapsed.
......@@ -84,14 +84,19 @@
<div style="text-align: center; padding: 40px; color: #a0a0a0;">
<i class="fas fa-credit-card fa-4x mb-3"></i>
<h4 style="margin-bottom: 15px;">No payment methods configured</h4>
<p style="margin-bottom: 20px;">Add a payment method to upgrade your plan and manage subscriptions</p>
<a href="{{ url_for(request, '/dashboard/billing/add-method') }}" class="btn">Add Payment Method</a>
<p style="margin-bottom: 20px;">Add a credit card to enable automatic subscription renewals.</p>
{% if 'stripe' in enabled_gateways %}
<button id="stripe-button" class="btn"><i class="fab fa-cc-stripe me-2"></i>Add Credit Card</button>
{% endif %}
</div>
{% endif %}
<div style="display: flex; gap: 10px; margin-top: 20px;">
<a href="{{ url_for(request, '/dashboard/billing/add-method') }}" class="btn">
<i class="fas fa-plus me-2"></i>Add Payment Method
<div style="display: flex; gap: 10px; margin-top: 20px; flex-wrap: wrap; align-items: center;">
{% if 'stripe' in enabled_gateways %}
<button id="stripe-button-bottom" class="btn"><i class="fab fa-cc-stripe me-2"></i>Add Credit Card</button>
{% endif %}
<a href="{{ url_for(request, '/dashboard/wallet') }}" class="btn btn-secondary">
<i class="fas fa-wallet me-2"></i>Top Up Wallet
</a>
</div>
</div>
......@@ -295,9 +300,83 @@
}
</style>
<!-- Stripe Card Modal -->
<div id="stripe-modal" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:1000;align-items:center;justify-content:center;">
<div style="background:#16213e;border:2px solid #4a9eff;border-radius:8px;padding:30px;max-width:500px;width:90%;position:relative;">
<button id="close-stripe-modal" style="position:absolute;top:15px;right:15px;background:transparent;border:none;color:#e0e0e0;font-size:24px;cursor:pointer;">&times;</button>
<h3 style="margin:0 0 20px 0;color:#4a9eff;"><i class="fab fa-cc-stripe me-2"></i>Add Credit Card</h3>
<form id="stripe-card-form">
<div style="margin-bottom:20px;">
<label style="display:block;margin-bottom:8px;color:#e0e0e0;font-weight:500;">Card Details</label>
<div id="card-element" style="background:#1a1a2e;padding:12px;border:1px solid #0f3460;border-radius:4px;"></div>
<div id="card-errors" style="color:#dc3545;margin-top:8px;font-size:14px;"></div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button type="button" id="cancel-stripe" class="btn btn-secondary">Cancel</button>
<button type="submit" id="stripe-submit" class="btn"><i class="fas fa-plus me-2"></i>Add Card</button>
</div>
</form>
</div>
</div>
<script src="https://js.stripe.com/v3/"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('Billing page JavaScript loaded');
// existing delete/set-default logic unchanged below...
{% if stripe_publishable_key %}
const STRIPE_KEY = '{{ stripe_publishable_key }}';
let stripe, cardElement, stripeReady = false;
function openStripeModal() {
document.getElementById('stripe-modal').style.display = 'flex';
if (!stripeReady) {
stripe = Stripe(STRIPE_KEY);
cardElement = stripe.elements().create('card', {
style: { base: { color: '#e0e0e0', fontSize: '16px', '::placeholder': { color: '#a0a0a0' } }, invalid: { color: '#dc3545' } }
});
cardElement.mount('#card-element');
cardElement.on('change', e => { document.getElementById('card-errors').textContent = e.error ? e.error.message : ''; });
stripeReady = true;
}
}
['stripe-button', 'stripe-button-bottom'].forEach(id => {
const btn = document.getElementById(id);
if (btn) btn.addEventListener('click', openStripeModal);
});
document.getElementById('close-stripe-modal').addEventListener('click', () => document.getElementById('stripe-modal').style.display = 'none');
document.getElementById('cancel-stripe').addEventListener('click', () => document.getElementById('stripe-modal').style.display = 'none');
document.getElementById('stripe-modal').addEventListener('click', e => { if (e.target === document.getElementById('stripe-modal')) document.getElementById('stripe-modal').style.display = 'none'; });
document.getElementById('stripe-card-form').addEventListener('submit', async e => {
e.preventDefault();
const btn = document.getElementById('stripe-submit');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Processing…';
const { error, paymentMethod } = await stripe.createPaymentMethod({ type: 'card', card: cardElement });
if (error) {
document.getElementById('card-errors').textContent = error.message;
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-plus me-2"></i>Add Card';
return;
}
const res = await fetch('/dashboard/billing/add-method/stripe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payment_method_id: paymentMethod.id })
});
const data = await res.json();
if (data.success) {
window.location.href = '/dashboard/billing?success=' + encodeURIComponent(data.message || 'Card added');
} else {
document.getElementById('card-errors').textContent = data.error || 'Failed to add card';
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-plus me-2"></i>Add Card';
}
});
{% endif %}
// Delete payment method
const deleteButtons = document.querySelectorAll('.delete-method-btn');
......
......@@ -130,6 +130,7 @@
</div>
<div id="auto-topup-settings" {% if not wallet.auto_topup_enabled %}style="display:none;"{% endif %}>
{% if stripe_cards %}
<div style="margin-bottom: 14px;">
<label style="display: block; color: #a0a0a0; font-size: 13px; margin-bottom: 6px;">Top-up amount</label>
<div style="position: relative;">
......@@ -148,21 +149,26 @@
value="{{ wallet.auto_topup_threshold or '' }}" step="0.01" min="1">
</div>
</div>
<div style="margin-bottom: 18px;">
<label style="display: block; color: #a0a0a0; font-size: 13px; margin-bottom: 6px;">Payment method</label>
<select id="auto-topup-payment-method"
style="width: 100%; padding: 10px; background: #1a1a2e; border: 1px solid #0f3460; border-radius: 6px; color: #e0e0e0; font-size: 14px;">
{% for name in enabled_gateways.keys() | select('in', ['stripe', 'paypal']) %}
<option value="{{ name }}" {% if wallet.auto_topup_payment_method == name %}selected{% endif %}>
{{ name | capitalize }}
</option>
{% endfor %}
</select>
<div style="margin-bottom: 14px; color: #a0a0a0; font-size: 13px;">
<i class="fab fa-cc-stripe me-1"></i>
Charged to your default credit card:
{% set default_card = stripe_cards | selectattr('is_default') | first | default(stripe_cards[0]) %}
<strong style="color: #e0e0e0;">•••• {{ default_card.last4 or '????' }} ({{ default_card.brand | default('Card') | capitalize }})</strong>
</div>
<button id="save-auto-topup"
style="background: #17a2b8; border: none; color: white; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; transition: background .15s;">
<i class="fas fa-save me-2"></i>Save Settings
</button>
{% else %}
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 6px; padding: 16px; text-align: center;">
<p style="color: #a0a0a0; margin: 0 0 12px 0; font-size: 13px;">
Auto top-up charges your saved credit card automatically.<br>You don't have a credit card on file yet.
</p>
<a href="{{ url_for(request, '/dashboard/billing') }}" class="btn" style="font-size: 13px; padding: 8px 16px;">
<i class="fab fa-cc-stripe me-2"></i>Add Credit Card
</a>
</div>
{% endif %}
</div>
{% if not wallet.auto_topup_enabled %}
......@@ -342,7 +348,7 @@ document.addEventListener('DOMContentLoaded', function () {
enabled: toggle.checked,
topup_amount: parseFloat(document.getElementById('auto-topup-amount').value) || null,
threshold_amount: parseFloat(document.getElementById('auto-topup-threshold').value) || null,
payment_method: document.getElementById('auto-topup-payment-method').value || null
payment_method: 'stripe'
};
fetch('/api/wallet/auto-topup', {
method: 'POST',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment