Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from flask_bcrypt import Bcrypt
from flask_jwt_extended import JWTManager
from flask_apscheduler import APScheduler
from flask_pywebpush import WebPush
import sqlite_icu
import os

Expand Down Expand Up @@ -170,6 +171,7 @@ def get_secret(env_var: str, default: str = None) -> str | None:
migrate = Migrate(app, db, render_as_batch=True)
bcrypt = Bcrypt(app)
jwt = JWTManager(app)
push = WebPush(app)
socketio = SocketIO(
app,
json=app.json,
Expand Down
1 change: 1 addition & 0 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
from .challenge_password_reset import ChallengePasswordReset
from .oidc import OIDCLink, OIDCRequest
from .report import Report
from .push_notification_subscription import PushNotificationSubscription
41 changes: 41 additions & 0 deletions backend/app/models/push_notification_subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import Any, Self, TYPE_CHECKING, cast

from app import db
from sqlalchemy.orm import Mapped

Model = db.Model
if TYPE_CHECKING:
from app.models import Token
from app.helpers.db_model_base import DbModelBase

Model = DbModelBase


class PushNotificationSubscription(Model):
__tablename__ = "push_notification_subscription"

endpoint: Mapped[str] = db.Column(db.String(), primary_key=True)
pubkey: Mapped[str] = db.Column(db.String())
auth: Mapped[str] = db.Column(db.String())

# Should never be an access token
created_by_token_id: Mapped[int] = db.Column(
db.Integer, db.ForeignKey("token.jti"), nullable=False
)
created_by_token: Mapped["Token"] = cast(
Mapped["Token"],
db.relationship("Token"),
)

def toSubsciptionInfo(self) -> dict[str, Any]:
return {
"endpoint": self.endpoint,
"keys": {
"p256dh": self.pubkey,
"auth": self.auth,
},
}

@classmethod
def find_by_user(cls, user_id: int) -> list[Self] | None:
return cls.query.filter(cls.created_by_token.user_id == user_id).all()
11 changes: 11 additions & 0 deletions backend/app/models/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

Model = db.Model
if TYPE_CHECKING:
from app.models import PushNotificationSubscription
from app.helpers.db_model_base import DbModelBase

Model = DbModelBase
Expand Down Expand Up @@ -53,6 +54,16 @@ class Token(Model):
lazy="selectin",
),
)
created_push_notification_subscriptions: Mapped[
List["PushNotificationSubscription"]
] = cast(
Mapped[List["PushNotificationSubscription"]],
db.relationship(
"PushNotificationSubscription",
back_populates="created_by_token",
cascade="all, delete-orphan",
),
)

def obj_to_dict(
self,
Expand Down
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies = [
"psycopg2-binary>=2.9.10",
"uWSGI>=2.0.28",
"uwsgi-tools>=1.1.1",
"flask-pywebpush>=1.1",
]

[dependency-groups]
Expand Down
53 changes: 53 additions & 0 deletions backend/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions kitchenowl/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ if (keystorePropertiesFile.exists()) {
dependencies {
implementation 'com.google.errorprone:error_prone_annotations:2.36.0' // required by flutter_secure_storage
implementation 'com.github.spotbugs:spotbugs-annotations:4.8.6' // required by flutter_secure_storage
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' // required by local notifications
implementation 'androidx.window:window:1.0.0' // required by local notifications
implementation 'androidx.window:window-java:1.0.0' // required by local notifications
implementation 'org.unifiedpush.android:embedded-fcm-distributor:3.0.0' // required by unified push
}

configurations.configureEach {
exclude group: 'com.google.crypto.tink', module: 'tink'
}

android {
Expand All @@ -39,6 +47,8 @@ android {
ndkVersion flutter.ndkVersion

compileOptions {
// Flag to enable support for the new language APIs (local notifications)
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="@android:color/white"
android:pathData="M5,16c0,3.87 3.13,7 7,7s7,-3.13 7,-7v-4L5,12v4zM16.12,4.37l2.1,-2.1 -0.82,-0.83 -2.3,2.31C14.16,3.28 13.12,3 12,3s-2.16,0.28 -3.09,0.75L6.6,1.44l-0.82,0.83 2.1,2.1C6.14,5.64 5,7.68 5,10v1h14v-1c0,-2.32 -1.14,-4.36 -2.88,-5.63zM9,9c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM15,9c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>
</group>
</vector>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/notification_icon" />
4 changes: 4 additions & 0 deletions kitchenowl/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import 'package:kitchenowl/kitchenowl.dart';
import 'package:kitchenowl/router.dart';
import 'package:kitchenowl/services/api/api_service.dart';
import 'package:kitchenowl/services/background_task.dart';
import 'package:kitchenowl/services/notification_service.dart';
import 'package:kitchenowl/services/storage/storage.dart';
import 'package:kitchenowl/services/transaction_handler.dart';
import 'package:kitchenowl/styles/colors.dart';
Expand Down Expand Up @@ -109,6 +110,9 @@ class _AppState extends State<App> {
},
);
}

NotificationService.getInstance().initialize().whenComplete(
() => widget._settingsCubit.refreshNotificationDistributor());
}

@override
Expand Down
16 changes: 16 additions & 0 deletions kitchenowl/lib/cubits/settings_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kitchenowl/config.dart';
import 'package:kitchenowl/kitchenowl.dart';
import 'package:kitchenowl/services/notification_service.dart';
import 'package:kitchenowl/services/storage/storage.dart';
import 'package:package_info_plus/package_info_plus.dart';

Expand Down Expand Up @@ -55,6 +56,14 @@ class SettingsCubit extends Cubit<SettingsState> {
));
}

Future<void> refreshNotificationDistributor() async {
await Future.delayed(const Duration(seconds: 1));
String? distributor =
await NotificationService.getInstance().getDistributor();
print("Refresh distributor: ${distributor}");
emit(state.copyWith(notificationDistributor: Nullable(distributor)));
}

void setTheme(ThemeMode themeMode) {
PreferenceStorage.getInstance()
.writeInt(key: 'themeMode', value: themeMode.index);
Expand Down Expand Up @@ -133,6 +142,7 @@ class SettingsState extends Equatable {
final bool shoppingListTapToRemove;
final bool recentItemsCategorize;
final bool restoreLastShoppingList;
final String? notificationDistributor;

const SettingsState({
this.themeMode = ThemeMode.system,
Expand All @@ -144,6 +154,7 @@ class SettingsState extends Equatable {
this.shoppingListTapToRemove = true,
this.recentItemsCategorize = false,
this.restoreLastShoppingList = false,
this.notificationDistributor,
});

SettingsState copyWith({
Expand All @@ -156,6 +167,7 @@ class SettingsState extends Equatable {
bool? shoppingListTapToRemove,
bool? recentItemsCategorize,
bool? restoreLastShoppingList,
Nullable<String>? notificationDistributor,
}) =>
SettingsState(
themeMode: themeMode ?? this.themeMode,
Expand All @@ -170,6 +182,9 @@ class SettingsState extends Equatable {
recentItemsCategorize ?? this.recentItemsCategorize,
restoreLastShoppingList:
restoreLastShoppingList ?? this.restoreLastShoppingList,
notificationDistributor:
(notificationDistributor ?? Nullable(this.notificationDistributor))
.value,
);

@override
Expand All @@ -183,5 +198,6 @@ class SettingsState extends Equatable {
shoppingListTapToRemove,
recentItemsCategorize,
restoreLastShoppingList,
notificationDistributor
];
}
29 changes: 29 additions & 0 deletions kitchenowl/lib/pages/settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import 'package:kitchenowl/pages/household_update_page.dart';
import 'package:kitchenowl/pages/reports_list_page.dart';
import 'package:kitchenowl/pages/settings_server_user_page.dart';
import 'package:kitchenowl/services/api/api_service.dart';
import 'package:kitchenowl/services/notification_service.dart';
import 'package:kitchenowl/styles/colors.dart';
import 'package:kitchenowl/widgets/settings/color_button.dart';
import 'package:kitchenowl/widgets/sliver_expansion_tile.dart';
import 'package:kitchenowl/widgets/user_list_tile.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:unifiedpush_ui/unifiedpush_ui.dart';

class SettingsPage extends StatefulWidget {
final Household? household;
Expand Down Expand Up @@ -381,6 +383,33 @@ class _SettingsPageState extends State<SettingsPage> {
),
),
),
if (!kIsWeb && (Platform.isAndroid || Platform.isLinux))
ListTile(
title: Text(
AppLocalizations.of(context)!.server,
),
leading: const Icon(Icons.notifications_rounded),
onTap: () async {
state.notificationDistributor != null
? await NotificationService.getInstance()
.unregister()
: await UnifiedPushUi(
context: context,
instances: [NotificationService.instanceName],
unifiedPushFunctions:
NotificationService.getInstance(),
showNoDistribDialog: true,
onNoDistribDialogDismissed: () {},
).registerAppWithDialog();
BlocProvider.of<SettingsCubit>(context)
.refreshNotificationDistributor();
},
trailing: Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(state.notificationDistributor ??
AppLocalizations.of(context)!.none),
),
),
],
),
),
Expand Down
Loading
Loading