Skip to content

Commit eff1947

Browse files
authored
Merge pull request #520 from sjefferson99/514-add-notification-about-availabliity-of-installable-app
feat(notifications): add mobile app install notification for new users
2 parents 745796a + ae1e034 commit eff1947

4 files changed

Lines changed: 141 additions & 7 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Add mobile/desktop app install notification.
2+
3+
Revision ID: 034
4+
Revises: 033
5+
Create Date: 2026-08-05
6+
7+
"""
8+
from alembic import op
9+
from sqlalchemy import text as sa_text
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '034'
14+
down_revision = '033'
15+
branch_labels = None
16+
depends_on = None
17+
18+
SUBJECT = 'Install AFT as an app'
19+
20+
MESSAGE = (
21+
"AFT can be installed as an app on your phone, tablet, Mac or PC for a "
22+
"full-screen, home-screen experience with no browser address bar. "
23+
"Mac/PC (Chrome or Edge): open AFT, then click the install icon in the "
24+
"address bar, or the browser menu > Install AFT. "
25+
"Android (Chrome): open AFT, tap the menu (three dots) > Install app, "
26+
"or use the Install App button in AFT Settings. "
27+
"iPhone/iPad (Safari): open AFT, tap the Share icon, then Add to Home "
28+
"Screen. See AFT Settings for step-by-step help."
29+
)
30+
31+
ACTION_TITLE = 'Open Settings'
32+
ACTION_URL = '/settings.html'
33+
34+
35+
def upgrade():
36+
"""Notify all active, approved users about the installable app."""
37+
conn = op.get_bind()
38+
conn.execute(
39+
sa_text("""
40+
INSERT INTO notifications (subject, message, unread, created_at, action_title, action_url, user_id)
41+
SELECT :subject, :message, 1, NOW(), :action_title, :action_url, u.id
42+
FROM users u
43+
WHERE u.is_active = 1
44+
AND u.is_approved = 1
45+
AND NOT EXISTS (
46+
SELECT 1 FROM notifications n
47+
WHERE n.user_id = u.id AND n.subject = :subject
48+
)
49+
"""),
50+
{
51+
'subject': SUBJECT,
52+
'message': MESSAGE,
53+
'action_title': ACTION_TITLE,
54+
'action_url': ACTION_URL,
55+
},
56+
)
57+
58+
59+
def downgrade():
60+
"""Remove the mobile/desktop app install notification."""
61+
conn = op.get_bind()
62+
conn.execute(
63+
sa_text("DELETE FROM notifications WHERE subject = :subject"),
64+
{'subject': SUBJECT},
65+
)

server/auth.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -946,9 +946,12 @@ def register():
946946
create_default_user_settings(user.id, db)
947947

948948
db.commit()
949-
949+
950950
logger.info(f"New user registered: {user.email} (ID: {user.id})")
951-
951+
952+
from notification_utils import create_mobile_app_notification
953+
create_mobile_app_notification(user.id)
954+
952955
# Don't auto-login - user needs admin approval
953956
# Return success but explain approval is needed
954957
user_data = {
@@ -1335,9 +1338,12 @@ def ensure_administrator_role(target_user):
13351338
ensure_administrator_role(existing_admin)
13361339

13371340
db.commit()
1338-
1341+
13391342
user = existing_admin
13401343
logger.info(f"Updated default admin user: {user.email} (ID: {user.id})")
1344+
1345+
from notification_utils import create_mobile_app_notification
1346+
create_mobile_app_notification(user.id)
13411347
else:
13421348
# Create new admin user
13431349
user = User(
@@ -1362,9 +1368,12 @@ def ensure_administrator_role(target_user):
13621368
ensure_administrator_role(user)
13631369

13641370
db.commit()
1365-
1371+
13661372
logger.info(f"Created first admin user: {user.email} (ID: {user.id})")
1367-
1373+
1374+
from notification_utils import create_mobile_app_notification
1375+
create_mobile_app_notification(user.id)
1376+
13681377
# Auto-login after setup
13691378
session['user_id'] = user.id
13701379
session['user_email_hash'] = hashlib.sha256(user.email.encode()).hexdigest()[:16]

server/notification_utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,32 @@
66

77
logger = logging.getLogger(__name__)
88

9+
# Shared with alembic/versions/034_add_mobile_app_notification.py - keep both in sync.
10+
MOBILE_APP_NOTIFICATION_SUBJECT = 'Install AFT as an app'
11+
MOBILE_APP_NOTIFICATION_MESSAGE = (
12+
"AFT can be installed as an app on your phone, tablet, Mac or PC for a "
13+
"full-screen, home-screen experience with no browser address bar. "
14+
"Mac/PC (Chrome or Edge): open AFT, then click the install icon in the "
15+
"address bar, or the browser menu > Install AFT. "
16+
"Android (Chrome): open AFT, tap the menu (three dots) > Install app, "
17+
"or use the Install App button in AFT Settings. "
18+
"iPhone/iPad (Safari): open AFT, tap the Share icon, then Add to Home "
19+
"Screen. See AFT Settings for step-by-step help."
20+
)
21+
MOBILE_APP_NOTIFICATION_ACTION_TITLE = 'Open Settings'
22+
MOBILE_APP_NOTIFICATION_ACTION_URL = '/settings.html'
23+
24+
25+
def create_mobile_app_notification(user_id: int) -> bool:
26+
"""Create the 'install AFT as an app' notification for a single new user."""
27+
return create_notification(
28+
subject=MOBILE_APP_NOTIFICATION_SUBJECT,
29+
message=MOBILE_APP_NOTIFICATION_MESSAGE,
30+
action_title=MOBILE_APP_NOTIFICATION_ACTION_TITLE,
31+
action_url=MOBILE_APP_NOTIFICATION_ACTION_URL,
32+
user_id=user_id,
33+
)
34+
935

1036
def _resolve_recipient_user_ids(db, explicit_user_id=None):
1137
"""Resolve recipient user IDs for internal notifications.

server/tests/test_api_notifications.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,9 +1224,14 @@ def test_mark_all_read_only_affects_own_notifications(
12241224
})
12251225
assert b_create.status_code == 201
12261226

1227+
b_before = second_user_session.get(f'{api_client}/api/notifications')
1228+
b_unread_count = sum(1 for n in b_before.json()['notifications'] if n['unread'])
1229+
12271230
mark_resp = second_user_session.put(f'{api_client}/api/notifications/mark-all-read')
12281231
assert mark_resp.status_code == 200
1229-
assert mark_resp.json()['count'] == 1, "Expected exactly User B's 1 notification to be updated"
1232+
assert mark_resp.json()['count'] == b_unread_count, (
1233+
"Expected exactly User B's own unread notifications to be updated"
1234+
)
12301235

12311236
check = authenticated_session.get(f'{api_client}/api/notifications')
12321237
notifs = check.json()['notifications']
@@ -1255,9 +1260,15 @@ def test_delete_all_only_affects_own_notifications(
12551260
'subject': 'User B to delete',
12561261
'message': 'User B notification',
12571262
})
1263+
1264+
b_before = second_user_session.get(f'{api_client}/api/notifications')
1265+
b_notif_count = len(b_before.json()['notifications'])
1266+
12581267
delete_resp = second_user_session.delete(f'{api_client}/api/notifications/delete-all')
12591268
assert delete_resp.status_code == 200
1260-
assert delete_resp.json()['count'] == 1, "Expected exactly User B's 1 notification to be deleted"
1269+
assert delete_resp.json()['count'] == b_notif_count, (
1270+
"Expected exactly User B's own notifications to be deleted"
1271+
)
12611272

12621273
check = authenticated_session.get(f'{api_client}/api/notifications')
12631274
notifs = check.json()['notifications']
@@ -1266,6 +1277,29 @@ def test_delete_all_only_affects_own_notifications(
12661277
)
12671278

12681279

1280+
@pytest.mark.api
1281+
class TestMobileAppInstallNotification:
1282+
"""Regression test for Issue 514: new users get a mobile app install notification."""
1283+
1284+
def test_new_user_receives_mobile_app_install_notification(
1285+
self, api_client, second_user_session
1286+
):
1287+
"""A freshly registered user should have the 'Install AFT as an app' notification."""
1288+
response = second_user_session.get(f'{api_client}/api/notifications')
1289+
assert response.status_code == 200
1290+
1291+
notifications = response.json()['notifications']
1292+
install_notif = next(
1293+
(n for n in notifications if n['subject'] == 'Install AFT as an app'), None
1294+
)
1295+
assert install_notif is not None, (
1296+
f"Expected an 'Install AFT as an app' notification for a new user, "
1297+
f"but found subjects: {[n.get('subject') for n in notifications]}"
1298+
)
1299+
assert install_notif['unread'] is True
1300+
assert install_notif['action_url'] == '/settings.html'
1301+
1302+
12691303
@pytest.mark.api
12701304
class TestNotificationMultiUserCreation:
12711305
"""Test cases for admin creating notifications for all users."""

0 commit comments

Comments
 (0)