Skip to content

Commit f6291a0

Browse files
authored
Merge pull request #500 from DevKor-github/codexd/add-privacy-policy-link
feat: add in-app privacy policy link
2 parents 6c33ee4 + d3f052f commit f6291a0

8 files changed

Lines changed: 287 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
final class ExternalLinks {
2+
static final privacyPolicyUri = Uri.parse(
3+
'https://ontime-back.duckdns.org/privacy-policy',
4+
);
5+
}

lib/l10n/app_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,14 @@
327327
"@allowAppNotifications": {
328328
"description": "Setting tile for allowing app notifications"
329329
},
330+
"privacyPolicy": "Privacy Policy",
331+
"@privacyPolicy": {
332+
"description": "Setting tile for opening the privacy policy"
333+
},
334+
"privacyPolicyOpenError": "Could not open the privacy policy. Please try again later.",
335+
"@privacyPolicyOpenError": {
336+
"description": "Dialog message shown when the privacy policy link cannot be opened"
337+
},
330338
"logOut": "Log out",
331339
"@logOut": {
332340
"description": "Setting tile for logging out"

lib/l10n/app_ko.arb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@
7676
"accountSettings": "계정 설정",
7777
"editDefaultPreparation": "기본 준비과정 / 여유시간 수정",
7878
"allowAppNotifications": "앱 알림 허용",
79+
"privacyPolicy": "개인정보 처리방침",
80+
"privacyPolicyOpenError": "개인정보 처리방침을 열 수 없습니다. 잠시 후 다시 시도해주세요.",
7981
"logOut": "로그아웃",
8082
"deleteAccount": "회원 탈퇴",
8183
"editSpareTime": "여유시간 수정",

lib/l10n/app_localizations.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,18 @@ abstract class AppLocalizations {
560560
/// **'Allow App Notifications'**
561561
String get allowAppNotifications;
562562

563+
/// Setting tile for opening the privacy policy
564+
///
565+
/// In en, this message translates to:
566+
/// **'Privacy Policy'**
567+
String get privacyPolicy;
568+
569+
/// Dialog message shown when the privacy policy link cannot be opened
570+
///
571+
/// In en, this message translates to:
572+
/// **'Could not open the privacy policy. Please try again later.'**
573+
String get privacyPolicyOpenError;
574+
563575
/// Setting tile for logging out
564576
///
565577
/// In en, this message translates to:

lib/l10n/app_localizations_en.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,13 @@ class AppLocalizationsEn extends AppLocalizations {
270270
@override
271271
String get allowAppNotifications => 'Allow App Notifications';
272272

273+
@override
274+
String get privacyPolicy => 'Privacy Policy';
275+
276+
@override
277+
String get privacyPolicyOpenError =>
278+
'Could not open the privacy policy. Please try again later.';
279+
273280
@override
274281
String get logOut => 'Log out';
275282

lib/l10n/app_localizations_ko.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,12 @@ class AppLocalizationsKo extends AppLocalizations {
251251
@override
252252
String get allowAppNotifications => '앱 알림 허용';
253253

254+
@override
255+
String get privacyPolicy => '개인정보 처리방침';
256+
257+
@override
258+
String get privacyPolicyOpenError => '개인정보 처리방침을 열 수 없습니다. 잠시 후 다시 시도해주세요.';
259+
254260
@override
255261
String get logOut => '로그아웃';
256262

lib/presentation/my_page/my_page_screen.dart

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import 'package:firebase_messaging/firebase_messaging.dart';
22
import 'package:flutter/material.dart';
33
import 'package:flutter_bloc/flutter_bloc.dart';
44
import 'package:go_router/go_router.dart';
5+
import 'package:url_launcher/url_launcher.dart';
6+
import 'package:on_time_front/core/constants/external_links.dart';
57
import 'package:on_time_front/core/di/di_setup.dart';
68
import 'package:on_time_front/core/services/alarm_scheduler_service.dart';
79
import 'package:on_time_front/core/services/fallback_alarm_notification_service.dart';
@@ -19,8 +21,13 @@ import 'package:on_time_front/presentation/my_page/my_page_modal/logout_modal.da
1921
import 'package:on_time_front/presentation/shared/components/modal_wide_button.dart';
2022
import 'package:on_time_front/presentation/shared/components/two_action_dialog.dart';
2123

24+
typedef PrivacyPolicyLauncher = Future<bool> Function(Uri uri);
25+
2226
class MyPageScreen extends StatelessWidget {
23-
const MyPageScreen({super.key});
27+
const MyPageScreen({super.key, PrivacyPolicyLauncher? openPrivacyPolicy})
28+
: _openPrivacyPolicy = openPrivacyPolicy;
29+
30+
final PrivacyPolicyLauncher? _openPrivacyPolicy;
2431

2532
@override
2633
Widget build(BuildContext context) {
@@ -91,6 +98,15 @@ class MyPageScreen extends StatelessWidget {
9198
await _handleNotificationPermission(context);
9299
},
93100
),
101+
_SettingTile(
102+
title: AppLocalizations.of(context)!.privacyPolicy,
103+
onTap: () async {
104+
await _handlePrivacyPolicyTap(
105+
context,
106+
_openPrivacyPolicy ?? _openPrivacyPolicyExternally,
107+
);
108+
},
109+
),
94110
],
95111
),
96112
),
@@ -101,6 +117,37 @@ class MyPageScreen extends StatelessWidget {
101117
}
102118
}
103119

120+
Future<bool> _openPrivacyPolicyExternally(Uri uri) {
121+
return launchUrl(uri, mode: LaunchMode.externalApplication);
122+
}
123+
124+
Future<void> _handlePrivacyPolicyTap(
125+
BuildContext context,
126+
PrivacyPolicyLauncher openPrivacyPolicy,
127+
) async {
128+
var opened = false;
129+
try {
130+
opened = await openPrivacyPolicy(ExternalLinks.privacyPolicyUri);
131+
} catch (_) {
132+
opened = false;
133+
}
134+
135+
if (opened || !context.mounted) return;
136+
137+
final l10n = AppLocalizations.of(context)!;
138+
await showTwoActionDialog(
139+
context,
140+
config: TwoActionDialogConfig(
141+
title: l10n.error,
142+
description: l10n.privacyPolicyOpenError,
143+
primaryAction: DialogActionConfig(
144+
label: l10n.ok,
145+
variant: ModalWideButtonVariant.primary,
146+
),
147+
),
148+
);
149+
}
150+
104151
class _AlarmStatusView extends StatefulWidget {
105152
const _AlarmStatusView();
106153

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
import 'package:mockito/mockito.dart';
5+
import 'package:on_time_front/core/constants/external_links.dart';
6+
import 'package:on_time_front/core/di/di_setup.dart';
7+
import 'package:on_time_front/core/services/alarm_scheduler_service.dart';
8+
import 'package:on_time_front/core/services/fallback_alarm_notification_service.dart';
9+
import 'package:on_time_front/domain/entities/alarm_entities.dart';
10+
import 'package:on_time_front/domain/entities/schedule_with_preparation_entity.dart';
11+
import 'package:on_time_front/domain/repositories/alarm_registry_repository.dart';
12+
import 'package:on_time_front/domain/repositories/alarm_repository.dart';
13+
import 'package:on_time_front/l10n/app_localizations.dart';
14+
import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart';
15+
import 'package:on_time_front/presentation/my_page/my_page_screen.dart';
16+
import 'package:on_time_front/presentation/shared/theme/theme.dart';
17+
18+
void main() {
19+
TestWidgetsFlutterBinding.ensureInitialized();
20+
21+
setUp(() async {
22+
await getIt.reset();
23+
getIt
24+
..registerSingleton<AlarmRepository>(_FakeAlarmRepository())
25+
..registerSingleton<AlarmRegistryRepository>(_FakeAlarmRegistry())
26+
..registerSingleton<AlarmSchedulerService>(_FakeAlarmSchedulerService())
27+
..registerSingleton<FallbackAlarmNotificationService>(
28+
_FakeFallbackAlarmNotificationService(),
29+
);
30+
});
31+
32+
tearDown(() async {
33+
await getIt.reset();
34+
});
35+
36+
testWidgets('shows English privacy policy setting', (tester) async {
37+
await _pumpMyPage(tester, locale: const Locale('en'));
38+
39+
expect(find.text('Privacy Policy'), findsOneWidget);
40+
});
41+
42+
testWidgets('shows Korean privacy policy setting', (tester) async {
43+
await _pumpMyPage(tester, locale: const Locale('ko'));
44+
45+
expect(find.text('개인정보 처리방침'), findsOneWidget);
46+
});
47+
48+
testWidgets('opens hosted privacy policy URL from setting', (tester) async {
49+
final openedUris = <Uri>[];
50+
51+
await _pumpMyPage(
52+
tester,
53+
locale: const Locale('en'),
54+
openPrivacyPolicy: (uri) async {
55+
openedUris.add(uri);
56+
return true;
57+
},
58+
);
59+
60+
await tester.ensureVisible(find.text('Privacy Policy'));
61+
await tester.tap(find.text('Privacy Policy'));
62+
await tester.pumpAndSettle();
63+
64+
expect(openedUris, [ExternalLinks.privacyPolicyUri]);
65+
});
66+
}
67+
68+
Future<void> _pumpMyPage(
69+
WidgetTester tester, {
70+
required Locale locale,
71+
PrivacyPolicyLauncher? openPrivacyPolicy,
72+
}) async {
73+
await tester.pumpWidget(
74+
MaterialApp(
75+
theme: themeData,
76+
locale: locale,
77+
localizationsDelegates: AppLocalizations.localizationsDelegates,
78+
supportedLocales: AppLocalizations.supportedLocales,
79+
home: BlocProvider<AuthBloc>.value(
80+
value: _StubAuthBloc(),
81+
child: MyPageScreen(openPrivacyPolicy: openPrivacyPolicy),
82+
),
83+
),
84+
);
85+
await tester.pumpAndSettle();
86+
}
87+
88+
class _StubAuthBloc extends Mock implements AuthBloc {
89+
@override
90+
AuthState get state => const AuthState.loading();
91+
92+
@override
93+
Stream<AuthState> get stream => const Stream.empty();
94+
95+
@override
96+
bool get isClosed => false;
97+
}
98+
99+
class _FakeAlarmRepository implements AlarmRepository {
100+
@override
101+
Future<String> getDeviceId() => throw UnimplementedError();
102+
103+
@override
104+
Future<AlarmDeviceInfo> buildCurrentDeviceInfo() =>
105+
throw UnimplementedError();
106+
107+
@override
108+
Future<AlarmSettings> getAlarmSettings() async {
109+
return const AlarmSettings(alarmsEnabled: false);
110+
}
111+
112+
@override
113+
Future<AlarmSettings> updateAlarmSettings({required bool alarmsEnabled}) {
114+
throw UnimplementedError();
115+
}
116+
117+
@override
118+
Future<void> registerCurrentDevice(AlarmDeviceInfo deviceInfo) {
119+
throw UnimplementedError();
120+
}
121+
122+
@override
123+
Future<void> unregisterCurrentDevice(String deviceId) {
124+
throw UnimplementedError();
125+
}
126+
127+
@override
128+
Future<List<ScheduleWithPreparationEntity>> getAlarmWindow(
129+
DateTime startDate,
130+
DateTime endDate,
131+
) {
132+
throw UnimplementedError();
133+
}
134+
135+
@override
136+
Future<void> postAlarmStatus(AlarmStatusReport report) {
137+
throw UnimplementedError();
138+
}
139+
}
140+
141+
class _FakeAlarmRegistry implements AlarmRegistryRepository {
142+
@override
143+
Future<List<ScheduledAlarmRecord>> loadAll() async => const [];
144+
145+
@override
146+
Future<void> upsert(ScheduledAlarmRecord record) {
147+
throw UnimplementedError();
148+
}
149+
150+
@override
151+
Future<void> deleteByScheduleId(String scheduleId) {
152+
throw UnimplementedError();
153+
}
154+
155+
@override
156+
Future<void> deleteAll() {
157+
throw UnimplementedError();
158+
}
159+
160+
@override
161+
Future<void> replaceAll(List<ScheduledAlarmRecord> records) {
162+
throw UnimplementedError();
163+
}
164+
}
165+
166+
class _FakeAlarmSchedulerService extends AlarmSchedulerService {
167+
@override
168+
Future<AlarmSchedulerCapabilities> getCapabilities() async {
169+
return AlarmSchedulerCapabilities.unsupported;
170+
}
171+
172+
@override
173+
Future<AlarmPermissionState> checkPermission() async {
174+
return AlarmPermissionState.unsupported;
175+
}
176+
}
177+
178+
class _FakeFallbackAlarmNotificationService
179+
implements FallbackAlarmNotificationService {
180+
@override
181+
Future<AlarmPermissionState> checkPermission() async {
182+
return AlarmPermissionState.unsupported;
183+
}
184+
185+
@override
186+
Future<AlarmPermissionState> requestPermission() {
187+
throw UnimplementedError();
188+
}
189+
190+
@override
191+
Future<void> scheduleFallbackAlarm(ScheduledAlarmRecord record) {
192+
throw UnimplementedError();
193+
}
194+
195+
@override
196+
Future<void> cancelFallbackAlarm(ScheduledAlarmRecord record) {
197+
throw UnimplementedError();
198+
}
199+
}

0 commit comments

Comments
 (0)