feat(auth): complete Phase 1 passwordless identity and encrypted DB foundation

This commit is contained in:
Mike Kell 2026-10-16 05:01:54 -04:00
parent 98e0d318f3
commit 7b6b646d2c
16 changed files with 1546 additions and 189 deletions

View File

@ -1,20 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../scaffold/adaptive_home_scaffold.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../../../core/network/supabase_status.dart';
import '../../features/auth/presentation/controllers/auth_controller.dart';
import '../../features/auth/presentation/screens/login_screen.dart';
import '../../features/medication/presentation/screens/medication_screen.dart';
import '../../features/nutrition/presentation/screens/nutrition_screen.dart';
import '../../features/meal_planner/presentation/screens/meal_planner_screen.dart';
import '../../features/pantry/presentation/screens/pantry_screen.dart';
import '../scaffold/adaptive_home_scaffold.dart';
/// Locations that may be visited without an authenticated session.
const Set<String> _publicPaths = {'/login'};
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();
/// Application router with a reactive authentication guard.
///
/// The guard re-evaluates on every GoTrue auth transition (bridged into
/// [refreshListenable] below), so a successful magic-link or OAuth round trip
/// immediately navigates into the protected shell, and signing out bounces the
/// user back to `/login`.
final routerProvider = Provider<GoRouter>((ref) {
final authRefreshNotifier = ValueNotifier<int>(0);
ref.onDispose(authRefreshNotifier.dispose);
ref.listen(authStateProvider, (_, __) => authRefreshNotifier.value++);
return GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/medication',
refreshListenable: authRefreshNotifier,
redirect: _authGuard,
routes: [
GoRoute(
path: '/login',
@ -47,3 +66,21 @@ final routerProvider = Provider<GoRouter>((ref) {
],
);
});
/// Redirect guard enforcing the passwordless session lifecycle:
/// * Unauthenticated sessions on a protected route -> `/login`.
/// * Authenticated sessions on `/login` -> `/medication`.
String? _authGuard(BuildContext context, GoRouterState state) {
final location = state.matchedLocation;
final isAuthenticated = isSupabaseAvailable &&
Supabase.instance.client.auth.currentSession != null;
if (!isAuthenticated && !_publicPaths.contains(location)) {
return '/login';
}
if (isAuthenticated && location == '/login') {
return '/medication';
}
return null;
}

View File

@ -0,0 +1,62 @@
import 'package:drift/drift.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'connection/unsupported.dart'
if (dart.library.ffi) 'connection/native.dart'
if (dart.library.js_interop) 'connection/web.dart';
part 'app_database.g.dart';
/// Immutable log of GLP-1 injections and scheduled doses.
///
/// [injectionSite] stores the anatomical rotation quadrant as a stable string
/// value (the `InjectionQuadrant.name` identifier of the medication domain
/// enum) so the persistence schema stays decoupled from domain types and the
/// `core` layer never imports feature code.
@DataClassName('MedicationDoseRecord')
class MedicationDosesTable extends Table {
IntColumn get id => integer().autoIncrement()();
/// Commercial or compounded name of the GLP-1/GIP agonist (e.g. Tirzepatide).
TextColumn get compoundName => text().withLength(min: 1, max: 128)();
/// Total injected quantity in milligrams.
RealColumn get doseMg => real()();
/// When the dose was prescribed to be taken (titration schedule anchor).
DateTimeColumn get scheduledTime => dateTime()();
/// When the dose was actually administered; `null` while still pending.
DateTimeColumn get administeredTime => dateTime().nullable()();
/// Rotation quadrant identifier (e.g. `abdomenLeftLower`).
TextColumn get injectionSite => text().nullable()();
TextColumn get notes => text().nullable()();
}
/// Root drift database for the GLP-1 Companion.
///
/// On native platforms the connection is backed by SQLCipher: the 256-bit AES
/// master key provisioned by `CryptoManager` is applied via `PRAGMA key`
/// immediately after the database opens (see `connection/native.dart`). Web
/// builds transparently fall back to a WASM SQLite database, since SQLCipher
/// cannot run inside the browser.
@DriftDatabase(tables: [MedicationDosesTable])
class AppDatabase extends _$AppDatabase {
/// Opens the platform-appropriate (encrypted on native) database connection.
AppDatabase() : super(openDatabaseConnection());
/// Injects a custom executor; used by unit tests with an in-memory database.
AppDatabase.forTesting(super.executor);
@override
int get schemaVersion => 1;
}
/// Application-wide database instance; closed when the container is disposed.
final appDatabaseProvider = Provider<AppDatabase>((ref) {
final database = AppDatabase();
ref.onDispose(database.close);
return database;
});

View File

@ -0,0 +1,654 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_database.dart';
// ignore_for_file: type=lint
class $MedicationDosesTableTable extends MedicationDosesTable
with TableInfo<$MedicationDosesTableTable, MedicationDoseRecord> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$MedicationDosesTableTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
static const VerificationMeta _compoundNameMeta =
const VerificationMeta('compoundName');
@override
late final GeneratedColumn<String> compoundName = GeneratedColumn<String>(
'compound_name', aliasedName, false,
additionalChecks:
GeneratedColumn.checkTextLength(minTextLength: 1, maxTextLength: 128),
type: DriftSqlType.string,
requiredDuringInsert: true);
static const VerificationMeta _doseMgMeta = const VerificationMeta('doseMg');
@override
late final GeneratedColumn<double> doseMg = GeneratedColumn<double>(
'dose_mg', aliasedName, false,
type: DriftSqlType.double, requiredDuringInsert: true);
static const VerificationMeta _scheduledTimeMeta =
const VerificationMeta('scheduledTime');
@override
late final GeneratedColumn<DateTime> scheduledTime =
GeneratedColumn<DateTime>('scheduled_time', aliasedName, false,
type: DriftSqlType.dateTime, requiredDuringInsert: true);
static const VerificationMeta _administeredTimeMeta =
const VerificationMeta('administeredTime');
@override
late final GeneratedColumn<DateTime> administeredTime =
GeneratedColumn<DateTime>('administered_time', aliasedName, true,
type: DriftSqlType.dateTime, requiredDuringInsert: false);
static const VerificationMeta _injectionSiteMeta =
const VerificationMeta('injectionSite');
@override
late final GeneratedColumn<String> injectionSite = GeneratedColumn<String>(
'injection_site', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false);
static const VerificationMeta _notesMeta = const VerificationMeta('notes');
@override
late final GeneratedColumn<String> notes = GeneratedColumn<String>(
'notes', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false);
@override
List<GeneratedColumn> get $columns => [
id,
compoundName,
doseMg,
scheduledTime,
administeredTime,
injectionSite,
notes
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'medication_doses_table';
@override
VerificationContext validateIntegrity(
Insertable<MedicationDoseRecord> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('compound_name')) {
context.handle(
_compoundNameMeta,
compoundName.isAcceptableOrUnknown(
data['compound_name']!, _compoundNameMeta));
} else if (isInserting) {
context.missing(_compoundNameMeta);
}
if (data.containsKey('dose_mg')) {
context.handle(_doseMgMeta,
doseMg.isAcceptableOrUnknown(data['dose_mg']!, _doseMgMeta));
} else if (isInserting) {
context.missing(_doseMgMeta);
}
if (data.containsKey('scheduled_time')) {
context.handle(
_scheduledTimeMeta,
scheduledTime.isAcceptableOrUnknown(
data['scheduled_time']!, _scheduledTimeMeta));
} else if (isInserting) {
context.missing(_scheduledTimeMeta);
}
if (data.containsKey('administered_time')) {
context.handle(
_administeredTimeMeta,
administeredTime.isAcceptableOrUnknown(
data['administered_time']!, _administeredTimeMeta));
}
if (data.containsKey('injection_site')) {
context.handle(
_injectionSiteMeta,
injectionSite.isAcceptableOrUnknown(
data['injection_site']!, _injectionSiteMeta));
}
if (data.containsKey('notes')) {
context.handle(
_notesMeta, notes.isAcceptableOrUnknown(data['notes']!, _notesMeta));
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
MedicationDoseRecord map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return MedicationDoseRecord(
id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
compoundName: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}compound_name'])!,
doseMg: attachedDatabase.typeMapping
.read(DriftSqlType.double, data['${effectivePrefix}dose_mg'])!,
scheduledTime: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, data['${effectivePrefix}scheduled_time'])!,
administeredTime: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, data['${effectivePrefix}administered_time']),
injectionSite: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}injection_site']),
notes: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}notes']),
);
}
@override
$MedicationDosesTableTable createAlias(String alias) {
return $MedicationDosesTableTable(attachedDatabase, alias);
}
}
class MedicationDoseRecord extends DataClass
implements Insertable<MedicationDoseRecord> {
final int id;
/// Commercial or compounded name of the GLP-1/GIP agonist (e.g. Tirzepatide).
final String compoundName;
/// Total injected quantity in milligrams.
final double doseMg;
/// When the dose was prescribed to be taken (titration schedule anchor).
final DateTime scheduledTime;
/// When the dose was actually administered; `null` while still pending.
final DateTime? administeredTime;
/// Rotation quadrant identifier (e.g. `abdomenLeftLower`).
final String? injectionSite;
final String? notes;
const MedicationDoseRecord(
{required this.id,
required this.compoundName,
required this.doseMg,
required this.scheduledTime,
this.administeredTime,
this.injectionSite,
this.notes});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['compound_name'] = Variable<String>(compoundName);
map['dose_mg'] = Variable<double>(doseMg);
map['scheduled_time'] = Variable<DateTime>(scheduledTime);
if (!nullToAbsent || administeredTime != null) {
map['administered_time'] = Variable<DateTime>(administeredTime);
}
if (!nullToAbsent || injectionSite != null) {
map['injection_site'] = Variable<String>(injectionSite);
}
if (!nullToAbsent || notes != null) {
map['notes'] = Variable<String>(notes);
}
return map;
}
MedicationDosesTableCompanion toCompanion(bool nullToAbsent) {
return MedicationDosesTableCompanion(
id: Value(id),
compoundName: Value(compoundName),
doseMg: Value(doseMg),
scheduledTime: Value(scheduledTime),
administeredTime: administeredTime == null && nullToAbsent
? const Value.absent()
: Value(administeredTime),
injectionSite: injectionSite == null && nullToAbsent
? const Value.absent()
: Value(injectionSite),
notes:
notes == null && nullToAbsent ? const Value.absent() : Value(notes),
);
}
factory MedicationDoseRecord.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return MedicationDoseRecord(
id: serializer.fromJson<int>(json['id']),
compoundName: serializer.fromJson<String>(json['compoundName']),
doseMg: serializer.fromJson<double>(json['doseMg']),
scheduledTime: serializer.fromJson<DateTime>(json['scheduledTime']),
administeredTime:
serializer.fromJson<DateTime?>(json['administeredTime']),
injectionSite: serializer.fromJson<String?>(json['injectionSite']),
notes: serializer.fromJson<String?>(json['notes']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'compoundName': serializer.toJson<String>(compoundName),
'doseMg': serializer.toJson<double>(doseMg),
'scheduledTime': serializer.toJson<DateTime>(scheduledTime),
'administeredTime': serializer.toJson<DateTime?>(administeredTime),
'injectionSite': serializer.toJson<String?>(injectionSite),
'notes': serializer.toJson<String?>(notes),
};
}
MedicationDoseRecord copyWith(
{int? id,
String? compoundName,
double? doseMg,
DateTime? scheduledTime,
Value<DateTime?> administeredTime = const Value.absent(),
Value<String?> injectionSite = const Value.absent(),
Value<String?> notes = const Value.absent()}) =>
MedicationDoseRecord(
id: id ?? this.id,
compoundName: compoundName ?? this.compoundName,
doseMg: doseMg ?? this.doseMg,
scheduledTime: scheduledTime ?? this.scheduledTime,
administeredTime: administeredTime.present
? administeredTime.value
: this.administeredTime,
injectionSite:
injectionSite.present ? injectionSite.value : this.injectionSite,
notes: notes.present ? notes.value : this.notes,
);
MedicationDoseRecord copyWithCompanion(MedicationDosesTableCompanion data) {
return MedicationDoseRecord(
id: data.id.present ? data.id.value : this.id,
compoundName: data.compoundName.present
? data.compoundName.value
: this.compoundName,
doseMg: data.doseMg.present ? data.doseMg.value : this.doseMg,
scheduledTime: data.scheduledTime.present
? data.scheduledTime.value
: this.scheduledTime,
administeredTime: data.administeredTime.present
? data.administeredTime.value
: this.administeredTime,
injectionSite: data.injectionSite.present
? data.injectionSite.value
: this.injectionSite,
notes: data.notes.present ? data.notes.value : this.notes,
);
}
@override
String toString() {
return (StringBuffer('MedicationDoseRecord(')
..write('id: $id, ')
..write('compoundName: $compoundName, ')
..write('doseMg: $doseMg, ')
..write('scheduledTime: $scheduledTime, ')
..write('administeredTime: $administeredTime, ')
..write('injectionSite: $injectionSite, ')
..write('notes: $notes')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, compoundName, doseMg, scheduledTime,
administeredTime, injectionSite, notes);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is MedicationDoseRecord &&
other.id == this.id &&
other.compoundName == this.compoundName &&
other.doseMg == this.doseMg &&
other.scheduledTime == this.scheduledTime &&
other.administeredTime == this.administeredTime &&
other.injectionSite == this.injectionSite &&
other.notes == this.notes);
}
class MedicationDosesTableCompanion
extends UpdateCompanion<MedicationDoseRecord> {
final Value<int> id;
final Value<String> compoundName;
final Value<double> doseMg;
final Value<DateTime> scheduledTime;
final Value<DateTime?> administeredTime;
final Value<String?> injectionSite;
final Value<String?> notes;
const MedicationDosesTableCompanion({
this.id = const Value.absent(),
this.compoundName = const Value.absent(),
this.doseMg = const Value.absent(),
this.scheduledTime = const Value.absent(),
this.administeredTime = const Value.absent(),
this.injectionSite = const Value.absent(),
this.notes = const Value.absent(),
});
MedicationDosesTableCompanion.insert({
this.id = const Value.absent(),
required String compoundName,
required double doseMg,
required DateTime scheduledTime,
this.administeredTime = const Value.absent(),
this.injectionSite = const Value.absent(),
this.notes = const Value.absent(),
}) : compoundName = Value(compoundName),
doseMg = Value(doseMg),
scheduledTime = Value(scheduledTime);
static Insertable<MedicationDoseRecord> custom({
Expression<int>? id,
Expression<String>? compoundName,
Expression<double>? doseMg,
Expression<DateTime>? scheduledTime,
Expression<DateTime>? administeredTime,
Expression<String>? injectionSite,
Expression<String>? notes,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (compoundName != null) 'compound_name': compoundName,
if (doseMg != null) 'dose_mg': doseMg,
if (scheduledTime != null) 'scheduled_time': scheduledTime,
if (administeredTime != null) 'administered_time': administeredTime,
if (injectionSite != null) 'injection_site': injectionSite,
if (notes != null) 'notes': notes,
});
}
MedicationDosesTableCompanion copyWith(
{Value<int>? id,
Value<String>? compoundName,
Value<double>? doseMg,
Value<DateTime>? scheduledTime,
Value<DateTime?>? administeredTime,
Value<String?>? injectionSite,
Value<String?>? notes}) {
return MedicationDosesTableCompanion(
id: id ?? this.id,
compoundName: compoundName ?? this.compoundName,
doseMg: doseMg ?? this.doseMg,
scheduledTime: scheduledTime ?? this.scheduledTime,
administeredTime: administeredTime ?? this.administeredTime,
injectionSite: injectionSite ?? this.injectionSite,
notes: notes ?? this.notes,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (compoundName.present) {
map['compound_name'] = Variable<String>(compoundName.value);
}
if (doseMg.present) {
map['dose_mg'] = Variable<double>(doseMg.value);
}
if (scheduledTime.present) {
map['scheduled_time'] = Variable<DateTime>(scheduledTime.value);
}
if (administeredTime.present) {
map['administered_time'] = Variable<DateTime>(administeredTime.value);
}
if (injectionSite.present) {
map['injection_site'] = Variable<String>(injectionSite.value);
}
if (notes.present) {
map['notes'] = Variable<String>(notes.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('MedicationDosesTableCompanion(')
..write('id: $id, ')
..write('compoundName: $compoundName, ')
..write('doseMg: $doseMg, ')
..write('scheduledTime: $scheduledTime, ')
..write('administeredTime: $administeredTime, ')
..write('injectionSite: $injectionSite, ')
..write('notes: $notes')
..write(')'))
.toString();
}
}
abstract class _$AppDatabase extends GeneratedDatabase {
_$AppDatabase(QueryExecutor e) : super(e);
$AppDatabaseManager get managers => $AppDatabaseManager(this);
late final $MedicationDosesTableTable medicationDosesTable =
$MedicationDosesTableTable(this);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [medicationDosesTable];
}
typedef $$MedicationDosesTableTableCreateCompanionBuilder
= MedicationDosesTableCompanion Function({
Value<int> id,
required String compoundName,
required double doseMg,
required DateTime scheduledTime,
Value<DateTime?> administeredTime,
Value<String?> injectionSite,
Value<String?> notes,
});
typedef $$MedicationDosesTableTableUpdateCompanionBuilder
= MedicationDosesTableCompanion Function({
Value<int> id,
Value<String> compoundName,
Value<double> doseMg,
Value<DateTime> scheduledTime,
Value<DateTime?> administeredTime,
Value<String?> injectionSite,
Value<String?> notes,
});
class $$MedicationDosesTableTableFilterComposer
extends Composer<_$AppDatabase, $MedicationDosesTableTable> {
$$MedicationDosesTableTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<int> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get compoundName => $composableBuilder(
column: $table.compoundName, builder: (column) => ColumnFilters(column));
ColumnFilters<double> get doseMg => $composableBuilder(
column: $table.doseMg, builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get scheduledTime => $composableBuilder(
column: $table.scheduledTime, builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get administeredTime => $composableBuilder(
column: $table.administeredTime,
builder: (column) => ColumnFilters(column));
ColumnFilters<String> get injectionSite => $composableBuilder(
column: $table.injectionSite, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get notes => $composableBuilder(
column: $table.notes, builder: (column) => ColumnFilters(column));
}
class $$MedicationDosesTableTableOrderingComposer
extends Composer<_$AppDatabase, $MedicationDosesTableTable> {
$$MedicationDosesTableTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<int> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get compoundName => $composableBuilder(
column: $table.compoundName,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<double> get doseMg => $composableBuilder(
column: $table.doseMg, builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get scheduledTime => $composableBuilder(
column: $table.scheduledTime,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get administeredTime => $composableBuilder(
column: $table.administeredTime,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get injectionSite => $composableBuilder(
column: $table.injectionSite,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get notes => $composableBuilder(
column: $table.notes, builder: (column) => ColumnOrderings(column));
}
class $$MedicationDosesTableTableAnnotationComposer
extends Composer<_$AppDatabase, $MedicationDosesTableTable> {
$$MedicationDosesTableTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<int> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<String> get compoundName => $composableBuilder(
column: $table.compoundName, builder: (column) => column);
GeneratedColumn<double> get doseMg =>
$composableBuilder(column: $table.doseMg, builder: (column) => column);
GeneratedColumn<DateTime> get scheduledTime => $composableBuilder(
column: $table.scheduledTime, builder: (column) => column);
GeneratedColumn<DateTime> get administeredTime => $composableBuilder(
column: $table.administeredTime, builder: (column) => column);
GeneratedColumn<String> get injectionSite => $composableBuilder(
column: $table.injectionSite, builder: (column) => column);
GeneratedColumn<String> get notes =>
$composableBuilder(column: $table.notes, builder: (column) => column);
}
class $$MedicationDosesTableTableTableManager extends RootTableManager<
_$AppDatabase,
$MedicationDosesTableTable,
MedicationDoseRecord,
$$MedicationDosesTableTableFilterComposer,
$$MedicationDosesTableTableOrderingComposer,
$$MedicationDosesTableTableAnnotationComposer,
$$MedicationDosesTableTableCreateCompanionBuilder,
$$MedicationDosesTableTableUpdateCompanionBuilder,
(
MedicationDoseRecord,
BaseReferences<_$AppDatabase, $MedicationDosesTableTable,
MedicationDoseRecord>
),
MedicationDoseRecord,
PrefetchHooks Function()> {
$$MedicationDosesTableTableTableManager(
_$AppDatabase db, $MedicationDosesTableTable table)
: super(TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$$MedicationDosesTableTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$$MedicationDosesTableTableOrderingComposer(
$db: db, $table: table),
createComputedFieldComposer: () =>
$$MedicationDosesTableTableAnnotationComposer(
$db: db, $table: table),
updateCompanionCallback: ({
Value<int> id = const Value.absent(),
Value<String> compoundName = const Value.absent(),
Value<double> doseMg = const Value.absent(),
Value<DateTime> scheduledTime = const Value.absent(),
Value<DateTime?> administeredTime = const Value.absent(),
Value<String?> injectionSite = const Value.absent(),
Value<String?> notes = const Value.absent(),
}) =>
MedicationDosesTableCompanion(
id: id,
compoundName: compoundName,
doseMg: doseMg,
scheduledTime: scheduledTime,
administeredTime: administeredTime,
injectionSite: injectionSite,
notes: notes,
),
createCompanionCallback: ({
Value<int> id = const Value.absent(),
required String compoundName,
required double doseMg,
required DateTime scheduledTime,
Value<DateTime?> administeredTime = const Value.absent(),
Value<String?> injectionSite = const Value.absent(),
Value<String?> notes = const Value.absent(),
}) =>
MedicationDosesTableCompanion.insert(
id: id,
compoundName: compoundName,
doseMg: doseMg,
scheduledTime: scheduledTime,
administeredTime: administeredTime,
injectionSite: injectionSite,
notes: notes,
),
withReferenceMapper: (p0) => p0
.map((e) => (
e.readTable<$MedicationDosesTableTable,
MedicationDoseRecord>(table),
BaseReferences<_$AppDatabase, $MedicationDosesTableTable,
MedicationDoseRecord>(db, table, e)
))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$MedicationDosesTableTableProcessedTableManager
= ProcessedTableManager<
_$AppDatabase,
$MedicationDosesTableTable,
MedicationDoseRecord,
$$MedicationDosesTableTableFilterComposer,
$$MedicationDosesTableTableOrderingComposer,
$$MedicationDosesTableTableAnnotationComposer,
$$MedicationDosesTableTableCreateCompanionBuilder,
$$MedicationDosesTableTableUpdateCompanionBuilder,
(
MedicationDoseRecord,
BaseReferences<_$AppDatabase, $MedicationDosesTableTable,
MedicationDoseRecord>
),
MedicationDoseRecord,
PrefetchHooks Function()>;
class $AppDatabaseManager {
final _$AppDatabase _db;
$AppDatabaseManager(this._db);
$$MedicationDosesTableTableTableManager get medicationDosesTable =>
$$MedicationDosesTableTableTableManager(_db, _db.medicationDosesTable);
}

View File

@ -0,0 +1,39 @@
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../security/crypto_manager.dart';
/// Opens the local SQLCipher-encrypted database file.
///
/// The hardware-backed 256-bit AES master key is generated on first launch and
/// stored in the OS secure vault (Keychain / Keystore / DPAPI) by
/// [CryptoManager]; it is handed to SQLCipher through `PRAGMA key` before any
/// statement executes.
///
/// The native SQLite engine is compiled as SQLCipher through the `sqlite3`
/// build-hook define in `pubspec.yaml` (`hooks.user_defines.sqlite3.source`),
/// which ships the encrypted engine on every native platform (Android, iOS,
/// macOS, Windows, Linux) without dynamic-library override code.
QueryExecutor openDatabaseConnection() {
return LazyDatabase(() async {
final supportDirectory = await getApplicationSupportDirectory();
final databaseFile = File(
p.join(supportDirectory.path, 'glp1_companion.db'),
);
final masterKey = await CryptoManager().getOrCreateDatabaseEncryptionKey();
return NativeDatabase(
databaseFile,
setup: (db) {
// SQLCipher derives the AES-256 page key from the master-key
// passphrase before drift issues its first statement.
db.execute("PRAGMA key = '$masterKey';");
},
);
});
}

View File

@ -0,0 +1,13 @@
import 'package:drift/drift.dart';
/// Fallback for platforms without a drift backend.
///
/// This branch is never selected at runtime on the supported targets: native
/// platforms compile to `native.dart` (SQLCipher) and the web compiler picks
/// `web.dart` (WASM) through the conditional import in `app_database.dart`.
QueryExecutor openDatabaseConnection() {
throw UnimplementedError(
'Local persistence is only implemented for native (SQLCipher) '
'and web (WASM SQLite) platforms.',
);
}

View File

@ -0,0 +1,20 @@
import 'package:drift/drift.dart';
import 'package:drift/wasm.dart';
/// Opens the browser database via SQLite compiled to WebAssembly.
///
/// SQLCipher cannot run in the browser, so web builds persist an unencrypted
/// database in OPFS/IndexedDB. [WasmDatabase.open] expects `sqlite3.wasm`
/// (from the `sqlite3.dart` releases) and `drift_worker.js` (from the drift
/// releases) to be copied into the `web/` directory of the Flutter host
/// project.
QueryExecutor openDatabaseConnection() {
return LazyDatabase(() async {
final result = await WasmDatabase.open(
databaseName: 'glp1_companion',
sqlite3Uri: Uri.parse('sqlite3.wasm'),
driftWorkerUri: Uri.parse('drift_worker.js'),
);
return result.resolvedExecutor;
});
}

View File

@ -0,0 +1,16 @@
import 'package:supabase_flutter/supabase_flutter.dart';
/// Whether the global [Supabase] SDK has been initialized.
///
/// [Supabase.instance] throws an [AssertionError] in debug builds when
/// [Supabase.initialize] has not run (for example when the `.env` file is
/// missing or incomplete during local development). Routing guards and the
/// provider graph must not depend on the SDK being configured, so this helper
/// probes the singleton defensively and reports `false` instead of throwing.
bool get isSupabaseAvailable {
try {
return Supabase.instance.isInitialized;
} on AssertionError {
return false;
}
}

View File

@ -0,0 +1,77 @@
import 'package:flutter/foundation.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../domain/auth_service.dart';
/// Custom URL scheme used to bring the user back into the app after an OAuth
/// or magic-link round trip on native platforms.
///
/// The scheme mirrors the platform bundle identifiers
/// (`com.glpcompanion.glpcompanion`) and must be registered in the iOS/macOS
/// `Info.plist`, the Android manifest intent filters, and the Windows runner
/// so the operating system can route the redirect callback back to the app.
/// [SupabaseAuth] (bundled with `supabase_flutter`) observes the deep link and
/// exchanges the returned auth code for a session automatically.
const kAuthCallbackRedirectUrl =
'com.glpcompanion.glpcompanion://login-callback/';
/// Resolves the redirect URL embedded in OAuth and magic-link flows.
///
/// On web the browser returns to the current origin; native platforms use the
/// registered deep-link scheme above.
String resolveAuthRedirectUrl() {
return kIsWeb ? Uri.base.origin : kAuthCallbackRedirectUrl;
}
/// [IAuthService] implementation backed by Supabase GoTrue.
///
/// Only passwordless sign-in modes are modeled: federated OAuth (Google,
/// Apple, Microsoft Identity via Supabase's `azure` provider) and email magic
/// links. Direct username/password credentials are intentionally absent in
/// line with the platform's security posture.
class SupabaseAuthService implements IAuthService {
/// Creates the service. Pass [client] in tests to inject a mock; production
/// code resolves the global Supabase client lazily on first use.
SupabaseAuthService({SupabaseClient? client})
: _client = client ?? Supabase.instance.client;
final SupabaseClient _client;
@override
Future<void> signInWithGoogle() => _signInWithOAuth(OAuthProvider.google);
@override
Future<void> signInWithApple() => _signInWithOAuth(OAuthProvider.apple);
/// Microsoft Identity (Entra ID) is exposed by Supabase Auth as the
/// `azure` OAuth provider.
@override
Future<void> signInWithMicrosoft() => _signInWithOAuth(OAuthProvider.azure);
@override
Future<void> signInWithMagicLink(String email) async {
await _client.auth.signInWithOtp(
email: email,
emailRedirectTo: resolveAuthRedirectUrl(),
);
}
@override
Future<void> signOut() => _client.auth.signOut();
/// Emits the authenticated user's id on every auth transition and `null`
/// once the user is signed out.
@override
Stream<String?> get authStateChanges =>
_client.auth.onAuthStateChange.map((state) => state.session?.user.id);
Future<void> _signInWithOAuth(OAuthProvider provider) async {
final launched = await _client.auth.signInWithOAuth(
provider,
redirectTo: resolveAuthRedirectUrl(),
);
if (!launched) {
throw AuthException('Could not launch the ${provider.name} sign-in page.');
}
}
}

View File

@ -0,0 +1,65 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../../../../core/network/supabase_status.dart';
import '../../data/supabase_auth_service.dart';
import '../../domain/auth_service.dart';
/// Provides the application-wide [IAuthService].
///
/// The concrete [SupabaseAuthService] is resolved behind the domain contract
/// so widget/unit tests can override this provider with a fake without touching
/// the presentation layer.
final authServiceProvider = Provider<IAuthService>(
(ref) => SupabaseAuthService(),
);
/// Raw GoTrue auth transitions (sign-in, sign-out, token refresh, ...).
///
/// The router subscribes to this stream through `refreshListenable` so route
/// guards react immediately after an OAuth or magic-link round trip completes.
/// Falls back to an empty stream when the Supabase SDK has not been
/// initialized (e.g. missing `.env` during local development).
final authStateProvider = StreamProvider<AuthState>((ref) {
if (!isSupabaseAvailable) return const Stream<AuthState>.empty();
return Supabase.instance.client.auth.onAuthStateChange;
});
/// Orchestrates asynchronous passwordless authentication flows.
///
/// Every method transitions through [AsyncValue.loading] and records failures
/// in [state] as [AsyncValue.error], so the presentation layer only needs to
/// watch/listen to the provider to render progress and error feedback.
class AuthController extends StateNotifier<AsyncValue<void>> {
AuthController(this._authService) : super(const AsyncValue.data(null));
final IAuthService _authService;
/// Dispatches a passwordless OTP/magic link to [email].
Future<void> signInWithMagicLink(String email) =>
_run(() => _authService.signInWithMagicLink(email));
Future<void> signInWithGoogle() => _run(_authService.signInWithGoogle);
Future<void> signInWithApple() => _run(_authService.signInWithApple);
Future<void> signInWithMicrosoft() => _run(_authService.signInWithMicrosoft);
Future<void> signOut() => _run(_authService.signOut);
Future<void> _run(Future<void> Function() action) async {
state = const AsyncValue.loading();
try {
await action();
if (mounted) state = const AsyncValue.data(null);
} catch (error, stackTrace) {
if (mounted) state = AsyncValue.error(error, stackTrace);
}
}
}
/// Exposes the [AuthController] to the widget tree.
final authControllerProvider =
StateNotifierProvider<AuthController, AsyncValue<void>>(
(ref) => AuthController(ref.watch(authServiceProvider)),
);

View File

@ -1,19 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart' show AuthException;
class LoginScreen extends StatelessWidget {
import '../controllers/auth_controller.dart';
/// Which authentication flow is currently executing, so the relevant button
/// can render a spinner while the request is in flight.
enum _AuthAction { google, apple, microsoft, magicLink }
/// Passwordless sign-in surface: federated OAuth (Google, Apple, Microsoft)
/// and email magic links. Wired to [authControllerProvider] for execution and
/// progress/error feedback.
class LoginScreen extends ConsumerStatefulWidget {
const LoginScreen({super.key});
@override
ConsumerState<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends ConsumerState<LoginScreen> {
final TextEditingController _emailController = TextEditingController();
/// The action currently awaiting completion, or `null` when idle.
_AuthAction? _pendingAction;
static final RegExp _emailPattern = RegExp(
r"^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$",
);
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final emailController = TextEditingController();
// The controller records failures in its AsyncValue state instead of
// throwing, so a single listener surfaces every flow's error feedback.
ref.listen<AsyncValue<void>>(authControllerProvider, (previous, next) {
final error = next.error;
if (error == null) return;
_showSnack(_messageFor(error), isError: true);
});
final isBusy = ref.watch(authControllerProvider).isLoading;
return Scaffold(
body: Center(
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -32,23 +73,14 @@ class LoginScreen extends StatelessWidget {
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.g_mobiledata, size: 28),
label: const Text('Continue with Google'),
),
_oauthButton(_AuthAction.google, Icons.g_mobiledata,
'Continue with Google'),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.apple),
label: const Text('Continue with Apple'),
),
_oauthButton(
_AuthAction.apple, Icons.apple, 'Continue with Apple'),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.window),
label: const Text('Continue with Microsoft'),
),
_oauthButton(_AuthAction.microsoft, Icons.window,
'Continue with Microsoft'),
const SizedBox(height: 24),
const Row(
children: [
@ -62,7 +94,10 @@ class LoginScreen extends StatelessWidget {
),
const SizedBox(height: 24),
TextField(
controller: emailController,
controller: _emailController,
enabled: !isBusy,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
decoration: const InputDecoration(
labelText: 'Email Address',
border: OutlineInputBorder(),
@ -71,14 +106,110 @@ class LoginScreen extends StatelessWidget {
),
const SizedBox(height: 12),
FilledButton(
onPressed: () {},
child: const Text('Send Magic Link'),
onPressed: isBusy ? null : _sendMagicLink,
child: _pendingAction == _AuthAction.magicLink
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Send Magic Link'),
),
],
),
),
),
),
),
);
}
/// Builds an OAuth button bound to [action]; disabled while any flow runs
/// and swapped for a spinner while [action] itself is executing.
Widget _oauthButton(_AuthAction action, IconData icon, String label) {
final isBusy = ref.watch(authControllerProvider).isLoading;
return ElevatedButton.icon(
onPressed: isBusy ? null : () => _start(action, _runnerFor(action)),
icon: _pendingAction == action
? const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(icon),
label: Text(label),
);
}
/// Resolves the controller method backing [action].
Future<void> Function() _runnerFor(_AuthAction action) {
final notifier = ref.read(authControllerProvider.notifier);
return switch (action) {
_AuthAction.google => notifier.signInWithGoogle,
_AuthAction.apple => notifier.signInWithApple,
_AuthAction.microsoft => notifier.signInWithMicrosoft,
_AuthAction.magicLink =>
() => notifier.signInWithMagicLink(_emailController.text.trim()),
};
}
/// Runs [execute] while marking [action] as pending.
///
/// A completed flow without a recorded error counts as success; optional
/// [successMessage] is surfaced via SnackBar. Errors are reported by the
/// listener in [build], so they are not repeated here.
Future<void> _start(
_AuthAction action,
Future<void> Function() execute, {
String? successMessage,
}) async {
if (_pendingAction != null) return;
setState(() => _pendingAction = action);
try {
await execute();
if (!mounted) return;
final result = ref.read(authControllerProvider);
if (!result.hasError && successMessage != null) {
_showSnack(successMessage);
}
} finally {
if (mounted) setState(() => _pendingAction = null);
}
}
Future<void> _sendMagicLink() async {
final email = _emailController.text.trim();
if (!_emailPattern.hasMatch(email)) {
_showSnack(
'Enter a valid email address to receive a magic link.',
isError: true,
);
return;
}
await _start(
_AuthAction.magicLink,
_runnerFor(_AuthAction.magicLink),
successMessage:
'Magic link sent to $email. Check your inbox to finish signing in.',
);
}
void _showSnack(String message, {bool isError = false}) {
if (!mounted) return;
final messenger = ScaffoldMessenger.of(context);
messenger
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Theme.of(context).colorScheme.error : null,
),
);
}
String _messageFor(Object error) {
if (error is AuthException) return error.message;
return 'Authentication failed. Please try again.';
}
}

View File

@ -2,11 +2,33 @@ import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'app/router/app_router.dart';
void main() async {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Load environment variables from .env
await dotenv.load(fileName: '.env');
final supabaseUrl = dotenv.env['SUPABASE_URL'] ?? '';
final supabaseKey = dotenv.env['SUPABASE_PUBLISHABLE_KEY'] ??
dotenv.env['SUPABASE_ANON_KEY'] ??
'';
if (supabaseUrl.isNotEmpty && supabaseKey.isNotEmpty) {
await Supabase.initialize(
url: supabaseUrl,
publishableKey: supabaseKey,
);
} else {
debugPrint(
'WARNING: SUPABASE_URL or SUPABASE_PUBLISHABLE_KEY missing in .env. Supabase client uninitialized.',
);
}
runApp(const ProviderScope(child: GLP1CompanionApp()));
}

View File

@ -5,14 +5,18 @@
import FlutterMacOS
import Foundation
import app_links
import device_info_plus
import flutter_local_notifications
import flutter_secure_storage_macos
import sqlite3_flutter_libs
import shared_preferences_foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@ -5,26 +5,50 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
sha256: "48e48f73a431eb269eec66d243a63a07ed90ef9417a74047a3b98d52338626ad"
url: "https://pub.dev"
source: hosted
version: "85.0.0"
version: "109.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c
sha256: "6d377901d198536b648172974d9d4f277910502cad92193466869cac0ff34990"
url: "https://pub.dev"
source: hosted
version: "7.6.0"
analyzer_plugin:
version: "14.5.0"
app_links:
dependency: transitive
description:
name: analyzer_plugin
sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce
name: app_links
sha256: "08ba88bf19d9edea33130cb71c8ced2cb982b4e00b82efef4a434d1357bc69c3"
url: "https://pub.dev"
source: hosted
version: "0.13.4"
version: "7.2.2"
app_links_linux:
dependency: transitive
description:
name: app_links_linux
sha256: "9ccb5802085a1e57bb07b6256fd1d61a78c7ec104944d4965e62938b4fefd9cb"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
app_links_platform_interface:
dependency: transitive
description:
name: app_links_platform_interface
sha256: "7546f09a6e93f4a2df2fe2bd40a5c6c64310ac461b036d82b43033be7a59f809"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
app_links_web:
dependency: transitive
description:
name: app_links_web
sha256: "382d32c6f486f15b1d7f5d6a37a70a481a4fc08382eef419d373169921a3e765"
url: "https://pub.dev"
source: hosted
version: "1.0.5"
args:
dependency: transitive
description:
@ -53,18 +77,18 @@ packages:
dependency: transitive
description:
name: build
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
sha256: "8a5c5761af8e31748bba3c82f68925ace40f3225c3eea25be9beb57eca7cd7a8"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
version: "4.0.11"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
sha256: d466ed2dc9c6cd1d169948879b84ee061eb5e22c64a7c6089879c6296d272a8d
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.3.3"
build_daemon:
dependency: transitive
description:
@ -73,30 +97,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.7"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
sha256: "6feadd32d90a68253c1f7b4fcff76adb4b41ad32a53f8bad835dc65f01ac9dc5"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
url: "https://pub.dev"
source: hosted
version: "9.1.2"
version: "2.16.2"
built_collection:
dependency: transitive
description:
@ -169,14 +177,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "811709c1badd02fc054039a13f66e0db1961865f9a314175f68c3eaeebec786f"
url: "https://pub.dev"
source: hosted
version: "4.13.0"
collection:
dependency: transitive
description:
@ -217,30 +217,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.9"
custom_lint_core:
dart_jsonwebtoken:
dependency: transitive
description:
name: custom_lint_core
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
name: dart_jsonwebtoken
sha256: ad84e60181696513d04d5f2078e0bbc20365b911f46f647797317414bdc88fbe
url: "https://pub.dev"
source: hosted
version: "0.7.5"
custom_lint_visitor:
dependency: transitive
description:
name: custom_lint_visitor
sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2"
url: "https://pub.dev"
source: hosted
version: "1.0.0+7.7.0"
version: "3.4.1"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
sha256: a0e4f65339b31eb6a9e6a12e14b4682a48d46fbc54a82f4a702396fbdbac63a7
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.14"
dbus:
dependency: transitive
description:
@ -269,26 +261,26 @@ packages:
dependency: "direct main"
description:
name: drift
sha256: "540cf382a3bfa99b76e51514db5b0ebcd81ce3679b7c1c9cb9478ff3735e47a1"
sha256: ef9598e1785c18db37209e8554fc79aff94632209d384ec98984b9e3184af0f7
url: "https://pub.dev"
source: hosted
version: "2.28.2"
version: "2.35.2"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: "68c138e884527d2bd61df2ade276c3a144df84d1adeb0ab8f3196b5afe021bd4"
sha256: e0d16aaa21f3408cbe2dd4bfc56c36cea43e53a537e6613b91380c0924b3bd30
url: "https://pub.dev"
source: hosted
version: "2.28.0"
version: "2.35.1"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: "0bc2f1dde59e59cedde0df67a4ff7bd8f0d42274f18b50bf7e7dae7ca3d77801"
sha256: "91acf4bee7c3c84467cba46455aa70e5292a3b889f4582645d74f2e5a8c106f2"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
version: "0.3.1"
fake_async:
dependency: transitive
description:
@ -326,6 +318,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_dotenv:
dependency: "direct main"
description:
name: flutter_dotenv
sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b
url: "https://pub.dev"
source: hosted
version: "5.2.1"
flutter_lints:
dependency: "direct dev"
description:
@ -429,22 +429,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
freezed_annotation:
functions_client:
dependency: transitive
description:
name: freezed_annotation
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
name: functions_client
sha256: "931e822688d2e97867b2300e2fdbb08c9db73f8653fc83efc7a259d2e0cca769"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: "404dbf9bce29beafbe9dcb19219447b6f7d49900cd42b8d3b7af102b24508e25"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "2.7.1"
glob:
dependency: transitive
description:
@ -461,6 +453,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "14.8.1"
gotrue:
dependency: transitive
description:
name: gotrue
sha256: cca816226824160e40d149816fd89740cc64dda1eaa3d9161e334330b18bba89
url: "https://pub.dev"
source: hosted
version: "2.27.3"
graphs:
dependency: transitive
description:
@ -645,6 +645,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "90eaad8059ebc2004a63810a4696ea9acdb02473e47ed3b6b3bafb100ac77e31"
url: "https://pub.dev"
source: hosted
version: "0.19.5"
objective_c:
dependency: transitive
description:
@ -661,8 +669,16 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
passkeys_platform_interface:
dependency: transitive
description:
name: passkeys_platform_interface
sha256: "5fca044a6f1a3c5abff645ca5a5417b613c130d1c4e08513ba9182c7f4834619"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
path:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
@ -670,7 +686,7 @@ packages:
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
dependency: "direct main"
description:
name: path_provider
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
@ -741,6 +757,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
pool:
dependency: transitive
description:
@ -749,6 +773,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.3"
postgrest:
dependency: transitive
description:
name: postgrest
sha256: b7f957273ba6a1c26cefa987ac524f4138e9ab7542fbb9e6abdfddacb9895a21
url: "https://pub.dev"
source: hosted
version: "2.9.1"
process:
dependency: transitive
description:
name: process
sha256: "4242ba3508d37e01808bdf71ad1d5bb93a8d671bf2e7450e6b1b353fb0808891"
url: "https://pub.dev"
source: hosted
version: "5.0.6"
pub_semver:
dependency: transitive
description:
@ -765,6 +805,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.6.0"
realtime_client:
dependency: transitive
description:
name: realtime_client
sha256: "2ef680d83ba2c0f76019d16194ca7067fa2458b2d4091b52323a9ca4a964fd40"
url: "https://pub.dev"
source: hosted
version: "2.13.2"
recase:
dependency: transitive
description:
@ -789,14 +837,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.6.1"
riverpod_analyzer_utils:
dependency: transitive
description:
name: riverpod_analyzer_utils
sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611"
url: "https://pub.dev"
source: hosted
version: "0.5.10"
riverpod_annotation:
dependency: "direct main"
description:
@ -805,14 +845,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.6.1"
riverpod_generator:
dependency: "direct dev"
shared_preferences:
dependency: transitive
description:
name: riverpod_generator
sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36"
name: shared_preferences
sha256: "1e85e0dccf5b4a016077fea7dfbb4a7cb39c759ac1596fcf60c06deadb7da91f"
url: "https://pub.dev"
source: hosted
version: "2.6.5"
version: "2.5.6"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "1e12aafe408aa50da80edfd679a2a6bf63ba7ab37c7fa98286da459a757b3399"
url: "https://pub.dev"
source: hosted
version: "2.4.28"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea"
url: "https://pub.dev"
source: hosted
version: "2.5.7"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:
@ -838,10 +926,10 @@ packages:
dependency: transitive
description:
name: source_gen
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
sha256: "833ea7d15b5be79949d3d6c077da0267a0bd93c16897b36a771395a91e63086e"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "4.3.1"
source_span:
dependency: transitive
description:
@ -850,30 +938,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.2"
sqlite3:
sqlcipher_flutter_libs:
dependency: transitive
description:
name: sqlite3
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2"
name: sqlcipher_flutter_libs
sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929"
url: "https://pub.dev"
source: hosted
version: "2.9.4"
version: "0.7.0+eol"
sqlite3:
dependency: "direct main"
description:
name: sqlite3
sha256: "144132790ec1947fad9f3ddceffd0af2ab80a1f0d1e81a4d094ee6707ec3df0b"
url: "https://pub.dev"
source: hosted
version: "3.7.1"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad
sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454"
url: "https://pub.dev"
source: hosted
version: "0.5.42"
version: "0.6.0+eol"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: "57090342af1ce32bb499aa641f4ecdd2d6231b9403cea537ac059e803cc20d67"
sha256: "772bb2f6f5bce0631a60f26b57d6e8882e1105c22345b05c163ee6ee5d7ba32d"
url: "https://pub.dev"
source: hosted
version: "0.41.2"
version: "0.45.0"
stack_trace:
dependency: transitive
description:
@ -890,6 +986,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
storage_client:
dependency: transitive
description:
name: storage_client
sha256: b951281b8e2b293460f0e6cc4e866f2546681390f0e37d24fbbd7b4a0deb27a3
url: "https://pub.dev"
source: hosted
version: "2.8.1"
stream_channel:
dependency: transitive
description:
@ -914,6 +1018,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
supabase:
dependency: transitive
description:
name: supabase
sha256: "123fe08792b24e5e7c4b9a01145de8e95ebaa9183cd5f3c9d48dd2ff890f28a6"
url: "https://pub.dev"
source: hosted
version: "2.16.5"
supabase_common:
dependency: transitive
description:
name: supabase_common
sha256: a7961a44953cf8d9da049381fc3377516348255042fbb12b9d4f8540816f2fe9
url: "https://pub.dev"
source: hosted
version: "0.1.2"
supabase_flutter:
dependency: "direct main"
description:
name: supabase_flutter
sha256: "48bbde226e68bdb293020859a1bfa42d0f3bd2239992fba0ec4f9194cbb842e6"
url: "https://pub.dev"
source: hosted
version: "2.18.3"
term_glyph:
dependency: transitive
description:
@ -938,14 +1066,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.9.4"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
typed_data:
dependency: transitive
description:
@ -954,14 +1074,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
url_launcher:
dependency: transitive
description:
name: uuid
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
name: url_launcher
sha256: e8cc955c97e06df9879140a1fe301f3d493f01b7c62dec7f46834be31d7e9fda
url: "https://pub.dev"
source: hosted
version: "4.6.0"
version: "6.3.3"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610"
url: "https://pub.dev"
source: hosted
version: "6.3.33"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a"
url: "https://pub.dev"
source: hosted
version: "6.4.2"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0"
url: "https://pub.dev"
source: hosted
version: "3.2.3"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201"
url: "https://pub.dev"
source: hosted
version: "3.2.6"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
vector_math:
dependency: transitive
description:
@ -1050,6 +1226,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.4"
yaml_edit:
dependency: transitive
description:
name: yaml_edit
sha256: "07c9e63ba42519745182b88ca12264a7ba2484d8239958778dfe4d44fe760488"
url: "https://pub.dev"
source: hosted
version: "2.2.4"
yet_another_json_isolate:
dependency: transitive
description:
name: yet_another_json_isolate
sha256: eaa26beb5990b25a49d942374fd5a0c5aa67a837e03b14b4c26134aaa1ed01a9
url: "https://pub.dev"
source: hosted
version: "2.1.1"
sdks:
dart: ">=3.13.0 <4.0.0"
flutter: ">=3.38.4"
flutter: ">=3.44.0"

View File

@ -28,21 +28,42 @@ dependencies:
html: ^0.15.4
# Persistence & Secure Storage
drift: ^2.16.0
drift_flutter: ^0.1.0
# drift 2.35+ pairs with package:sqlite3 3.x, which bundles SQLite through
# build hooks (see `hooks:` below) instead of the retired platform plugins.
drift: ^2.35.2
drift_flutter: ^0.3.1
flutter_secure_storage: ^9.0.0
# Multi-Platform Health & Background Tasks
health: 13.3.1
flutter_local_notifications: ^17.1.0
flutter_dotenv: ^5.1.0
supabase_flutter: ^2.5.0
path: ^1.9.1
path_provider: ^2.1.6
sqlite3: ^3.4.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
build_runner: ^2.4.9
riverpod_generator: ^2.4.0
drift_dev: ^2.16.0
# NOTE: riverpod_generator was removed because the 2.x code-generation line
# caps `analyzer <8.0.0`, which cannot parse the Dart 3.13 SDK and crashes
# build_runner. All providers in this codebase are declared manually with
# `Provider`/`StateNotifierProvider`; re-adding codegen requires the
# Riverpod 3 migration.
build_runner: ^2.16.0
drift_dev: ^2.35.1
# Compile SQLCipher into the SQLite engine bundled by package:sqlite3's build
# hooks (the sqlcipher_flutter_libs plugin is retired on sqlite3 3.x).
hooks:
user_defines:
sqlite3:
source: sqlcipher
flutter:
uses-material-design: true
assets:
- .env

View File

@ -6,12 +6,15 @@
#include "generated_plugin_registrant.h"
#include <app_links/app_links_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -3,8 +3,9 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
app_links
flutter_secure_storage_windows
sqlite3_flutter_libs
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST