Skip to content

Commit 4bacd07

Browse files
DAdjadjclaude
andcommitted
Add review prompt and convert sync times to local timezone
- Show review prompt to paid users after 7 days of use - Submit reviews to API, with dismiss option - Convert UTC sync timestamps to browser local timezone automatically Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 78271e5 commit 4bacd07

File tree

3 files changed

+123
-2
lines changed

3 files changed

+123
-2
lines changed

app/db.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,14 @@ def update_bank_account_field(account_id: int, field: str, value: str):
168168
conn.execute(f"UPDATE bank_accounts SET {field} = ? WHERE id = ?", (value, account_id))
169169
conn.commit()
170170

171+
def get_first_sync_date() -> str:
172+
with _conn() as conn:
173+
_ensure_tables(conn)
174+
row = conn.execute(
175+
"SELECT ran_at FROM sync_log ORDER BY id ASC LIMIT 1"
176+
).fetchone()
177+
return row["ran_at"] if row else ""
178+
171179
def remove_bank_account(account_id: int):
172180
with _conn() as conn:
173181
_ensure_tables(conn)

app/web/server.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,19 @@ def status():
594594
]
595595
fun_message = random.choice(fun_messages)
596596

597+
# Review prompt logic
598+
show_review_prompt = False
599+
if not act_info.get("is_trial", False) and not db.get_setting("review_dismissed") and not db.get_setting("review_submitted"):
600+
first_sync = db.get_first_sync_date()
601+
if first_sync:
602+
try:
603+
from datetime import datetime
604+
first_dt = datetime.fromisoformat(first_sync)
605+
if (datetime.now() - first_dt).days >= 7:
606+
show_review_prompt = True
607+
except Exception:
608+
pass
609+
597610
return render_template("status.html",
598611
syncs=syncs,
599612
all_accounts=all_accounts,
@@ -616,6 +629,8 @@ def status():
616629
total_tx=total_tx,
617630
streak=streak,
618631
fun_message=fun_message,
632+
show_review_prompt=show_review_prompt,
633+
license_key=config.LICENCE_KEY,
619634
)
620635

621636
# ---------------------------------------------------------------------------
@@ -658,6 +673,42 @@ def _run():
658673
def sync_status():
659674
return jsonify({"running": _sync_running})
660675

676+
# ---------------------------------------------------------------------------
677+
# Review
678+
# ---------------------------------------------------------------------------
679+
680+
@app.route("/review/dismiss", methods=["POST"])
681+
def review_dismiss():
682+
db.set_setting("review_dismissed", "1")
683+
return redirect(url_for("status"))
684+
685+
@app.route("/review/submit", methods=["POST"])
686+
def review_submit():
687+
import requests as _requests
688+
rating = request.form.get("rating", "").strip()
689+
review_text = request.form.get("review", "").strip()
690+
name = request.form.get("name", "").strip()
691+
key = config.LICENCE_KEY
692+
if not rating or not review_text or not key:
693+
return redirect(url_for("status"))
694+
try:
695+
resp = _requests.post("https://api.bridgebank.app/review", json={
696+
"license_key": key,
697+
"name": name or None,
698+
"rating": int(rating),
699+
"review": review_text,
700+
}, timeout=10)
701+
if resp.status_code in (200, 201):
702+
db.set_setting("review_submitted", "1")
703+
else:
704+
logger.warning("Review submit failed: %s %s", resp.status_code, resp.text)
705+
# Still mark as submitted to avoid nagging on API errors
706+
db.set_setting("review_submitted", "1")
707+
except Exception as e:
708+
logger.error("Review submit error: %s", e)
709+
db.set_setting("review_submitted", "1")
710+
return redirect(url_for("status"))
711+
661712

662713
# ---------------------------------------------------------------------------
663714
# Disconnect

app/web/templates/status.html

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,61 @@
2727
<div class="alert alert-warn">A bank connection expires in {{ days_left }} day{{ 's' if days_left != 1 else '' }}. <a href="/connect" style="color:inherit;font-weight:600;">Manage banks &rarr;</a></div>
2828
{% endif %}
2929

30+
{% if show_review_prompt %}
31+
<div class="card" style="border-color:#3b82f6; border-width:1px;">
32+
<div class="card-title" style="margin-bottom:0.25rem;">How's Bridge Bank working for you?</div>
33+
<div class="card-sub" style="margin-bottom:1rem;">Your feedback helps other users find Bridge Bank.</div>
34+
<form method="POST" action="/review/submit" id="review-form">
35+
<div style="margin-bottom:1rem;">
36+
<div id="star-rating" style="display:flex;gap:4px;font-size:28px;cursor:pointer;user-select:none;">
37+
<span class="review-star" data-val="1">&#9734;</span>
38+
<span class="review-star" data-val="2">&#9734;</span>
39+
<span class="review-star" data-val="3">&#9734;</span>
40+
<span class="review-star" data-val="4">&#9734;</span>
41+
<span class="review-star" data-val="5">&#9734;</span>
42+
</div>
43+
<input type="hidden" name="rating" id="review-rating" value="" required>
44+
</div>
45+
<textarea name="review" placeholder="What do you like about Bridge Bank? Anything we could improve?" rows="3" required style="width:100%;box-sizing:border-box;background:#1a2235;border:1px solid #1e2a40;border-radius:8px;color:#e2e8f0;padding:0.75rem;font-size:13px;font-family:inherit;resize:vertical;margin-bottom:0.75rem;"></textarea>
46+
<input type="text" name="name" placeholder="Your name (optional)" style="width:100%;box-sizing:border-box;background:#1a2235;border:1px solid #1e2a40;border-radius:8px;color:#e2e8f0;padding:0.75rem;font-size:13px;font-family:inherit;margin-bottom:1rem;">
47+
<div style="display:flex;align-items:center;gap:1rem;">
48+
<button type="submit" class="btn" id="review-submit-btn" disabled style="background:linear-gradient(135deg,#3b82f6,#6366f1);color:#fff;border:none;border-radius:8px;padding:10px 20px;font-size:13px;font-weight:600;cursor:pointer;opacity:0.5;">Submit review</button>
49+
<a href="/review/dismiss" onclick="event.preventDefault();fetch('/review/dismiss',{method:'POST'}).then(()=>location.reload());" style="font-size:13px;color:#64748b;text-decoration:none;">Maybe later</a>
50+
</div>
51+
</form>
52+
</div>
53+
<script>
54+
(function() {
55+
var stars = document.querySelectorAll('.review-star');
56+
var ratingInput = document.getElementById('review-rating');
57+
var submitBtn = document.getElementById('review-submit-btn');
58+
var currentRating = 0;
59+
function setStars(n, hover) {
60+
stars.forEach(function(s, i) {
61+
s.innerHTML = (i < n) ? '&#9733;' : '&#9734;';
62+
s.style.color = (i < n) ? '#f59e0b' : '#4a5568';
63+
});
64+
}
65+
stars.forEach(function(star) {
66+
star.addEventListener('click', function() {
67+
currentRating = parseInt(this.dataset.val);
68+
ratingInput.value = currentRating;
69+
setStars(currentRating, false);
70+
submitBtn.disabled = false;
71+
submitBtn.style.opacity = '1';
72+
});
73+
star.addEventListener('mouseenter', function() {
74+
setStars(parseInt(this.dataset.val), true);
75+
});
76+
star.addEventListener('mouseleave', function() {
77+
setStars(currentRating, false);
78+
});
79+
});
80+
setStars(0, false);
81+
})();
82+
</script>
83+
{% endif %}
84+
3085
<div class="card">
3186
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:1.5rem;">
3287
<div>
@@ -37,7 +92,7 @@
3792
</div>
3893
<div class="stat-row">
3994
<span class="stat-label">Last sync</span>
40-
<span class="stat-value">{{ last_sync or 'Never' }}</span>
95+
<span class="stat-value utc-time">{{ last_sync or 'Never' }}</span>
4196
</div>
4297
<div class="stat-row"><span class="stat-label">Notifications</span><span class="stat-value">{{ notify_email or 'Disabled' }}</span></div>
4398
{% if total_tx >= 0 %}
@@ -112,7 +167,7 @@
112167
<tbody>
113168
{% for s in syncs %}
114169
<tr>
115-
<td>{{ s.ran_at }}</td>
170+
<td class="utc-time">{{ s.ran_at }}</td>
116171
<td><span class="badge {% if s.status == 'success' %}badge-success{% else %}badge-fail{% endif %}">{{ s.status }}</span></td>
117172
<td>{{ s.tx_count }}</td>
118173
<td style="max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="{{ s.message }}">{{ s.message or '—' }}</td>
@@ -245,5 +300,12 @@
245300
btn.disabled = false;
246301
}
247302
}
303+
document.querySelectorAll('.utc-time').forEach(function(el) {
304+
var raw = el.textContent.trim();
305+
if (!raw || raw === 'Never') return;
306+
var d = new Date(raw + 'Z');
307+
if (isNaN(d)) return;
308+
el.textContent = d.toLocaleString(undefined, {year:'numeric',month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
309+
});
248310
</script>
249311
{% endblock %}

0 commit comments

Comments
 (0)