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: 0 additions & 3 deletions .github/workflows/auto-assign.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ name: Auto Assign
on:
issues:
types: [opened]
pull_request:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
Expand All @@ -16,4 +14,3 @@ jobs:
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
assignees: Eoic
numOfAssignee: 1
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ on:
push:
branches: [master, main]
paths:
- 'client/**'
- 'app/**'
- '.github/workflows/**'
pull_request:
branches: [master, main]
paths:
- 'client/**'
- 'app/**'
- '.github/workflows/**'
workflow_dispatch:

Expand All @@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
defaults:
run:
working-directory: client
working-directory: app

steps:
- name: Checkout repository
Expand All @@ -45,14 +45,14 @@ jobs:
run: dart format --set-exit-if-changed .

- name: Analyze code
run: dart analyze
run: flutter analyze --no-fatal-warnings --no-fatal-infos

- name: Run tests with coverage
run: flutter test --coverage

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: client/coverage/lcov.info
files: app/coverage/lcov.info
flags: client
token: ${{ secrets.CODECOV_TOKEN }}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ firebase-debug.log
.worktrees/
landing/node_modules/
landing/css/
test/data/
test/data/
.idea/
23 changes: 16 additions & 7 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
{
"version": "2.0.0",
"options": {
"cwd": "${workspaceFolder}/app/"
},
"tasks": [
{
"label": "Papyrus (web)",
"type": "shell",
"command": "flutter run",
"command": "flutter",
"args": [
"run",
"-d",
"chrome",
"--dart-define-from-file",
".dart_defines"
".dart_defines",
"--web-port",
"3000"
]
},
{
"label": "Papyrus (android)",
"type": "shell",
"command": "flutter run",
"command": "flutter",
"args": [
"run",
"-d",
"android",
"--dart-define-from-file",
Expand All @@ -26,21 +33,23 @@
{
"label": "Papyrus (iOS)",
"type": "shell",
"command": "flutter run",
"command": "flutter",
"args": [
"run",
"-d",
"ios",
"--dart-define-from-file",
".dart_defines"
]
},
{
"label": "Papyrus (desktop)",
"label": "Papyrus (Linux)",
"type": "shell",
"command": "flutter run",
"command": "flutter",
"args": [
"run",
"-d",
"desktop",
"linux",
"--dart-define-from-file",
".dart_defines"
]
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Many reading applications offer partial solutions but fall short on essential fe
2. **Install dependencies**

```bash
cd client
cd app
flutter pub get
```

Expand All @@ -87,6 +87,34 @@ Many reading applications offer partial solutions but fall short on essential fe
flutter run -d windows # or: macos, linux
```

### Running with the back-end

When the user signs in, the Flutter client communicates with the Papyrus server for auth and asks the server for PowerSync credentials.

Run the server and PowerSync locally, then start Flutter with:

```bash
cd ../server
./scripts/bootstrap_local.sh
cd ../client/app
flutter run -d chrome --web-hostname papyrus.localhost --web-port 3000 --dart-define-from-file=.dart_defines
```

Authenticated books use `papyrus-account.db` and synchronize through
PowerSync. Guest mode uses the separate local-only `papyrus-guest.db`; guest
books remain on that device and are not merged into an account.

For local web auth links, add these entries to `/etc/hosts`:

```text
127.0.0.1 papyrus.localhost
::1 papyrus.localhost
```

See
[`server/docs/flutter-auth-integration.md`](../server/docs/flutter-auth-integration.md)
for the full integration guide.

## Documentation

See [PapyrusReader/docs](https://github.com/PapyrusReader/docs).
Expand Down
4 changes: 2 additions & 2 deletions app/.dart_defines.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"SUPABASE_URL": "https://your-project-ref.supabase.co",
"SUPABASE_ANON_KEY": "your-anon-key"
"PAPYRUS_API_BASE_URL": "http://papyrus.localhost:8080",
"POWERSYNC_SERVICE_URL": "http://localhost:8081"
}
5 changes: 4 additions & 1 deletion app/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
include: package:flutter_lints/flutter.yaml

linter:
rules:
rules:
lines_longer_than_80_chars: false
formatter:
page_width: 120
10 changes: 10 additions & 0 deletions app/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
android:exported="true">
<intent-filter android:label="flutter_web_auth_2">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="papyrus" android:host="auth" android:path="/callback" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
Expand Down
2 changes: 0 additions & 2 deletions app/android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Web OAuth client ID — used by google_sign_in_android to request an ID token -->
<string name="default_web_client_id">856619753967-uks4vdv502s0kqd8017e7d6b4pvou8jo.apps.googleusercontent.com</string>
</resources>
121 changes: 121 additions & 0 deletions app/integration_test/powersync_books_integration_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:papyrus/auth/auth_api_client.dart';
import 'package:papyrus/auth/auth_repository.dart';
import 'package:papyrus/auth/papyrus_api_config.dart';
import 'package:papyrus/auth/token_store.dart';
import 'package:papyrus/models/book.dart';
import 'package:papyrus/powersync/papyrus_powersync_connector.dart';
import 'package:papyrus/powersync/powersync_service.dart';
import 'package:path/path.dart' as path;
import 'package:uuid/uuid.dart';

const runIntegration = bool.fromEnvironment('RUN_POWERSYNC_INTEGRATION');

class MemoryRefreshTokenStorage implements RefreshTokenStorage {
String? value;

@override
Future<void> delete() async => value = null;

@override
Future<String?> read() async => value;

@override
Future<void> write(String refreshToken) async => value = refreshToken;
}

Future<void> waitForBook(PapyrusPowerSyncService service, String id, {bool present = true, String? title}) async {
final deadline = DateTime.now().add(const Duration(seconds: 20));
while (DateTime.now().isBefore(deadline)) {
final book = await service.getById(id);
if ((book != null) == present && (title == null || book?.title == title)) {
return;
}
await Future<void>.delayed(const Duration(milliseconds: 200));
}
fail('Book $id did not reach expected presence=$present');
}

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

testWidgets('two clients sync books, reconnect offline writes, and isolate users', (tester) async {
final config = PapyrusApiConfig.fromEnvironment();
final email = 'powersync-${DateTime.now().microsecondsSinceEpoch}@example.com';
const password = 'SecureP@ss123';
final root = await Directory.systemTemp.createTemp('papyrus-e2e-');

AuthRepository repository(String label) {
return AuthRepository(
apiClient: AuthApiClient(config: config),
tokenStore: TokenStore(MemoryRefreshTokenStorage()),
);
}

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'),
);
}

final firstAuth = repository('first');
final firstTokens = await firstAuth.register(
email: email,
password: password,
displayName: 'PowerSync Test',
clientType: 'desktop',
);
final secondAuth = repository('second');
await secondAuth.login(email: email, password: password, clientType: 'desktop');
final otherAuth = repository('other');
final otherTokens = await otherAuth.register(
email: 'other-$email',
password: password,
displayName: 'Other User',
clientType: 'desktop',
);

final first = service(firstAuth, 'first');
final second = service(secondAuth, 'second');
final other = service(otherAuth, 'other');

try {
await Future.wait([
first.activateAuthenticated(firstTokens.user.userId),
second.activateAuthenticated(firstTokens.user.userId),
other.activateAuthenticated(otherTokens.user.userId),
]);
await Future.wait([
first.syncStates.firstWhere((state) => state.connected),
second.syncStates.firstWhere((state) => state.connected),
other.syncStates.firstWhere((state) => state.connected),
]).timeout(const Duration(seconds: 20));

final book = Book(
id: const Uuid().v4(),
title: 'Realtime book',
author: 'Papyrus',
addedAt: DateTime.now().toUtc(),
);
await first.upsert(book);
await waitForBook(second, book.id, title: book.title);
expect(await other.getById(book.id), isNull);

await first.setOnline(false);
await first.upsert(book.copyWith(title: 'Offline edit'));
await first.setOnline(true);
await waitForBook(second, book.id, title: 'Offline edit');
expect((await second.getById(book.id))?.title, 'Offline edit');

await second.delete(book.id);
await waitForBook(first, book.id, present: false);
} finally {
await Future.wait([first.close(), second.close(), other.close()]);
await root.delete(recursive: true);
}
}, skip: !runIntegration);
}
2 changes: 1 addition & 1 deletion app/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.856619753967-c7gsj5tujnukf8ku4a3kk8p8ttdt7qk3</string>
<string>papyrus</string>
</array>
</dict>
</array>
Expand Down
Loading
Loading