Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
e376d20
feat(l10n): 深夜服用記録の注意ダイアログの文言を追加
bannzai Jul 12, 2026
43f53f0
feat: 深夜服用記録の注意ダイアログ用のSharedPreferencesキーを追加
bannzai Jul 12, 2026
1a47a37
feat: 深夜(0:00-2:00)服用記録の注意ダイアログを追加
bannzai Jul 12, 2026
24e7634
feat: 服用記録の各経路に深夜服用の注意ダイアログを組み込み
bannzai Jul 12, 2026
f1b96e5
test: 深夜服用記録の注意ダイアログのテストを追加
bannzai Jul 12, 2026
84418fd
refactor: hooksを使っていないWidgetをConsumerWidgetに変更
bannzai Jul 14, 2026
0511f94
refactor: RecordPagePillSheetのConsumerWidget化と一時変数のインライン化
bannzai Jul 14, 2026
340dfa7
refactor: 注意ダイアログの一時変数インライン化とデフォルト値の根拠コメント追加
bannzai Jul 14, 2026
808d267
refactor: MidnightTakenWarningDialogDisplayMode enumを廃止しbool判定にする
bannzai Jul 14, 2026
5064f9b
fix: 深夜服用注意ダイアログの判定〜表示開始を表示中フラグで直列化
bannzai Jul 14, 2026
6f1c64d
fix: リマインダー通知OFFのユーザーには深夜服用注意ダイアログを表示しない
bannzai Jul 14, 2026
3d0468d
refactor: ピル番号タップの服用記録結果をisRecordedに代入して条件式を整形
bannzai Jul 14, 2026
45596ce
fix: 偽薬/休薬期間の通知OFF時は該当日の深夜服用注意ダイアログを表示しない
bannzai Jul 14, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:pilll/components/atoms/button.dart';
import 'package:pilll/components/atoms/font.dart';
import 'package:pilll/components/atoms/text_color.dart';
import 'package:pilll/entity/pill_sheet.codegen.dart';
import 'package:pilll/entity/pill_sheet_type.dart';
import 'package:pilll/entity/setting.codegen.dart';
import 'package:pilll/features/localizations/l.dart';
import 'package:pilll/utils/analytics.dart';
import 'package:pilll/utils/datetime/day.dart';
import 'package:pilll/utils/formatter/date_time_formatter.dart';
import 'package:pilll/utils/shared_preference/keys.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';

/// 記録操作時点でまだ届いていない当日の通知時刻を返す
///
/// 通知時刻が0:00-2:00内(例: 1:00)に設定されているユーザーがその時刻より後に記録した場合、
/// 通知は既に届いているため「届かない」対象から除外する
List<ReminderTime> midnightTakenWarningRemainingReminderTimes({
required List<ReminderTime> reminderTimes,
required DateTime recordedAt,
}) {
return reminderTimes
.where(
(reminderTime) => reminderTime.hour > recordedAt.hour || (reminderTime.hour == recordedAt.hour && reminderTime.minute > recordedAt.minute))
.toList();
}

/// 深夜(0:00-2:00)服用記録の注意ダイアログを表示すべきかどうかを判定する
///
/// 日を跨いだことに気づかず服用記録をすると当日分まで記録され、
/// 当日の通知が届かなくなる問い合わせへの対策として、30日に1回まで注意ダイアログを表示する
bool shouldShowMidnightTakenWarningDialog({
required SharedPreferences sharedPreferences,
required DateTime takenDate,
required DateTime recordedAt,
required Setting setting,
required PillSheet activePillSheet,
}) {
// 0:00〜1:59の記録操作のみ対象
if (recordedAt.hour >= 2) {
return false;
}
// 当日分まで服用記録された場合のみ対象。過去のピル番号タップ等、当日分が記録されない場合は当日の通知は届く
if (takenDate.date() != recordedAt.date()) {
Comment thread
bannzai marked this conversation as resolved.
return false;
}
// リマインダー通知がOFFの場合は当日の通知が元々送信されない(LocalNotificationService.runが早期return)ため、
// 「通知が届かない」という注意自体が成立しない
if (!setting.isOnReminder) {
return false;
}
// 偽薬/休薬期間の通知がOFFで当日が偽薬/休薬期間の場合、当日の通知は元々登録されない
// (RegisterReminderLocalNotification.runがdosingPeriod超のピル番号をスキップ)ため、注意自体が成立しない
if (!setting.isOnNotifyInNotTakenDuration && activePillSheet.pillSheetType.dosingPeriod < activePillSheet.pillNumberFor(targetDate: recordedAt)) {
return false;
}
// 記録時点でこれから届く予定の当日通知がなければ「通知が届かない」という注意自体が成立しない
if (midnightTakenWarningRemainingReminderTimes(reminderTimes: setting.reminderTimes, recordedAt: recordedAt).isEmpty) {
return false;
}
// キーは「二度と表示しない」押下時にのみ保存されるため、未保存(null)は未押下として扱う
if (sharedPreferences.getBool(BoolKey.midnightTakenWarningDialogNeverShowAgain) ?? false) {
return false;
}
final lastShownMilliseconds = sharedPreferences.getDouble(DoubleKey.midnightTakenWarningDialogLastShownDateTime);
// 30日に1回まで: 前回表示から30日未満の場合は表示しない。未表示(null)なら無条件で表示する
if (lastShownMilliseconds != null && daysBetween(DateTime.fromMillisecondsSinceEpoch(lastShownMilliseconds.toInt()), recordedAt) < 30) {
return false;
}
return true;
}

/// 深夜(0:00-2:00)服用記録の注意ダイアログが表示中かどうか
///
/// 「飲んだ」ボタンとピルシートの番号タップの複数経路から短時間に呼ばれた場合に、
/// 表示記録(SharedPreferences)の保存前に両方が表示可能と判定して二重表示されるのを防ぐ
bool _midnightTakenWarningDialogIsShowing = false;

/// 深夜(0:00-2:00)にアプリ上で服用記録をした場合に、当日分まで服用記録されたことを知らせる注意ダイアログを表示する
///
/// [takenDate] は服用記録された最後の日、[recordedAt] は記録操作を行った日時。
/// 服用記録(takePill)の完了を待たずに呼び出してよい
void showMidnightTakenWarningDialogIfNeeded({
required BuildContext context,
required DateTime takenDate,
required DateTime recordedAt,
required Setting setting,
required PillSheet activePillSheet,
}) async {
if (_midnightTakenWarningDialogIsShowing) {
return;
}
_midnightTakenWarningDialogIsShowing = true;
try {
final sharedPreferences = await SharedPreferences.getInstance();
if (!shouldShowMidnightTakenWarningDialog(
sharedPreferences: sharedPreferences,
takenDate: takenDate,
recordedAt: recordedAt,
setting: setting,
activePillSheet: activePillSheet,
)) {
return;
}
if (!context.mounted) {
return;
}

analytics.logScreenView(screenName: 'MidnightTakenWarningDialog');
await showDialog(
Comment thread
bannzai marked this conversation as resolved.
context: context,
// 「閉じる」でSharedPreferencesに表示記録を残すため、ボタン以外で閉じられないようにする
barrierDismissible: false,
Comment thread
bannzai marked this conversation as resolved.
builder: (context) => MidnightTakenWarningDialog(
takenDate: takenDate,
reminderTimes: midnightTakenWarningRemainingReminderTimes(reminderTimes: setting.reminderTimes, recordedAt: recordedAt),
// 2回目以降(過去に「閉じる」で表示日時が保存済み)の表示でのみ「二度と表示しない」を出す
showsNeverShowAgainButton: sharedPreferences.containsKey(DoubleKey.midnightTakenWarningDialogLastShownDateTime),
),
);
} finally {
// ダイアログが閉じた時点で表示記録は保存済みのため、フラグ解除後の再表示は30日条件で防がれる
_midnightTakenWarningDialogIsShowing = false;
}
}

/// 深夜(0:00-2:00)の服用記録で当日分まで記録されたことを知らせる注意ダイアログ
class MidnightTakenWarningDialog extends StatelessWidget {
/// 服用記録をした日時。本文の日付(例: 7/11)の表示に使う
final DateTime takenDate;

/// 記録時点でまだ届いていない当日の通知時刻。本文の通知時刻(例: 19:00)の表示に使う
final List<ReminderTime> reminderTimes;

/// 「二度と表示しない」ボタンを表示するかどうか。2回目以降の表示でtrue
final bool showsNeverShowAgainButton;

const MidnightTakenWarningDialog({
super.key,
required this.takenDate,
required this.reminderTimes,
required this.showsNeverShowAgainButton,
});

@override
Widget build(BuildContext context) {
return AlertDialog(
contentPadding: const EdgeInsets.only(
left: 24,
right: 24,
top: 4,
bottom: 24,
),
actionsPadding: const EdgeInsets.only(left: 24, right: 24, bottom: 32),
titlePadding: const EdgeInsets.only(top: 32),
title: SvgPicture.asset('images/alert_24.svg', width: 24, height: 24),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
content: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
L.midnightTakenWarningDialogTitle,
style: const TextStyle(
fontFamily: FontFamily.japanese,
fontWeight: FontWeight.w700,
fontSize: 16,
color: TextColor.main,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
L.midnightTakenWarningDialogBody(
'${takenDate.month}/${takenDate.day}',
reminderTimes.map((reminderTime) => DateTimeFormatter.militaryTime(reminderTime.dateTime())).join(', '),
),
style: const TextStyle(
fontFamily: FontFamily.japanese,
fontWeight: FontWeight.w300,
fontSize: 14,
height: 1.7,
color: TextColor.main,
),
textAlign: TextAlign.center,
),
],
),
actions: [
AppOutlinedButton(
onPressed: () async {
analytics.logEvent(name: 'midnight_taken_warning_faq');
launchUrl(
Uri.parse(
'https://pilll.notion.site/63c3436e63ea4ba8a1407be40cb5f0e5',
),
);
Comment thread
bannzai marked this conversation as resolved.
},
text: L.midnightTakenWarningDialogSeeHowToRevert,
),
Center(
child: AlertButton(
text: L.close,
onPressed: () async {
analytics.logEvent(name: 'midnight_taken_warning_close');
await (await SharedPreferences.getInstance()).setDouble(
DoubleKey.midnightTakenWarningDialogLastShownDateTime,
now().millisecondsSinceEpoch.toDouble(),
);
if (context.mounted) {
Navigator.of(context).pop();
}
Comment thread
bannzai marked this conversation as resolved.
},
),
),
if (showsNeverShowAgainButton)
Center(
child: AlertButton(
text: L.midnightTakenWarningDialogNeverShowAgain,
onPressed: () async {
analytics.logEvent(name: 'midnight_taken_warning_never_show_again');
await (await SharedPreferences.getInstance()).setBool(
BoolKey.midnightTakenWarningDialogNeverShowAgain,
true,
);
if (context.mounted) {
Navigator.of(context).pop();
}
},
),
),
],
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@ import 'package:pilll/features/record/components/button/rest_duration_button.dar
import 'package:pilll/features/record/components/button/taken_button.dart';
import 'package:pilll/entity/pill_sheet.codegen.dart';
import 'package:pilll/entity/pill_sheet_group.codegen.dart';
import 'package:pilll/entity/setting.codegen.dart';
import 'package:pilll/utils/local_notification.dart';

class RecordPageButton extends HookConsumerWidget {
class RecordPageButton extends ConsumerWidget {
final PillSheetGroup pillSheetGroup;
final PillSheet currentPillSheet;
final bool userIsPremiumOtTrial;
final User user;

/// TakenButtonの深夜(0:00-2:00)服用記録の注意ダイアログの表示判定・文言に使う
final Setting setting;

const RecordPageButton({
super.key,
required this.pillSheetGroup,
required this.currentPillSheet,
required this.userIsPremiumOtTrial,
required this.user,
required this.setting,
});

@override
Expand Down Expand Up @@ -48,6 +53,7 @@ class RecordPageButton extends HookConsumerWidget {
pillSheetGroup: pillSheetGroup,
activePillSheet: currentPillSheet,
userIsPremiumOtTrial: userIsPremiumOtTrial,
setting: setting,
registerReminderLocalNotification: registerReminderLocalNotification,
);
}
Expand Down
22 changes: 19 additions & 3 deletions lib/features/record/components/button/taken_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,23 @@ import 'package:pilll/features/release_note/release_note.dart';
import 'package:pilll/features/record/util/request_in_app_review.dart';
import 'package:pilll/entity/pill_sheet.codegen.dart';
import 'package:pilll/entity/pill_sheet_group.codegen.dart';
import 'package:pilll/entity/setting.codegen.dart';
import 'package:pilll/features/error/error_alert.dart';
import 'package:pilll/features/record/components/button/midnight_taken_warning_dialog.dart';
import 'package:pilll/utils/error_log.dart';
import 'package:pilll/native/widget.dart';
import 'package:pilll/provider/take_pill.dart';
import 'package:pilll/utils/datetime/day.dart';
import 'package:pilll/utils/local_notification.dart';

class TakenButton extends HookConsumerWidget {
class TakenButton extends ConsumerWidget {
final BuildContext parentContext;
final PillSheetGroup pillSheetGroup;
final PillSheet activePillSheet;
final bool userIsPremiumOtTrial;

/// 深夜(0:00-2:00)服用記録の注意ダイアログの表示判定・文言に使う
final Setting setting;
final RegisterReminderLocalNotification registerReminderLocalNotification;

const TakenButton({
Expand All @@ -28,6 +33,7 @@ class TakenButton extends HookConsumerWidget {
required this.pillSheetGroup,
required this.activePillSheet,
required this.userIsPremiumOtTrial,
required this.setting,
required this.registerReminderLocalNotification,
});
@override
Expand All @@ -49,12 +55,22 @@ class TakenButton extends HookConsumerWidget {
);
requestInAppReview();
showReleaseNotePreDialog(context);
final updatedPillSheetGroup = await takePill(
takenDate: now(),
final takenDate = now();
// 深夜服用の注意ダイアログは服用記録の完了(await)を待たずに表示するため、awaitの前に服用記録を開始する
final updatedPillSheetGroupFuture = takePill(
takenDate: takenDate,
pillSheetGroup: pillSheetGroup,
activePillSheet: activePillSheet,
isQuickRecord: false,
);
showMidnightTakenWarningDialogIfNeeded(
context: context,
takenDate: takenDate,
recordedAt: takenDate,
setting: setting,
Comment thread
bannzai marked this conversation as resolved.
activePillSheet: activePillSheet,
);
final updatedPillSheetGroup = await updatedPillSheetGroupFuture;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
syncActivePillSheetValue(pillSheetGroup: updatedPillSheetGroup);
await registerReminderLocalNotification();
} catch (exception, stack) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:pilll/components/organisms/pill_mark/pill_mark_with_number_layou
import 'package:pilll/components/organisms/pill_sheet/pill_sheet_view_layout.dart';
import 'package:pilll/components/organisms/pill_sheet/pill_sheet_view_weekday_line.dart';
import 'package:pilll/features/release_note/release_note.dart';
import 'package:pilll/features/record/components/button/midnight_taken_warning_dialog.dart';
import 'package:pilll/features/record/components/pill_sheet/components/pill_number.dart';
import 'package:pilll/features/record/util/request_in_app_review.dart';
import 'package:pilll/provider/revert_take_pill.dart';
Expand All @@ -29,7 +30,7 @@ import 'package:pilll/utils/datetime/day.dart';
import 'package:pilll/utils/local_notification.dart';

// ここを編集する時は historical_pill_sheet_group/component/pill_sheet.dart も編集する
class RecordPagePillSheet extends HookConsumerWidget {
class RecordPagePillSheet extends ConsumerWidget {
final PillSheetGroup pillSheetGroup;
final PillSheet pillSheet;
final Setting setting;
Expand Down Expand Up @@ -175,13 +176,26 @@ class RecordPagePillSheet extends HookConsumerWidget {
requestInAppReview();
showReleaseNotePreDialog(context);

await _takeWithPillNumber(
takePill,
registerReminderLocalNotification,
pillSheetGroup: pillSheetGroup,
pillNumberInPillSheet: pillNumberInPillSheet,
pillSheet: pillSheet,
);
// _takeWithPillNumberは服用記録されないケースでnullを返す
final isRecorded = await _takeWithPillNumber(
takePill,
registerReminderLocalNotification,
pillSheetGroup: pillSheetGroup,
pillNumberInPillSheet: pillNumberInPillSheet,
pillSheet: pillSheet,
) !=
null;
// 実際に服用記録された場合のみダイアログの表示判定をする。記録された場合activePillSheetは必ず存在する
final activePillSheet = pillSheetGroup.activePillSheet;
if (isRecorded && activePillSheet != null && context.mounted) {
showMidnightTakenWarningDialogIfNeeded(
context: context,
takenDate: pillSheet.displayPillTakeDate(pillNumberInPillSheet),
recordedAt: now(),
Comment thread
bannzai marked this conversation as resolved.
setting: setting,
activePillSheet: activePillSheet,
);
}
}
} catch (exception, stack) {
errorLogger.recordError(exception, stack);
Expand Down
Loading
Loading