Skip to content
Merged
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
3 changes: 2 additions & 1 deletion app/integration_test/powersync_books_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ void main() {
PapyrusPowerSyncService service(AuthRepository repository, String label) {
return PapyrusPowerSyncService(
connectorFactory: () => PapyrusPowerSyncConnector(authRepository: repository, config: config),
pathResolver: (mode) async => path.join(root.path, '$label-${mode.name}.db'),
pathResolver: (mode, profileKey, userId) async =>
path.join(root.path, '$label-${mode.name}-${profileKey ?? 'default'}-${userId ?? 'none'}.db'),
);
}

Expand Down
19 changes: 15 additions & 4 deletions app/lib/auth/token_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,30 @@ class SecureRefreshTokenStorage implements RefreshTokenStorage {
static const _refreshTokenKey = 'papyrus_refresh_token';

final FlutterSecureStorage _storage;
final String namespace;

const SecureRefreshTokenStorage([this._storage = const FlutterSecureStorage()]);
const SecureRefreshTokenStorage([this._storage = const FlutterSecureStorage()]) : namespace = 'default';

const SecureRefreshTokenStorage.scoped(this.namespace, [this._storage = const FlutterSecureStorage()]);

String get _storageKey {
if (namespace == 'default') {
return _refreshTokenKey;
}

return '$_refreshTokenKey.$namespace';
}

@override
Future<String?> read() => _storage.read(key: _refreshTokenKey);
Future<String?> read() => _storage.read(key: _storageKey);

@override
Future<void> write(String refreshToken) {
return _storage.write(key: _refreshTokenKey, value: refreshToken);
return _storage.write(key: _storageKey, value: refreshToken);
}

@override
Future<void> delete() => _storage.delete(key: _refreshTokenKey);
Future<void> delete() => _storage.delete(key: _storageKey);
}

class TokenStore {
Expand Down
61 changes: 50 additions & 11 deletions app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:papyrus/powersync/sync_state.dart';
import 'package:papyrus/providers/auth_provider.dart';
import 'package:papyrus/providers/library_provider.dart';
import 'package:papyrus/providers/preferences_provider.dart';
import 'package:papyrus/providers/sync_settings_provider.dart';
import 'package:papyrus/providers/sidebar_provider.dart';
import 'package:papyrus/themes/app_theme.dart';
import 'package:provider/provider.dart';
Expand Down Expand Up @@ -40,39 +41,54 @@ class Papyrus extends StatefulWidget {
class _PapyrusState extends State<Papyrus> {
late final DataStore _dataStore;
late final AuthProvider _authProvider;
late final SyncSettingsProvider _syncSettingsProvider;
late final PapyrusPowerSyncService _powerSyncService;
late final PapyrusApiConfig _officialApiConfig;
late AuthRepository _authRepository;
late String _activeProfileKey;
late final AppRouter _appRouter;
bool _switchingSyncProfile = false;

@override
void initState() {
super.initState();

final apiConfig = PapyrusApiConfig.fromEnvironment();
final tokenStore = TokenStore(const SecureRefreshTokenStorage());
final authRepository = AuthRepository(
apiClient: AuthApiClient(config: apiConfig),
tokenStore: tokenStore,
);
_officialApiConfig = PapyrusApiConfig.fromEnvironment();
_syncSettingsProvider = SyncSettingsProvider(widget.prefs, officialConfig: _officialApiConfig);
_activeProfileKey = _syncSettingsProvider.activeProfileKey;
_authRepository = _buildAuthRepository(_syncSettingsProvider.activeApiConfig, _activeProfileKey);

_dataStore = DataStore();
_authProvider = AuthProvider(widget.prefs, repository: authRepository);
_authProvider = AuthProvider(widget.prefs, repository: _authRepository);
_powerSyncService = PapyrusPowerSyncService(
connectorFactory: () => PapyrusPowerSyncConnector(authRepository: authRepository, config: apiConfig),
connectorFactory: () =>
PapyrusPowerSyncConnector(authRepository: _authRepository, config: _syncSettingsProvider.activeApiConfig),
);
unawaited(_dataStore.attachBookRepository(_powerSyncService));
_appRouter = AppRouter(authProvider: _authProvider);
_authProvider.addListener(_syncPowerSyncAuthState);
_syncSettingsProvider.addListener(_handleSyncSettingsChanged);
_syncPowerSyncAuthState();
}

@override
void dispose() {
_authProvider.removeListener(_syncPowerSyncAuthState);
_syncSettingsProvider.removeListener(_handleSyncSettingsChanged);
unawaited(_disposeDataServices());
_authProvider.dispose();
_syncSettingsProvider.dispose();
super.dispose();
}

AuthRepository _buildAuthRepository(PapyrusApiConfig config, String profileKey) {
final tokenStore = TokenStore(SecureRefreshTokenStorage.scoped(profileKey));
return AuthRepository(
apiClient: AuthApiClient(config: config),
tokenStore: tokenStore,
);
}

Future<void> _disposeDataServices() async {
await _dataStore.disposeBookRepository();
await _powerSyncService.close();
Expand All @@ -82,7 +98,7 @@ class _PapyrusState extends State<Papyrus> {
final user = _authProvider.user;
if (user != null && !_authProvider.isOfflineMode) {
final userId = user.userId;
unawaited(_powerSyncService.activateAuthenticated(userId));
unawaited(_powerSyncService.activateAuthenticated(userId, profileKey: _activeProfileKey));
return;
}

Expand All @@ -91,8 +107,30 @@ class _PapyrusState extends State<Papyrus> {
return;
}

if (!_authProvider.isBootstrapping) {
unawaited(_powerSyncService.deactivate());
if (!_authProvider.isBootstrapping && _powerSyncService.mode != null) {
unawaited(_powerSyncService.deactivate(clearAuthenticated: !_switchingSyncProfile));
}
}

void _handleSyncSettingsChanged() {
final nextProfileKey = _syncSettingsProvider.activeProfileKey;
if (nextProfileKey == _activeProfileKey) {
return;
}

_activeProfileKey = nextProfileKey;
unawaited(_switchActiveSyncProfile());
}

Future<void> _switchActiveSyncProfile() async {
_switchingSyncProfile = true;
try {
await _powerSyncService.deactivate(clearAuthenticated: false);
_authRepository = _buildAuthRepository(_syncSettingsProvider.activeApiConfig, _activeProfileKey);
await _authProvider.replaceRepository(_authRepository, bootstrapNewRepository: !_authProvider.isOfflineMode);
} finally {
_switchingSyncProfile = false;
_syncPowerSyncAuthState();
}
}
Comment on lines +115 to 135

Expand All @@ -102,6 +140,7 @@ class _PapyrusState extends State<Papyrus> {
providers: [
// Core data store - single source of truth
ChangeNotifierProvider.value(value: _dataStore),
ChangeNotifierProvider.value(value: _syncSettingsProvider),
Provider.value(value: _powerSyncService),
StreamProvider<SyncState>.value(value: _powerSyncService.syncStates, initialData: _powerSyncService.syncState),
// Auth and UI state providers
Expand Down
28 changes: 0 additions & 28 deletions app/lib/pages/developer_options_page.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:papyrus/providers/preferences_provider.dart';
import 'package:papyrus/themes/design_tokens.dart';
import 'package:papyrus/widgets/settings/settings_row.dart';
import 'package:papyrus/widgets/settings/settings_section.dart';
import 'package:provider/provider.dart';

/// Developer options page with debug-only settings.
Expand Down Expand Up @@ -72,30 +70,4 @@ class DeveloperOptionsPage extends StatelessWidget {
),
);
}

Widget _buildEinkToggle(bool isOn) {
return Container(
width: 56,
height: 32,
decoration: BoxDecoration(
border: Border.all(color: Colors.black, width: BorderWidths.einkDefault),
),
child: Row(
children: [
Expanded(
child: Container(
color: isOn ? Colors.black : Colors.white,
child: Center(child: isOn ? const Icon(Icons.check, color: Colors.white, size: 16) : null),
),
),
Expanded(
child: Container(
color: isOn ? Colors.white : Colors.black,
child: Center(child: !isOn ? const Icon(Icons.close, color: Colors.white, size: 16) : null),
),
),
],
),
);
}
}
Loading
Loading