new migration in apps.applets to seed wallet applet models; many expanded styles in wallet.js, chiefly concerned w. wallet-oriented FTs tbh; some intermittent Windows cache errors quashed in dash view ITs; apps.dash.views & .urls now support wallet applets; apps.lyric.models now discerns tithe coins (available for purchase soon); new styles across many scss files, again many concerning wallet applets but also applets more generally and also unorthodox media query parameters to make UX more usable; a slew of new wallet partials
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
37
src/apps/applets/migrations/0003_wallet_applets.py
Normal file
37
src/apps/applets/migrations/0003_wallet_applets.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def seed_wallet_applets(apps, schema_editor):
|
||||||
|
Applet = apps.get_model('applets', 'Applet')
|
||||||
|
for slug, name, cols, rows in [
|
||||||
|
('wallet-balances', 'Wallet Balances', 3, 3),
|
||||||
|
('wallet-tokens', 'Wallet Tokens', 3, 3),
|
||||||
|
('wallet-payment', 'Payment Methods', 6, 2),
|
||||||
|
]:
|
||||||
|
Applet.objects.get_or_create(
|
||||||
|
slug=slug,
|
||||||
|
defaults={'name': name, 'grid_cols': cols, 'grid_rows': rows, 'context': 'wallet'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('applets', '0002_seed_applets'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='applet',
|
||||||
|
name='context',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
('dashboard', 'Dashboard'),
|
||||||
|
('gameboard', 'Gameboard'),
|
||||||
|
('wallet', 'Wallet'),
|
||||||
|
],
|
||||||
|
default='dashboard',
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(seed_wallet_applets, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -3,9 +3,11 @@ from django.db import models
|
|||||||
class Applet(models.Model):
|
class Applet(models.Model):
|
||||||
DASHBOARD = "dashboard"
|
DASHBOARD = "dashboard"
|
||||||
GAMEBOARD = "gameboard"
|
GAMEBOARD = "gameboard"
|
||||||
|
WALLET = "wallet"
|
||||||
CONTEXT_CHOICES = [
|
CONTEXT_CHOICES = [
|
||||||
(DASHBOARD, "Dashboard"),
|
(DASHBOARD, "Dashboard"),
|
||||||
(GAMEBOARD, "Gameboard"),
|
(GAMEBOARD, "Gameboard"),
|
||||||
|
(WALLET, "Wallet"),
|
||||||
]
|
]
|
||||||
|
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const initWallet = () => {
|
|||||||
|
|
||||||
const addBtn = document.getElementById('id_add_payment_method');
|
const addBtn = document.getElementById('id_add_payment_method');
|
||||||
const saveBtn = document.getElementById('id_save_payment_method');
|
const saveBtn = document.getElementById('id_save_payment_method');
|
||||||
|
const cancelBtn = document.getElementById('id_cancel_payment_method');
|
||||||
if (!addBtn) return;
|
if (!addBtn) return;
|
||||||
|
|
||||||
const getCsrf = () => document.cookie.match(/csrftoken=([^;]+)/)[1];
|
const getCsrf = () => document.cookie.match(/csrftoken=([^;]+)/)[1];
|
||||||
@@ -15,8 +16,25 @@ const initWallet = () => {
|
|||||||
const {client_secret, publishable_key} = await res.json();
|
const {client_secret, publishable_key} = await res.json();
|
||||||
stripe = Stripe(publishable_key);
|
stripe = Stripe(publishable_key);
|
||||||
elements = stripe.elements({clientSecret: client_secret});
|
elements = stripe.elements({clientSecret: client_secret});
|
||||||
elements.create('payment').mount('#id_stripe_payment_element');
|
const paymentEl = elements.create('payment');
|
||||||
|
paymentEl.mount('#id_stripe_payment_element');
|
||||||
saveBtn.hidden = false;
|
saveBtn.hidden = false;
|
||||||
|
cancelBtn.hidden = false;
|
||||||
|
const section = addBtn.closest('section');
|
||||||
|
const rowPx = 3 * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
|
const updateRows = () => {
|
||||||
|
const rows = Math.ceil(section.scrollHeight / rowPx) + 1;
|
||||||
|
section.style.setProperty('--applet-rows', String(rows));
|
||||||
|
};
|
||||||
|
paymentEl.on('ready', () => {
|
||||||
|
updateRows();
|
||||||
|
const iframe = document.querySelector('#id_stripe_payment_element iframe');
|
||||||
|
if (iframe) {
|
||||||
|
const obs = new MutationObserver(updateRows);
|
||||||
|
obs.observe(iframe, { attributes: true, attributeFilter: ['style'] });
|
||||||
|
section._stripeObs = obs;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
saveBtn.addEventListener('click', async () => {
|
saveBtn.addEventListener('click', async () => {
|
||||||
@@ -37,7 +55,55 @@ const initWallet = () => {
|
|||||||
const pm = document.createElement('div');
|
const pm = document.createElement('div');
|
||||||
pm.textContent = `${brand} ····${last4}`;
|
pm.textContent = `${brand} ····${last4}`;
|
||||||
document.getElementById('id_payment_methods').appendChild(pm);
|
document.getElementById('id_payment_methods').appendChild(pm);
|
||||||
|
elements.getElement('payment').unmount();
|
||||||
|
elements = null;
|
||||||
|
stripe = null;
|
||||||
|
saveBtn.hidden = true;
|
||||||
|
cancelBtn.hidden = true;
|
||||||
|
const section = cancelBtn.closest('section');
|
||||||
|
section.style.setProperty('--applet-rows', '2');
|
||||||
|
if (section._stripeObs) { section._stripeObs.disconnect(); section._stripeObs = null; }
|
||||||
|
});
|
||||||
|
|
||||||
|
cancelBtn.addEventListener('click', () => {
|
||||||
|
if (elements) {
|
||||||
|
elements.getElement('payment').unmount();
|
||||||
|
elements = null;
|
||||||
|
stripe = null;
|
||||||
|
}
|
||||||
|
saveBtn.hidden = true;
|
||||||
|
cancelBtn.hidden = true;
|
||||||
|
const section = cancelBtn.closest('section');
|
||||||
|
section.style.setProperty('--applet-rows', '2');
|
||||||
|
if (section._stripeObs) { section._stripeObs.disconnect(); section._stripeObs = null; }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initWallet);
|
function initWalletTooltips() {
|
||||||
|
const portal = document.getElementById('id_tooltip_portal');
|
||||||
|
if (!portal) return;
|
||||||
|
|
||||||
|
document.querySelectorAll('.wallet-tokens .token').forEach(token => {
|
||||||
|
const tooltip = token.querySelector('.token-tooltip');
|
||||||
|
if (!tooltip) return;
|
||||||
|
|
||||||
|
token.addEventListener('mouseenter', () => {
|
||||||
|
const rect = token.getBoundingClientRect();
|
||||||
|
portal.innerHTML = tooltip.innerHTML;
|
||||||
|
portal.classList.add('active');
|
||||||
|
const halfW = portal.offsetWidth / 2;
|
||||||
|
const rawLeft = rect.left + rect.width / 2;
|
||||||
|
const clampedLeft = Math.max(halfW + 8, Math.min(rawLeft, window.innerWidth - halfW - 8));
|
||||||
|
portal.style.left = Math.round(clampedLeft) + 'px';
|
||||||
|
portal.style.top = Math.round(rect.top) + 'px';
|
||||||
|
portal.style.transform = 'translate(-50%, calc(-100% - 0.5rem))';
|
||||||
|
});
|
||||||
|
|
||||||
|
token.addEventListener('mouseleave', () => {
|
||||||
|
portal.classList.remove('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initWallet);
|
||||||
|
document.addEventListener('DOMContentLoaded', initWalletTooltips);
|
||||||
@@ -2,6 +2,7 @@ import lxml.html
|
|||||||
|
|
||||||
from django.test import override_settings, TestCase
|
from django.test import override_settings, TestCase
|
||||||
|
|
||||||
|
from apps.applets.models import Applet, UserApplet
|
||||||
from apps.lyric.models import Token, User, Wallet
|
from apps.lyric.models import Token, User, Wallet
|
||||||
|
|
||||||
|
|
||||||
@@ -47,4 +48,95 @@ class WalletViewTest(TestCase):
|
|||||||
bundles = self.parsed.cssselect("#id_tithe_token_shop .token-bundle")
|
bundles = self.parsed.cssselect("#id_tithe_token_shop .token-bundle")
|
||||||
self.assertGreater(len(bundles), 0)
|
self.assertGreater(len(bundles), 0)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(COMPRESS_ENABLED=False)
|
||||||
|
class WalletViewAppletContextTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create(email="walletctx@test.io")
|
||||||
|
Applet.objects.get_or_create(
|
||||||
|
slug="wallet-balances",
|
||||||
|
defaults={"name": "Wallet Balances", "grid_cols": 3, "grid_rows": 3, "context": "wallet"},
|
||||||
|
)
|
||||||
|
Applet.objects.get_or_create(
|
||||||
|
slug="wallet-tokens",
|
||||||
|
defaults={"name": "Wallet Tokens", "grid_cols": 3, "grid_rows": 3, "context": "wallet"},
|
||||||
|
)
|
||||||
|
Applet.objects.get_or_create(
|
||||||
|
slug="wallet-payment",
|
||||||
|
defaults={"name": "Payment Methods", "grid_cols": 6, "grid_rows": 2, "context": "wallet"},
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_wallet_view_passes_applets_context(self):
|
||||||
|
response = self.client.get("/dashboard/wallet/")
|
||||||
|
slugs = [e["applet"].slug for e in response.context["applets"]]
|
||||||
|
self.assertIn("wallet-balances", slugs)
|
||||||
|
self.assertIn("wallet-tokens", slugs)
|
||||||
|
self.assertIn("wallet-payment", slugs)
|
||||||
|
|
||||||
|
def test_wallet_page_renders_applets_container(self):
|
||||||
|
response = self.client.get("/dashboard/wallet/")
|
||||||
|
parsed = lxml.html.fromstring(response.content)
|
||||||
|
[_] = parsed.cssselect("#id_wallet_applets_container")
|
||||||
|
|
||||||
|
def test_wallet_page_renders_gear_button(self):
|
||||||
|
response = self.client.get("/dashboard/wallet/")
|
||||||
|
parsed = lxml.html.fromstring(response.content)
|
||||||
|
[_] = parsed.cssselect(".gear-btn")
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(COMPRESS_ENABLED=False)
|
||||||
|
class ToggleWalletAppletsTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create(email="wallettoggle@test.io")
|
||||||
|
self.balances = Applet.objects.get_or_create(
|
||||||
|
slug="wallet-balances",
|
||||||
|
defaults={"name": "Wallet Balances", "grid_cols": 3, "grid_rows": 3, "context": "wallet"},
|
||||||
|
)[0]
|
||||||
|
self.tokens = Applet.objects.get_or_create(
|
||||||
|
slug="wallet-tokens",
|
||||||
|
defaults={"name": "Wallet Tokens", "grid_cols": 3, "grid_rows": 3, "context": "wallet"},
|
||||||
|
)[0]
|
||||||
|
Applet.objects.get_or_create(
|
||||||
|
slug="wallet-payment",
|
||||||
|
defaults={"name": "Payment Methods", "grid_cols": 6, "grid_rows": 2, "context": "wallet"},
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_toggle_requires_login(self):
|
||||||
|
self.client.logout()
|
||||||
|
response = self.client.post("/dashboard/wallet/toggle-applets", {})
|
||||||
|
self.assertRedirects(
|
||||||
|
response, "/?next=/dashboard/wallet/toggle-applets",
|
||||||
|
fetch_redirect_response=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_toggle_redirects_to_wallet(self):
|
||||||
|
response = self.client.post(
|
||||||
|
"/dashboard/wallet/toggle-applets", {"applets": ["wallet-balances"]}
|
||||||
|
)
|
||||||
|
self.assertRedirects(response, "/dashboard/wallet/", fetch_redirect_response=False)
|
||||||
|
|
||||||
|
def test_toggle_hides_unchecked_applet(self):
|
||||||
|
self.client.post(
|
||||||
|
"/dashboard/wallet/toggle-applets", {"applets": ["wallet-balances"]}
|
||||||
|
)
|
||||||
|
ua = UserApplet.objects.get(user=self.user, applet=self.tokens)
|
||||||
|
self.assertFalse(ua.visible)
|
||||||
|
|
||||||
|
def test_toggle_shows_checked_applet(self):
|
||||||
|
UserApplet.objects.create(user=self.user, applet=self.balances, visible=False)
|
||||||
|
self.client.post(
|
||||||
|
"/dashboard/wallet/toggle-applets", {"applets": ["wallet-balances"]}
|
||||||
|
)
|
||||||
|
ua = UserApplet.objects.get(user=self.user, applet=self.balances)
|
||||||
|
self.assertTrue(ua.visible)
|
||||||
|
|
||||||
|
def test_toggle_htmx_returns_container_partial(self):
|
||||||
|
response = self.client.post(
|
||||||
|
"/dashboard/wallet/toggle-applets",
|
||||||
|
{"applets": ["wallet-balances"]},
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "id_wallet_applets_container")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ urlpatterns = [
|
|||||||
path('users/<uuid:user_id>/', views.my_lists, name='my_lists'),
|
path('users/<uuid:user_id>/', views.my_lists, name='my_lists'),
|
||||||
path('toggle_applets', views.toggle_applets, name="toggle_applets"),
|
path('toggle_applets', views.toggle_applets, name="toggle_applets"),
|
||||||
path('wallet/', views.wallet, name='wallet'),
|
path('wallet/', views.wallet, name='wallet'),
|
||||||
|
path('wallet/toggle-applets', views.toggle_wallet_applets, name='toggle_wallet_applets'),
|
||||||
path('wallet/setup-intent', views.setup_intent, name='setup_intent'),
|
path('wallet/setup-intent', views.setup_intent, name='setup_intent'),
|
||||||
path('wallet/save-payment-method', views.save_payment_method, name='save_payment_method'),
|
path('wallet/save-payment-method', views.save_payment_method, name='save_payment_method'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -144,15 +144,34 @@ def toggle_applets(request):
|
|||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def wallet(request):
|
def wallet(request):
|
||||||
wallet = request.user.wallet
|
|
||||||
coin = request.user.tokens.filter(token_type=Token.COIN).first()
|
|
||||||
free_tokens = list(request.user.tokens.filter(token_type=Token.FREE))
|
|
||||||
return render(request, "apps/dashboard/wallet.html", {
|
return render(request, "apps/dashboard/wallet.html", {
|
||||||
"wallet": wallet,
|
"wallet": request.user.wallet,
|
||||||
"coin": coin,
|
"coin": request.user.tokens.filter(token_type=Token.COIN).first(),
|
||||||
"free_tokens": free_tokens,
|
"free_tokens": list(request.user.tokens.filter(token_type=Token.FREE)),
|
||||||
|
"tithe_tokens": list(request.user.tokens.filter(token_type=Token.TITHE)),
|
||||||
|
"applets": applet_context(request.user, "wallet"),
|
||||||
|
"page_class": "page-wallet",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@login_required(login_url="/")
|
||||||
|
def toggle_wallet_applets(request):
|
||||||
|
checked = request.POST.getlist("applets")
|
||||||
|
for applet in Applet.objects.filter(context="wallet"):
|
||||||
|
UserApplet.objects.update_or_create(
|
||||||
|
user=request.user,
|
||||||
|
applet=applet,
|
||||||
|
defaults={"visible": applet.slug in checked},
|
||||||
|
)
|
||||||
|
if request.headers.get("HX-Request"):
|
||||||
|
return render(request, "apps/wallet/_partials/_applets.html", {
|
||||||
|
"applets": applet_context(request.user, "wallet"),
|
||||||
|
"wallet": request.user.wallet,
|
||||||
|
"coin": request.user.tokens.filter(token_type=Token.COIN).first(),
|
||||||
|
"free_tokens": list(request.user.tokens.filter(token_type=Token.FREE)),
|
||||||
|
"tithe_tokens": list(request.user.tokens.filter(token_type=Token.TITHE)),
|
||||||
|
})
|
||||||
|
return redirect("wallet")
|
||||||
|
|
||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
def setup_intent(request):
|
def setup_intent(request):
|
||||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ class Token(models.Model):
|
|||||||
def tooltip_description(self):
|
def tooltip_description(self):
|
||||||
if self.token_type in (self.COIN, self.FREE):
|
if self.token_type in (self.COIN, self.FREE):
|
||||||
return "Admit 1 Entry"
|
return "Admit 1 Entry"
|
||||||
|
if self.token_type == self.TITHE:
|
||||||
|
return "+ Writ bonus"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def tooltip_expiry(self):
|
def tooltip_expiry(self):
|
||||||
|
|||||||
@@ -81,6 +81,16 @@ class FunctionalTest(StaticLiveServerTestCase):
|
|||||||
@wait
|
@wait
|
||||||
def wait_for(self, fn):
|
def wait_for(self, fn):
|
||||||
return fn()
|
return fn()
|
||||||
|
|
||||||
|
def wait_for_slow(self, fn, timeout=30):
|
||||||
|
start_time = time.time()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return fn()
|
||||||
|
except (AssertionError, WebDriverException) as e:
|
||||||
|
if time.time() - start_time > timeout:
|
||||||
|
raise e
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
def create_pre_authenticated_session(self, email):
|
def create_pre_authenticated_session(self, email):
|
||||||
if self.test_server:
|
if self.test_server:
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ class WalletDisplayTest(FunctionalTest):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
Applet.objects.get_or_create(slug="wallet", defaults={"name": "Wallet"})
|
Applet.objects.get_or_create(slug="wallet", defaults={"name": "Wallet"})
|
||||||
|
for slug, name, cols, rows in [
|
||||||
|
("wallet-balances", "Wallet Balances", 3, 3),
|
||||||
|
("wallet-tokens", "Wallet Tokens", 3, 3),
|
||||||
|
("wallet-payment", "Payment Methods", 6, 2),
|
||||||
|
]:
|
||||||
|
Applet.objects.get_or_create(
|
||||||
|
slug=slug,
|
||||||
|
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "wallet"},
|
||||||
|
)
|
||||||
|
|
||||||
def test_new_user_wallet_shows_starting_balances(self):
|
def test_new_user_wallet_shows_starting_balances(self):
|
||||||
# 1. Log in as new user
|
# 1. Log in as new user
|
||||||
@@ -45,14 +54,10 @@ class WalletDisplayTest(FunctionalTest):
|
|||||||
ActionChains(self.browser).move_to_element(coin).perform()
|
ActionChains(self.browser).move_to_element(coin).perform()
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertTrue(
|
lambda: self.assertTrue(
|
||||||
self.browser.find_element(
|
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
|
||||||
By.CSS_SELECTOR, "#id_coin_on_a_string .token-tooltip"
|
|
||||||
).is_displayed()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
coin_tooltip = self.browser.find_element(
|
coin_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
|
||||||
By.CSS_SELECTOR, "#id_coin_on_a_string .token-tooltip"
|
|
||||||
).text
|
|
||||||
self.assertIn("Coin-on-a-String", coin_tooltip)
|
self.assertIn("Coin-on-a-String", coin_tooltip)
|
||||||
self.assertIn("Admit 1 Entry", coin_tooltip)
|
self.assertIn("Admit 1 Entry", coin_tooltip)
|
||||||
self.assertIn("no expiry", coin_tooltip)
|
self.assertIn("no expiry", coin_tooltip)
|
||||||
@@ -62,14 +67,10 @@ class WalletDisplayTest(FunctionalTest):
|
|||||||
ActionChains(self.browser).move_to_element(free_token).perform()
|
ActionChains(self.browser).move_to_element(free_token).perform()
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertTrue(
|
lambda: self.assertTrue(
|
||||||
self.browser.find_element(
|
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
|
||||||
By.CSS_SELECTOR, "#id_free_token_0 .token-tooltip"
|
|
||||||
).is_displayed()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
free_tooltip = self.browser.find_element(
|
free_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
|
||||||
By.CSS_SELECTOR, "#id_free_token_0 .token-tooltip"
|
|
||||||
).text
|
|
||||||
self.assertIn("Free Token", free_tooltip)
|
self.assertIn("Free Token", free_tooltip)
|
||||||
self.assertIn("Admit 1 Entry", free_tooltip)
|
self.assertIn("Admit 1 Entry", free_tooltip)
|
||||||
self.assertIn("Expires", free_tooltip)
|
self.assertIn("Expires", free_tooltip)
|
||||||
@@ -125,10 +126,15 @@ class WalletDisplayTest(FunctionalTest):
|
|||||||
).send_keys("42424")
|
).send_keys("42424")
|
||||||
# 6. Return to main doc & submit form
|
# 6. Return to main doc & submit form
|
||||||
self.browser.switch_to.default_content()
|
self.browser.switch_to.default_content()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertFalse(
|
||||||
|
self.browser.find_element(By.ID, "id_save_payment_method").get_attribute("hidden")
|
||||||
|
)
|
||||||
|
)
|
||||||
self.browser.find_element(By.ID, "id_save_payment_method").click()
|
self.browser.find_element(By.ID, "id_save_payment_method").click()
|
||||||
# 7. Wait for saved card to appear in payment methods list
|
# 7. Wait for saved card to appear in payment methods list
|
||||||
# Assert last 4 digits shown
|
# Assert last 4 digits shown (Stripe confirmSetup + server round-trip can be slow)
|
||||||
self.wait_for(
|
self.wait_for_slow(
|
||||||
lambda: self.assertIn(
|
lambda: self.assertIn(
|
||||||
"4242",
|
"4242",
|
||||||
self.browser.find_element(By.ID, "id_payment_methods").text,
|
self.browser.find_element(By.ID, "id_payment_methods").text,
|
||||||
|
|||||||
@@ -72,8 +72,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#id_dash_applet_menu { @extend %applet-menu; }
|
#id_dash_applet_menu { @extend %applet-menu; }
|
||||||
#id_game_applet_menu { @extend %applet-menu; }
|
#id_game_applet_menu { @extend %applet-menu; }
|
||||||
|
#id_wallet_applet_menu { @extend %applet-menu; }
|
||||||
|
|
||||||
// ── Applets grid (shared across all boards) ────────────────
|
// ── Applets grid (shared across all boards) ────────────────
|
||||||
%applets-grid {
|
%applets-grid {
|
||||||
@@ -101,7 +102,7 @@
|
|||||||
0.2rem solid rgba(var(--secUser), 0.5),
|
0.2rem solid rgba(var(--secUser), 0.5),
|
||||||
;
|
;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset -1px -1px 0 rgba(255, 255, 255, 0.125),
|
inset -0.125rem -0.125rem 0 rgba(var(--ninUser), 0.125),
|
||||||
inset 0.125rem 0.125rem 0 rgba(0, 0, 0, 0.8)
|
inset 0.125rem 0.125rem 0 rgba(0, 0, 0, 0.8)
|
||||||
;
|
;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
@@ -119,3 +120,4 @@
|
|||||||
|
|
||||||
#id_applets_container { @extend %applets-grid; }
|
#id_applets_container { @extend %applets-grid; }
|
||||||
#id_game_applets_container { @extend %applets-grid; }
|
#id_game_applets_container { @extend %applets-grid; }
|
||||||
|
#id_wallet_applets_container { @extend %applets-grid; }
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ body {
|
|||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
|
|
||||||
.col-lg-6 h2 {
|
.col-lg-6 h2 {
|
||||||
font-size: 2.1rem;
|
font-size: 2rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
// text-justify: inter-character is Firefox-only; approximate for Safari/Chrome
|
// text-justify: inter-character is Firefox-only; approximate for Safari/Chrome
|
||||||
letter-spacing: 1em;
|
letter-spacing: 1em;
|
||||||
@@ -234,7 +234,7 @@ body {
|
|||||||
text-align-last: center;
|
text-align-last: center;
|
||||||
letter-spacing: 0.25em;
|
letter-spacing: 0.25em;
|
||||||
margin: 0 0 0.5rem;
|
margin: 0 0 0.5rem;
|
||||||
font-size: 2.2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ body.page-gameboard {
|
|||||||
.gameboard-page {
|
.gameboard-page {
|
||||||
min-width: 666px;
|
min-width: 666px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.page-gameboard .container {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#id_applet_game_kit {
|
#id_applet_game_kit {
|
||||||
|
|||||||
@@ -51,6 +51,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.token--empty {
|
||||||
|
cursor: help;
|
||||||
|
|
||||||
|
> i { opacity: 0.4; }
|
||||||
|
}
|
||||||
|
|
||||||
|
html:has(body.page-wallet) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.page-wallet {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-page {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-tokens {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.token {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token:hover .token-tooltip { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#id_payment_methods {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.token .token-tooltip {
|
.token .token-tooltip {
|
||||||
width: 13rem;
|
width: 13rem;
|
||||||
@@ -58,4 +110,9 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wallet-tokens .token-tooltip {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,15 @@
|
|||||||
{% extends "core/base.html" %}
|
{% extends "core/base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title_text %}Dashboard{% endblock title_text %}
|
{% block title_text %}Dashwallet{% endblock title_text %}
|
||||||
{% block header_text %}<span>Dash</span>board{% endblock header_text %}
|
{% block header_text %}<span>Dash</span>wallet{% endblock header_text %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="wallet-page">
|
<div class="wallet-page">
|
||||||
<h1>Wallet</h1>
|
{% include "apps/applets/_partials/_gear.html" with menu_id="id_wallet_applet_menu" %}
|
||||||
|
{% include "apps/wallet/_partials/_applets.html" %}
|
||||||
<section class="wallet-balances">
|
|
||||||
<div><i class="fa-solid fa-ticket"></i>: <span id="id_writs_balance">{{ wallet.writs }}</span></div>
|
|
||||||
<div>Esteem: <span id="id_esteem_balance">{{ wallet.esteem }}</span></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="wallet-tokens">
|
|
||||||
{% if coin %}
|
|
||||||
<div id="id_coin_on_a_string" class="token">
|
|
||||||
<i class="fa-solid fa-clover"></i>
|
|
||||||
<div class="token-tooltip">
|
|
||||||
<h4>{{ coin.tooltip_name }}</h4>
|
|
||||||
<p>{{ coin.tooltip_description }}</p>
|
|
||||||
{% if coin.tooltip_shoptalk %}
|
|
||||||
<small><em>{{ coin.tooltip_shoptalk }}</em></small>
|
|
||||||
{% endif %}
|
|
||||||
<p class="expiry">{{ coin.tooltip_expiry }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% for token in free_tokens %}
|
|
||||||
<div id="id_free_token_{{ forloop.counter0 }}" class="token">
|
|
||||||
<i class="fa-solid fa-coins"></i>
|
|
||||||
<div class="token-tooltip">
|
|
||||||
<h4>{{ token.tooltip_name }}</h4>
|
|
||||||
<p>{{ token.tooltip_description }}</p>
|
|
||||||
{% if token.tooltip_shoptalk %}
|
|
||||||
<small><em>{{ token.tooltip_shoptalk }}</em></small>
|
|
||||||
{% endif %}
|
|
||||||
<p class="expiry">{{ token.tooltip_expiry }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="id_payment_methods">
|
|
||||||
<h2>Payment Methods</h2>
|
|
||||||
|
|
||||||
<button id="id_add_payment_method">Add Payment Method</button>
|
|
||||||
<div id="id_stripe_payment_element"></div>
|
|
||||||
<button id="id_save_payment_method" hidden>Save Card</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="id_tithe_token_shop">
|
|
||||||
<h2>Tithe Tokens</h2>
|
|
||||||
|
|
||||||
<div class="token-bundle">
|
|
||||||
<span><i class="fa-solid fa-piggy-bank"></i> Tithe Token ×1</span>
|
|
||||||
<span>+ Writ bonus</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="id_tooltip_portal" class="token-tooltip"></div>
|
||||||
<script src="https://js.stripe.com/v3/"></script>
|
<script src="https://js.stripe.com/v3/"></script>
|
||||||
<script src="{% static "apps/scripts/wallet.js" %}"></script>
|
<script src="{% static "apps/scripts/wallet.js" %}"></script>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<section
|
||||||
|
id="id_wallet_balances"
|
||||||
|
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||||
|
>
|
||||||
|
<div><i class="fa-solid fa-ticket"></i>: <span id="id_writs_balance">{{ wallet.writs }}</span></div>
|
||||||
|
<div>Esteem: <span id="id_esteem_balance">{{ wallet.esteem }}</span></div>
|
||||||
|
<div id="id_tithe_token_shop">
|
||||||
|
<div class="token-bundle" data-qty="1" data-price-cents="100">
|
||||||
|
<span class="bundle-qty">1 Tithe Token</span>
|
||||||
|
<span class="bundle-writs">+144 Writs</span>
|
||||||
|
<span class="bundle-price">$1.00</span>
|
||||||
|
</div>
|
||||||
|
<div class="token-bundle" data-qty="5" data-price-cents="400">
|
||||||
|
<span class="bundle-qty">5 Tithe Tokens</span>
|
||||||
|
<span class="bundle-writs">+750 Writs</span>
|
||||||
|
<span class="bundle-price">$4.00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<section
|
||||||
|
id="id_payment_methods"
|
||||||
|
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||||
|
>
|
||||||
|
<h2>Payment Methods</h2>
|
||||||
|
<button id="id_add_payment_method">Add Payment Method</button>
|
||||||
|
<div id="id_stripe_payment_element"></div>
|
||||||
|
<button id="id_save_payment_method" hidden>Save Card</button>
|
||||||
|
<button id="id_cancel_payment_method" hidden>Cancel</button>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<section
|
||||||
|
class="wallet-tokens"
|
||||||
|
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||||
|
>
|
||||||
|
{% if coin %}
|
||||||
|
<div id="id_coin_on_a_string" class="token">
|
||||||
|
<i class="fa-solid fa-clover"></i>
|
||||||
|
<div class="token-tooltip">
|
||||||
|
<h4>{{ coin.tooltip_name }}</h4>
|
||||||
|
<p>{{ coin.tooltip_description }}</p>
|
||||||
|
{% if coin.tooltip_shoptalk %}
|
||||||
|
<small><em>{{ coin.tooltip_shoptalk }}</em></small>
|
||||||
|
{% endif %}
|
||||||
|
<p class="expiry">{{ coin.tooltip_expiry }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% for token in free_tokens %}
|
||||||
|
<div id="id_free_token_{{ forloop.counter0 }}" class="token">
|
||||||
|
<i class="fa-solid fa-coins"></i>
|
||||||
|
<div class="token-tooltip">
|
||||||
|
<h4>{{ token.tooltip_name }}</h4>
|
||||||
|
<p>{{ token.tooltip_description }}</p>
|
||||||
|
{% if token.tooltip_shoptalk %}
|
||||||
|
<small><em>{{ token.tooltip_shoptalk }}</em></small>
|
||||||
|
{% endif %}
|
||||||
|
<p class="expiry">{{ token.tooltip_expiry }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% for token in tithe_tokens %}
|
||||||
|
<div id="id_tithe_token_{{ forloop.counter0 }}" class="token">
|
||||||
|
<i class="fa-solid fa-piggy-bank"></i>
|
||||||
|
<div class="token-tooltip">
|
||||||
|
<h4>{{ token.tooltip_name }}</h4>
|
||||||
|
<p>{{ token.tooltip_description }}</p>
|
||||||
|
{% if token.tooltip_shoptalk %}
|
||||||
|
<small><em>{{ token.tooltip_shoptalk }}</em></small>
|
||||||
|
{% endif %}
|
||||||
|
<p class="expiry">{{ token.tooltip_expiry }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div id="id_tithe_token_empty" class="token token--empty">
|
||||||
|
<i class="fa-solid fa-piggy-bank"></i>
|
||||||
|
<div class="token-tooltip">
|
||||||
|
<h4>Tithe Token</h4>
|
||||||
|
<p>0 owned</p>
|
||||||
|
<p class="expiry">purchase one above</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
27
src/templates/apps/wallet/_partials/_applets.html
Normal file
27
src/templates/apps/wallet/_partials/_applets.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<div id="id_wallet_applets_container">
|
||||||
|
<div id="id_wallet_applet_menu" style="display:none;">
|
||||||
|
<form
|
||||||
|
hx-post="{% url "toggle_wallet_applets" %}"
|
||||||
|
hx-target="#id_wallet_applets_container"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for entry in applets %}
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="applets"
|
||||||
|
value="{{ entry.applet.slug }}"
|
||||||
|
{% if entry.visible %}checked{% endif %}
|
||||||
|
>
|
||||||
|
{{ entry.applet.name }}
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="menu-btns">
|
||||||
|
<button type="submit" class="btn btn-confirm">OK</button>
|
||||||
|
<button type="button" class="btn btn-cancel applet-menu-cancel">NVM</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% include "apps/applets/_partials/_applets.html" %}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user