From f5f99ba9f88982dcf60ac6d7bfa486396b2fba8a Mon Sep 17 00:00:00 2001 From: ShojiTakayuki Date: Mon, 2 Mar 2026 17:52:10 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E8=A8=AD=E5=AE=9A=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E3=81=AB=E3=83=97=E3=83=A9=E3=82=A4=E3=83=90=E3=82=B7?= =?UTF-8?q?=E3=83=BC=E3=83=9D=E3=83=AA=E3=82=B7=E3=83=BC=E3=83=BB=E5=88=A9?= =?UTF-8?q?=E7=94=A8=E8=A6=8F=E7=B4=84=E3=83=BB=E3=82=B5=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=88=E5=B0=8E=E7=B7=9A=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - url_launcher / in_app_review パッケージを追加 - 定数ファイル(app_constants.dart)でURL・メールアドレスを一元管理 - URL起動ユーティリティ(url_launcher_helper.dart)を追加 - 設定画面を3セクション6メニュー項目に拡充 - アプリ情報: バージョン情報・ライセンス情報 - 法的情報: プライバシーポリシー・利用規約 - サポート: お問い合わせ(端末情報自動付与)・レビューを書く - テストにセクション・メニュー項目の表示確認を追加 Co-Authored-By: Claude Opus 4.6 --- lib/constants/app_constants.dart | 24 +++ lib/screens/settings_screen.dart | 137 +++++++++++++++--- lib/utils/url_launcher_helper.dart | 46 ++++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 82 ++++++++++- pubspec.yaml | 2 + test/widget_test.dart | 33 ++++- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 11 files changed, 313 insertions(+), 24 deletions(-) create mode 100644 lib/constants/app_constants.dart create mode 100644 lib/utils/url_launcher_helper.dart diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart new file mode 100644 index 0000000..bcafe48 --- /dev/null +++ b/lib/constants/app_constants.dart @@ -0,0 +1,24 @@ +/// アプリ全体で使用する定数 +class AppConstants { + AppConstants._(); + + // TODO: リリース前に実際のURLに差し替えること + /// プライバシーポリシーURL + static const privacyPolicyUrl = + 'https://example.com/privacy-policy'; + + /// 利用規約URL + static const termsOfServiceUrl = + 'https://example.com/terms-of-service'; + + /// お問い合わせメールアドレス + static const supportEmail = 'support@example.com'; + + /// App Store URL(iOS) + static const appStoreUrl = + 'https://apps.apple.com/app/idXXXXXXXXXX'; + + /// Google Play URL(Android) + static const googlePlayUrl = + 'https://play.google.com/store/apps/details?id=com.example.book_manager'; +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 700d485..342fb97 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,6 +1,12 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:book_manager/constants/app_constants.dart'; import 'package:book_manager/providers/package_info_provider.dart'; +import 'package:book_manager/utils/url_launcher_helper.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:in_app_review/in_app_review.dart'; /// 設定画面 class SettingsScreen extends ConsumerWidget { @@ -17,34 +23,125 @@ class SettingsScreen extends ConsumerWidget { ), body: ListView( children: [ - // アプリ情報セクション - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - 'アプリ情報', - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - const ListTile( - leading: Icon(Icons.info_outline), - title: Text('アプリ名'), - subtitle: Text('読書管理'), - ), + // ── アプリ情報 ── + _buildSectionHeader(context, 'アプリ情報'), ListTile( - leading: const Icon(Icons.new_releases_outlined), - title: const Text('バージョン'), - subtitle: Text( + leading: const Icon(Icons.info_outline), + title: const Text('バージョン情報'), + trailing: Text( packageInfoAsync.when( - data: (info) => info.version, + data: (info) => 'v${info.version}', loading: () => '', error: (_, _) => '取得失敗', ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), + ListTile( + leading: const Icon(Icons.description_outlined), + title: const Text('ライセンス情報'), + trailing: const Icon(Icons.chevron_right), + onTap: () { + showLicensePage( + context: context, + applicationName: '読書管理', + applicationVersion: packageInfoAsync.whenOrNull( + data: (info) => 'v${info.version}', + ), + ); + }, + ), + + // ── 法的情報 ── + _buildSectionHeader(context, '法的情報'), + ListTile( + leading: const Icon(Icons.privacy_tip_outlined), + title: const Text('プライバシーポリシー'), + trailing: const Icon(Icons.open_in_new), + onTap: () { + unawaited( + launchExternalUrl( + context, + AppConstants.privacyPolicyUrl, + ), + ); + }, + ), + ListTile( + leading: const Icon(Icons.gavel_outlined), + title: const Text('利用規約'), + trailing: const Icon(Icons.open_in_new), + onTap: () { + unawaited( + launchExternalUrl( + context, + AppConstants.termsOfServiceUrl, + ), + ); + }, + ), + + // ── サポート ── + _buildSectionHeader(context, 'サポート'), + ListTile( + leading: const Icon(Icons.mail_outlined), + title: const Text('お問い合わせ'), + trailing: const Icon(Icons.open_in_new), + onTap: () { + final version = packageInfoAsync.whenOrNull( + data: (info) => info.version, + ); + unawaited( + launchEmail( + context, + to: AppConstants.supportEmail, + subject: '【読書管理】お問い合わせ', + body: '\n\n---\n' + 'アプリバージョン: ${version ?? "不明"}\n' + 'OS: ${Platform.operatingSystem} ' + '${Platform.operatingSystemVersion}\n', + ), + ); + }, + ), + ListTile( + leading: const Icon(Icons.rate_review_outlined), + title: const Text('レビューを書く'), + trailing: const Icon(Icons.open_in_new), + onTap: () { + unawaited(_requestReview(context)); + }, + ), ], ), ); } -} \ No newline at end of file + + Widget _buildSectionHeader(BuildContext context, String title) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Text( + title, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ); + } + + Future _requestReview(BuildContext context) async { + final inAppReview = InAppReview.instance; + if (await inAppReview.isAvailable()) { + await inAppReview.requestReview(); + } else { + if (context.mounted) { + final storeUrl = Platform.isIOS + ? AppConstants.appStoreUrl + : AppConstants.googlePlayUrl; + await launchExternalUrl(context, storeUrl); + } + } + } +} diff --git a/lib/utils/url_launcher_helper.dart b/lib/utils/url_launcher_helper.dart new file mode 100644 index 0000000..5501b41 --- /dev/null +++ b/lib/utils/url_launcher_helper.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +/// 外部URLをブラウザで開く +/// +/// 失敗時は [context] を使って SnackBar でエラーを表示する。 +Future launchExternalUrl(BuildContext context, String url) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('URLを開けませんでした')), + ); + } + } +} + +/// メールアプリを起動する(件名・本文付き) +/// +/// 失敗時は [context] を使って SnackBar でエラーを表示する。 +Future launchEmail( + BuildContext context, { + required String to, + String subject = '', + String body = '', +}) async { + final uri = Uri( + scheme: 'mailto', + path: to, + queryParameters: { + if (subject.isNotEmpty) 'subject': subject, + if (body.isNotEmpty) 'body': body, + }, + ); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('メールアプリを起動できませんでした')), + ); + } + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 8a2cd8b..ba58caa 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,14 @@ import FlutterMacOS import Foundation +import in_app_review import package_info_plus import sqflite_darwin +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 5c393e7..fbc2a0c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -336,6 +336,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + in_app_review: + dependency: "direct main" + description: + name: in_app_review + sha256: ab26ac54dbd802896af78c670b265eaeab7ecddd6af4d0751e9604b60574817f + url: "https://pub.dev" + source: hosted + version: "2.0.11" + in_app_review_platform_interface: + dependency: transitive + description: + name: in_app_review_platform_interface + sha256: fed2c755f2125caa9ae10495a3c163aa7fab5af3585a9c62ef4a6920c5b45f10 + url: "https://pub.dev" + source: hosted + version: "2.0.5" io: dependency: transitive description: @@ -693,6 +709,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + url: "https://pub.dev" + source: hosted + version: "6.3.6" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + 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: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" uuid: dependency: "direct main" description: @@ -767,4 +847,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.9.0 <4.0.0" - flutter: ">=3.32.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 954a690..eacc4f5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,8 @@ dependencies: path: ^1.9.0 uuid: ^4.5.3 package_info_plus: ^9.0.0 + url_launcher: ^6.3.1 + in_app_review: ^2.0.10 dev_dependencies: flutter_test: diff --git a/test/widget_test.dart b/test/widget_test.dart index 1e054b4..c83230b 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -79,9 +79,36 @@ void main() { // 設定画面の内容が表示されることを確認 expect(find.widgetWithText(AppBar, '設定'), findsOneWidget); - // バージョンが表示されていることを確認 - expect(find.text('バージョン'), findsOneWidget); - expect(find.text('1.0.0'), findsOneWidget); + // バージョン情報が表示されていることを確認 + expect(find.text('バージョン情報'), findsOneWidget); + expect(find.text('v1.0.0'), findsOneWidget); + }); + + testWidgets('Settings screen displays all sections and menu items', + (WidgetTester tester) async { + await tester.pumpWidget(createTestApp()); + await tester.pumpAndSettle(); + + // 設定タブをタップ + await tester.tap(find.text('設定')); + await tester.pumpAndSettle(); + + // セクションヘッダーの確認 + expect(find.text('アプリ情報'), findsOneWidget); + expect(find.text('法的情報'), findsOneWidget); + expect(find.text('サポート'), findsOneWidget); + + // アプリ情報セクション + expect(find.text('バージョン情報'), findsOneWidget); + expect(find.text('ライセンス情報'), findsOneWidget); + + // 法的情報セクション + expect(find.text('プライバシーポリシー'), findsOneWidget); + expect(find.text('利用規約'), findsOneWidget); + + // サポートセクション + expect(find.text('お問い合わせ'), findsOneWidget); + expect(find.text('レビューを書く'), findsOneWidget); }); testWidgets('Tapping add button navigates to BookFormScreen', diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..4f78848 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..88b22e5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From b3fc96065792a81b50e58d4486e9dd19a9b4b938 Mon Sep 17 00:00:00 2001 From: ShojiTakayuki Date: Mon, 2 Mar 2026 18:27:39 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20GitHub=20Pages=E3=81=A7=E3=83=97?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=83=90=E3=82=B7=E3=83=BC=E3=83=9D=E3=83=AA?= =?UTF-8?q?=E3=82=B7=E3=83=BC=E3=83=BB=E5=88=A9=E7=94=A8=E8=A6=8F=E7=B4=84?= =?UTF-8?q?=E3=82=92=E5=85=AC=E9=96=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/にプライバシーポリシー・利用規約のHTMLページを作成 - app_constants.dartのURLをGitHub PagesのURLに更新 - アプリから外部ブラウザで法的文書を表示する方式に統一 Co-Authored-By: Claude Opus 4.6 --- docs/privacy-policy.html | 73 ++++++++++++++++++++++++++++++++ docs/terms-of-service.html | 73 ++++++++++++++++++++++++++++++++ lib/constants/app_constants.dart | 9 ++-- 3 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 docs/privacy-policy.html create mode 100644 docs/terms-of-service.html diff --git a/docs/privacy-policy.html b/docs/privacy-policy.html new file mode 100644 index 0000000..9c8fbea --- /dev/null +++ b/docs/privacy-policy.html @@ -0,0 +1,73 @@ + + + + + + プライバシーポリシー - 読書管理 + + + +

プライバシーポリシー

+

最終更新日: 2026年3月2日

+

本プライバシーポリシーは、「読書管理」アプリ(以下「本アプリ」)における個人情報の取扱いについて説明します。

+ +

収集する情報

+

本アプリは、現在個人情報を収集していません。書籍データや読書メモなど、すべてのデータはお使いの端末内にローカル保存されます。

+ +

情報の利用目的

+

本アプリは以下の目的でデータを利用します。

+
    +
  • 書籍データの管理・表示のため
  • +
  • Google Books APIへの検索クエリ送信(書籍検索時のみ)
  • +
+ +

第三者提供

+

本アプリは、ユーザーのデータを第三者に提供しません。

+

書籍検索機能の利用時に、検索クエリがGoogle Books APIに送信されます。Googleのプライバシーポリシーについては、Googleのウェブサイトをご確認ください。

+ +

データの保存場所

+

すべてのデータは端末内のローカルデータベース(SQLite)に保存されます。外部サーバーへのデータ送信は行いません。

+ +

データの削除

+
    +
  • アプリをアンインストールすると、すべてのデータが削除されます
  • +
  • アプリ内から個別の書籍データを削除できます
  • +
+ +

プライバシーポリシーの変更

+

本プライバシーポリシーは、必要に応じて更新されることがあります。変更があった場合は、アプリのアップデートを通じてお知らせします。

+ +

お問い合わせ

+

本プライバシーポリシーに関するお問い合わせは、アプリ内の「お問い合わせ」からご連絡ください。

+ + diff --git a/docs/terms-of-service.html b/docs/terms-of-service.html new file mode 100644 index 0000000..6df3364 --- /dev/null +++ b/docs/terms-of-service.html @@ -0,0 +1,73 @@ + + + + + + 利用規約 - 読書管理 + + + +

利用規約

+

最終更新日: 2026年3月2日

+

本利用規約(以下「本規約」)は、「読書管理」アプリ(以下「本アプリ」)の利用条件を定めるものです。

+ +

免責事項

+
    +
  • 本アプリの利用は、ユーザーご自身の責任において行ってください
  • +
  • 端末の故障、アプリの不具合等によるデータの損失について、開発者は責任を負いません
  • +
  • 本アプリは現状有姿(as-is)で提供され、特定目的への適合性を保証するものではありません
  • +
+ +

禁止事項

+

本アプリの利用にあたり、以下の行為を禁止します。

+
    +
  • 本アプリのリバースエンジニアリング、逆コンパイル、逆アセンブル
  • +
  • 本アプリの改変、二次配布
  • +
  • 本アプリを利用した違法行為
  • +
  • その他、開発者が不適切と判断する行為
  • +
+ +

著作権について

+
    +
  • 本アプリの著作権は開発者に帰属します
  • +
  • 書籍検索で表示される書影や書籍情報はGoogle Books APIを通じて提供されており、それぞれの著作権者に帰属します
  • +
  • ユーザーが登録した書籍データ・メモの著作権はユーザーに帰属します
  • +
+ +

規約の変更

+

開発者は、必要に応じて本規約を変更することがあります。変更後の利用規約は、アプリのアップデートを通じて公開されます。アップデート後のアプリの利用をもって、変更後の規約に同意したものとみなします。

+ +

お問い合わせ

+

本規約に関するお問い合わせは、アプリ内の「お問い合わせ」からご連絡ください。

+ + diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart index bcafe48..3ef919c 100644 --- a/lib/constants/app_constants.dart +++ b/lib/constants/app_constants.dart @@ -2,14 +2,17 @@ class AppConstants { AppConstants._(); - // TODO: リリース前に実際のURLに差し替えること + /// GitHub Pages ベースURL + static const _pagesBaseUrl = + 'https://haino357.github.io/book_manager'; + /// プライバシーポリシーURL static const privacyPolicyUrl = - 'https://example.com/privacy-policy'; + '$_pagesBaseUrl/privacy-policy.html'; /// 利用規約URL static const termsOfServiceUrl = - 'https://example.com/terms-of-service'; + '$_pagesBaseUrl/terms-of-service.html'; /// お問い合わせメールアドレス static const supportEmail = 'support@example.com'; From cafd7e182aa1db6b8a41e4b89f807c71db562088 Mon Sep 17 00:00:00 2001 From: ShojiTakayuki Date: Mon, 2 Mar 2026 18:36:18 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20PR=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E6=8C=87=E6=91=98=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dart:ioのPlatformをfoundationのdefaultTargetPlatform/kIsWebに変更しWeb対応 - レビュー機能のストアURL分岐をAndroid/iOSのみに限定し他プラットフォームは未対応表示 - ダブルクォートをシングルクォートに統一 - canLaunchUrlをlaunchUrlの戻り値判定に変更しAndroid 11+の誤判定を回避 - Uri.parseにtry/catchを追加し不正URL時のクラッシュを防止 Co-Authored-By: Claude Opus 4.6 --- lib/screens/settings_screen.dart | 51 +++++++++++++++++++++++++----- lib/utils/url_launcher_helper.dart | 32 +++++++++++-------- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 342fb97..f065059 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'dart:io'; import 'package:book_manager/constants/app_constants.dart'; import 'package:book_manager/providers/package_info_provider.dart'; import 'package:book_manager/utils/url_launcher_helper.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:in_app_review/in_app_review.dart'; @@ -93,15 +93,15 @@ class SettingsScreen extends ConsumerWidget { final version = packageInfoAsync.whenOrNull( data: (info) => info.version, ); + final os = _platformName(); unawaited( launchEmail( context, to: AppConstants.supportEmail, subject: '【読書管理】お問い合わせ', body: '\n\n---\n' - 'アプリバージョン: ${version ?? "不明"}\n' - 'OS: ${Platform.operatingSystem} ' - '${Platform.operatingSystemVersion}\n', + 'アプリバージョン: ${version ?? '不明'}\n' + 'OS: $os\n', ), ); }, @@ -131,16 +131,51 @@ class SettingsScreen extends ConsumerWidget { ); } + /// プラットフォーム名を返す(dart:io に依存しない) + static String _platformName() { + if (kIsWeb) { + return 'Web'; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 'Android'; + case TargetPlatform.iOS: + return 'iOS'; + case TargetPlatform.macOS: + return 'macOS'; + case TargetPlatform.windows: + return 'Windows'; + case TargetPlatform.linux: + return 'Linux'; + case TargetPlatform.fuchsia: + return 'Fuchsia'; + } + } + Future _requestReview(BuildContext context) async { final inAppReview = InAppReview.instance; if (await inAppReview.isAvailable()) { await inAppReview.requestReview(); } else { - if (context.mounted) { - final storeUrl = Platform.isIOS - ? AppConstants.appStoreUrl - : AppConstants.googlePlayUrl; + if (!context.mounted) { + return; + } + + String? storeUrl; + if (defaultTargetPlatform == TargetPlatform.iOS) { + storeUrl = AppConstants.appStoreUrl; + } else if (defaultTargetPlatform == TargetPlatform.android) { + storeUrl = AppConstants.googlePlayUrl; + } + + if (storeUrl != null) { await launchExternalUrl(context, storeUrl); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('このプラットフォームではレビュー機能は未対応です'), + ), + ); } } } diff --git a/lib/utils/url_launcher_helper.dart b/lib/utils/url_launcher_helper.dart index 5501b41..62c9270 100644 --- a/lib/utils/url_launcher_helper.dart +++ b/lib/utils/url_launcher_helper.dart @@ -5,15 +5,23 @@ import 'package:url_launcher/url_launcher.dart'; /// /// 失敗時は [context] を使って SnackBar でエラーを表示する。 Future launchExternalUrl(BuildContext context, String url) async { - final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } else { + late final Uri uri; + try { + uri = Uri.parse(url); + } on FormatException { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('URLを開けませんでした')), + const SnackBar(content: Text('不正なURLです')), ); } + return; + } + + final launched = await launchUrl(uri, mode: LaunchMode.externalApplication); + if (!launched && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('URLを開けませんでした')), + ); } } @@ -34,13 +42,11 @@ Future launchEmail( if (body.isNotEmpty) 'body': body, }, ); - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } else { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('メールアプリを起動できませんでした')), - ); - } + + final launched = await launchUrl(uri); + if (!launched && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('メールアプリを起動できませんでした')), + ); } } From f08ba3617ec2cb0b70f1c023fe6720de0d9e832d Mon Sep 17 00:00:00 2001 From: ShojiTakayuki Date: Mon, 4 May 2026 16:59:40 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20PR=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E6=8C=87=E6=91=98=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - in_app_review 呼び出しを Web/非対応プラットフォーム判定 + try/catch で囲み、MissingPluginException を吸収 - pubspec.yaml の Flutter SDK 制約を pubspec.lock に合わせて >=3.35.0 に統一 - supportEmail/appStoreUrl/googlePlayUrl をプレースホルダーから空文字に変更し、 お問い合わせメニューは空文字時に「準備中」表示で無効化、 レビュー導線は空文字時にストアURLフォールバックを行わずSnackBar表示 --- lib/constants/app_constants.dart | 17 ++++-- lib/screens/settings_screen.dart | 94 ++++++++++++++++++++------------ pubspec.yaml | 2 +- 3 files changed, 72 insertions(+), 41 deletions(-) diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart index 3ef919c..4117b4e 100644 --- a/lib/constants/app_constants.dart +++ b/lib/constants/app_constants.dart @@ -15,13 +15,20 @@ class AppConstants { '$_pagesBaseUrl/terms-of-service.html'; /// お問い合わせメールアドレス - static const supportEmail = 'support@example.com'; + /// + /// 空文字の場合は未設定として扱い、問い合わせ導線を「準備中」表示にする。 + /// リリース前に実値を設定すること。 + static const supportEmail = ''; /// App Store URL(iOS) - static const appStoreUrl = - 'https://apps.apple.com/app/idXXXXXXXXXX'; + /// + /// 空文字の場合は未設定として扱い、ストア遷移フォールバックを行わない。 + /// リリース前に実値を設定すること。 + static const appStoreUrl = ''; /// Google Play URL(Android) - static const googlePlayUrl = - 'https://play.google.com/store/apps/details?id=com.example.book_manager'; + /// + /// 空文字の場合は未設定として扱い、ストア遷移フォールバックを行わない。 + /// リリース前に実値を設定すること。 + static const googlePlayUrl = ''; } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index f065059..0da3e62 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -88,23 +88,31 @@ class SettingsScreen extends ConsumerWidget { ListTile( leading: const Icon(Icons.mail_outlined), title: const Text('お問い合わせ'), - trailing: const Icon(Icons.open_in_new), - onTap: () { - final version = packageInfoAsync.whenOrNull( - data: (info) => info.version, - ); - final os = _platformName(); - unawaited( - launchEmail( - context, - to: AppConstants.supportEmail, - subject: '【読書管理】お問い合わせ', - body: '\n\n---\n' - 'アプリバージョン: ${version ?? '不明'}\n' - 'OS: $os\n', - ), - ); - }, + subtitle: AppConstants.supportEmail.isEmpty + ? const Text('準備中') + : null, + trailing: AppConstants.supportEmail.isEmpty + ? null + : const Icon(Icons.open_in_new), + enabled: AppConstants.supportEmail.isNotEmpty, + onTap: AppConstants.supportEmail.isEmpty + ? null + : () { + final version = packageInfoAsync.whenOrNull( + data: (info) => info.version, + ); + final os = _platformName(); + unawaited( + launchEmail( + context, + to: AppConstants.supportEmail, + subject: '【読書管理】お問い合わせ', + body: '\n\n---\n' + 'アプリバージョン: ${version ?? '不明'}\n' + 'OS: $os\n', + ), + ); + }, ), ListTile( leading: const Icon(Icons.rate_review_outlined), @@ -153,30 +161,46 @@ class SettingsScreen extends ConsumerWidget { } Future _requestReview(BuildContext context) async { - final inAppReview = InAppReview.instance; - if (await inAppReview.isAvailable()) { - await inAppReview.requestReview(); - } else { + if (kIsWeb || + (defaultTargetPlatform != TargetPlatform.android && + defaultTargetPlatform != TargetPlatform.iOS)) { if (!context.mounted) { return; } + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('このプラットフォームではレビュー機能は未対応です'), + ), + ); + return; + } - String? storeUrl; - if (defaultTargetPlatform == TargetPlatform.iOS) { - storeUrl = AppConstants.appStoreUrl; - } else if (defaultTargetPlatform == TargetPlatform.android) { - storeUrl = AppConstants.googlePlayUrl; + try { + final inAppReview = InAppReview.instance; + if (await inAppReview.isAvailable()) { + await inAppReview.requestReview(); + return; } + } on Exception { + // プラグイン未登録など想定外の例外時はストアURLフォールバックへ + } - if (storeUrl != null) { - await launchExternalUrl(context, storeUrl); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('このプラットフォームではレビュー機能は未対応です'), - ), - ); - } + if (!context.mounted) { + return; + } + + final storeUrl = defaultTargetPlatform == TargetPlatform.iOS + ? AppConstants.appStoreUrl + : AppConstants.googlePlayUrl; + + if (storeUrl.isNotEmpty) { + await launchExternalUrl(context, storeUrl); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('このプラットフォームではレビュー機能は未対応です'), + ), + ); } } } diff --git a/pubspec.yaml b/pubspec.yaml index eacc4f5..a5c53fd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ version: 1.0.0+1 environment: sdk: ^3.9.0 - flutter: ">=3.32.0" + flutter: ">=3.35.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions