部署指南

建置指令

Flutter 支援從同一程式碼庫建置多個平台的應用程式。以下是各平台的建置指令:

# Android APK(通用)
flutter build apk

# Android App Bundle(Play Store 推薦)
flutter build appbundle

# iOS(需要 macOS + Xcode)
flutter build ios

# Web
flutter build web

# macOS(需要 macOS + Xcode)
flutter build macos

# Windows
flutter build windows

Android 部署

APK 建置

# 產生 release APK
flutter build apk --release

# 輸出位置:
# build/app/outputs/flutter-apk/app-release.apk

App Bundle 建置(Play Store)

# 產生 AAB(Google Play 推薦格式)
flutter build appbundle --release

# 輸出位置:
# build/app/outputs/bundle/release/app-release.aab

Play Store 上架步驟

  1. Google Play Console 建立應用程式
  2. 設定簽名金鑰(keystore)
  3. 上傳 AAB 檔案
  4. 填寫商店資訊、螢幕截圖、內容分級
  5. 提交審核

Android 建置與部署(完整指南)

把你的 Flutter App 送進 Android 手機——比想像中簡單,前提是你的 USB 線不是只能充電的那種。

前置需求

項目 需求
Flutter SDK ≥ 3.38
Android SDK 透過 Android Studio 安裝
Java JDK 17(Flutter 3.x 需要)
USB 傳輸線 能傳輸資料的那種,不是夜市買的
flutter doctor

確認 FlutterAndroid toolchainConnected device 三項都打勾。

一、手機開啟開發者模式

  1. 進入 設定 → 關於手機
  2. 連續點擊 版本號碼 7 次 → 出現「您已成為開發者」
  3. 回到 設定 → 系統 → 開發者選項
  4. 開啟 USB 偵錯 (USB Debugging)
  5. USB 連接電腦後,手機會彈出「允許 USB 偵錯」→ 點 允許

二、確認手機已連接

flutter devices

應該看到你的 Android 裝置:

Pixel 7 (mobile) • abc12345 • android-arm64 • Android 14

三、Debug 模式直接運行

# 自動選擇裝置
flutter run

# 指定裝置(多裝置時)
flutter run -d abc12345

支援 Hot Reload,改一行存一下就能看到效果——工程師的即時滿足感。

四、建置 Release APK

方法 A:APK(通用,可直接傳輸安裝)

flutter build apk --release
# 產出:build/app/outputs/flutter-apk/app-release.apk

方法 B:App Bundle(上架 Google Play)

flutter build appbundle --release
# 產出:build/app/outputs/bundle/release/app-release.aab

五、安裝 APK 到手機

# 方法 1:adb 安裝
flutter build apk --release
adb install build/app/outputs/flutter-apk/app-release.apk

# 已有舊版本?加 -r 覆蓋
adb install -r build/app/outputs/flutter-apk/app-release.apk

# 方法 2:flutter 直接安裝
flutter install --release

方法 3:手動傳檔 — 把 APK 傳到手機(USB/雲端/LINE),手機上點開安裝。系統提示「允許安裝未知來源」就允許。

六、一行指令:建置 + 安裝

flutter build apk --release && adb install -r build/app/outputs/flutter-apk/app-release.apk

複製貼上,去泡杯咖啡,回來就好了。

七、簽署 APK(正式發布用)

Debug 版本用預設簽署金鑰,正式發布需要你自己的。

1. 產生 Keystore

keytool -genkey -v -keystore ~/idempiere-release.jks 
  -keyalias idempiere -keyalg RSA -keysize 2048 -validity 10000

請妥善保管此檔案和密碼。丟了就像忘了保險箱密碼——裡面的東西還在,但你再也打不開。

2. 建立 key.properties

android/ 目錄下建立(不要加入版本控制):

storePassword=你的密碼
keyPassword=你的密碼
keyAlias=idempiere
storeFile=/Users/你/idempiere-release.jks

3. 修改 android/app/build.gradle.kts

android { 區塊前加入:

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

android { 區塊內加入 signingConfigs 並修改 buildTypes:

signingConfigs {
    create("release") {
        keyAlias = keystoreProperties["keyAlias"] as String
        keyPassword = keystoreProperties["keyPassword"] as String
        storeFile = file(keystoreProperties["storeFile"] as String)
        storePassword = keystoreProperties["storePassword"] as String
    }
}
buildTypes {
    release {
        signingConfig = signingConfigs.getByName("release")
    }
}

4. 建置已簽署的 APK

flutter build apk --release

Android 常見問題

問題 解法
flutter devices 看不到手機 確認 USB 偵錯已開、換條能傳資料的 USB 線、adb devices 看是否 unauthorized
安裝時「應用程式未安裝」 adb uninstall tw.idempiere.app 再重新安裝
Gradle 建置失敗 cd android && ./gradlew clean && cd .. && flutter clean && flutter pub get && flutter build apk --release
無法安裝未知來源 APK 設定 → 應用程式 → 特殊存取權 → 安裝未知的應用程式 → 允許

iOS 部署

建置步驟

# 建置 iOS release
flutter build ios --release

# 開啟 Xcode 進行 archive
open ios/Runner.xcworkspace

App Store 上架步驟

  1. 在 Xcode 中選擇 Product → Archive
  2. 在 Organizer 中選擇 Distribute App
  3. 選擇 App Store Connect
  4. App Store Connect 填寫應用資訊
  5. 提交審核

Web 部署

# 建置 Web 版本
flutter build web

# 輸出位置:
# build/web/

# 部署至任何靜態檔案伺服器
# 例如 Nginx、Apache、Firebase Hosting、GitHub Pages

Web 版本產生的是靜態檔案(HTML、CSS、JS),可以部署到任何支援靜態檔案的伺服器。

Nginx 設定範例

server {
    listen 80;
    server_name your-domain.com;
    root /var/www/idempiere-mobile/build/web;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

macOS / Windows 部署

# macOS
flutter build macos --release
# 輸出: build/macos/Build/Products/Release/

# Windows
flutter build windows --release
# 輸出: build/windows/x64/runner/Release/

白牌設定(White-Label)

iDempiere Mobile 支援透過伺服器端的 AD_SysConfig 設定項進行白牌客製化,無需修改 App 程式碼:

設定鍵 預設值 說明
MOBILE_APP_NAME iDempiere Mobile App 標題列顯示的名稱
MOBILE_SEED_COLOR 2196F3 Material 3 主題色彩種子值(hex 色碼)
MOBILE_FEATURE_CHAT Y 啟用/停用聊天模組
MOBILE_FEATURE_BOOKING Y 啟用/停用預約模組
MOBILE_FEATURE_ATTENDANCE Y 啟用/停用出勤模組

這些設定由 BrandingConfig 在登入後從伺服器載入,自動套用到 App 的外觀和功能開關。

Material 3 色彩種子

MOBILE_SEED_COLOR 使用 hex 色碼(不含 #),Flutter 的 ColorScheme.fromSeed() 會自動產生完整的色彩方案。例如:

  • 2196F3 — 藍色(預設)
  • 4CAF50 — 綠色
  • FF5722 — 橘紅色
  • 9C27B0 — 紫色

API Key 安全設定

為強化 iDempiere Mobile App 的安全性,支援透過 Nginx 反向代理實施 API Key 驗證機制。只有持有正確 Key 的用戶端才能存取 iDempiere 後端 API。

運作原理

啟用 API Key 功能後,App 會在每個 API 請求中自動注入 HTTP 標頭:

X-API-KEY: <your-secret-key>
項目 說明
Header 名稱 X-API-KEY
儲存位置 SharedPreferences(nginx_api_keynginx_api_key_enabled
啟用方式 App 登入畫面 → 設定齒輪 → Nginx API Key 開關

Nginx 伺服器設定

在 Nginx 設定檔中加入 API Key 驗證區塊:

server {
    listen 443 ssl;
    server_name erp.example.com;

    # ... SSL 設定 ...

    location /api/ {
        # 1. 定義密鑰(避免特殊字元)
        set $api_secret "your-secret-key-here";

        # 2. 檢查 Header
        if ($http_x_api_key != $api_secret) {
            return 403; # 拒絕存取
        }

        # 3. 轉發至 iDempiere
        proxy_pass http://127.0.0.1:8080/api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

產生安全金鑰

# 方法 A:OpenSSL(推薦)— 產生 32 字元隨機 hex 字串
openssl rand -hex 16
# 範例輸出:8a4c2b9d3e5f1a7b8c9d0e1f2a3b4c5d

# 方法 B:Python
python3 -c "import secrets; print(secrets.token_urlsafe(24))"

Smart Scan QR Code 快速設定

App 登入畫面支援「Smart Scan」功能,掃描 QR Code 即可自動設定伺服器 URL 和 API Key,支援兩種格式:

Format A:JSON 格式(推薦)

同時設定伺服器 URL 和 API Key:

{
  "url": "https://erp.example.com",
  "key": "your-secret-api-key-123"
}
  • url:自動加入伺服器清單並選取
  • key:自動填入 API Key 欄位並啟用開關

Format B:純文字(僅 Key)

QR Code 內容為純字串時,僅設定 API Key,不變更伺服器 URL:

your-secret-api-key-123

使用者操作步驟

  1. 開啟 App,進入登入畫面
  2. 點擊右上角 設定齒輪 圖示
  3. 下捲至 Nginx API Key 區塊
  4. 勾選 啟用 核取方塊
  5. 點擊輸入欄旁的 QR Code 圖示
  6. 掃描管理員提供的設定 QR Code
  7. 伺服器 URL 和 API Key 自動填入完成

Firebase Cloud Messaging (FCM) 完整設定

推播通知功能(聊天訊息、簽核提醒)需要 Firebase Cloud Messaging (FCM)。App 在缺少 Firebase 設定時仍可正常運作 — FCM 功能會優雅地停用。

架構

┌─────────────┐     FCM HTTP v1 API     ┌───────────┐     APNs / FCM     ┌─────────────┐
│  iDempiere   │ ──────────────────────→ │  Google    │ ────────────────→ │  Flutter App │
│  Server      │   OAuth2 + JSON         │  FCM       │                   │  (iOS/Android)│
│              │                         └───────────┘                   └─────────────┘
│ tw.idempiere.firebase plugin                                           │
│  ├─ WorkflowFcmEventHandler (DS)                                      │ FCM token
│  ├─ FcmService (OAuth2 + send)                                        │ registration
│  └─ FcmTokenServlet (WAB)  ←──────────────────────────────────────────┘ POST /fcm/token
└─────────────┘

伺服器端:tw.idempiere.firebase OSGi 插件使用 WAB(Web Application Bundle)模式提供 /fcm/token endpoint。WorkflowFcmEventHandler 監聽 workflow 狀態變更事件,觸發 FcmService 透過 FCM HTTP v1 API + OAuth2 認證發送推播。

用戶端:Flutter App 在登入時透過 POST /fcm/token 註冊裝置 token。前景通知使用 flutter_local_notifications 顯示,背景和終止狀態的通知由 Firebase SDK 處理。

步驟一:建立 Firebase 專案

  1. 前往 Firebase Console
  2. 點擊 「Add project」
  3. 命名(例如 idempiere-mobile
  4. Google Analytics 可選擇停用
  5. 點擊 「Create project」

步驟二:新增 Android App

  1. Firebase Console → Project Overview → Add app → Android
  2. Package name:tw.idempiere.app(或您的 applicationId
  3. 下載 google-services.json
  4. 放置於 android/app/google-services.json

步驟三:新增 iOS App

  1. Firebase Console → Project Overview → Add app → iOS
  2. Bundle ID:tw.idempiere.app(須與 Xcode bundle identifier 一致)
  3. 下載 GoogleService-Info.plist
  4. 放置於 ios/Runner/GoogleService-Info.plist
  5. 重要:上傳 APNs Authentication Key(.p8 檔案)至 Firebase Console → Project Settings → Cloud Messaging → Apple app configuration

APNs Authentication Key 詳細步驟

APNs Key 是 iOS 推播通知的靈魂。沒有它,你的 APP 在 iOS 上就是個不會說話的啞巴。

  1. 前往 Apple Developer → Certificates, Identifiers & Profiles → Keys
  2. 點擊 + 建立新 Key,勾選 Apple Push Notifications service (APNs)
  3. 選擇環境:Sandbox(開發)vs Production(App Store/TestFlight)
  4. 下載 .p8 檔案 — 只能下載一次!丟了就只能重新建立。跟你的初戀日記一樣重要。
  5. 記下 Key ID(10 位字元,例如 ABC123DEF4
  6. 在 Firebase Console → Cloud Messaging → Apple app configuration:上傳 .p8、填入 Key ID + Team ID

小撇步:同一把 .p8 Key 可同時用於 Sandbox 和 Production slot。

iOS Entitlements 設定

ios/Runner/Runner.entitlements 中加入:

<key>aps-environment</key>
<string>development</string>

正式發布改為 production。在 Xcode → Runner target → Signing & Capabilities → 加入 Push Notifications

沒做這一步,你的 App 會像個不舉手的學生——老師(APNs)永遠不會點到他。

步驟四:產生 Firebase Options

方法 A — FlutterFire CLI(推薦):

dart pub global activate flutterfire_cli
flutterfire configure

方法 B — 手動設定:

若 CLI 無法使用,可手動建立 lib/firebase_options.dart,從 google-services.jsonGoogleService-Info.plist 中取得所需參數值。

步驟五:取得 Service Account Key

Service Account JSON 已內嵌在 tw.idempiere.firebase 插件的 resources/firebase-service-account.json 中。所有部署共用同一個 Firebase 專案(App 發布者的專案)。

若需重新產生/替換 key:

  1. 前往 Google Cloud Console → IAM & Admin → Service Accounts
  2. 選擇 firebase-adminsdk service account
  3. 指派以下角色(缺一不可,少一個就寄不出推播):
    • Editor — FCM v1 API 存取權限
    • Service Account Token Creator — JWT/OAuth2 token 產生權限
  4. 點擊 Keys 標籤 → Add KeyCreate new keyJSON
  5. 用下載的檔案替換插件中的 resources/firebase-service-account.json
  6. 重新建置插件 bundle

技術細節:FcmService 使用 OAuth2 scope https://www.googleapis.com/auth/cloud-platform。Service account 簽署 JWT,交換為 access token,再用它呼叫 FCM HTTP v1 API。FcmService 直接從內嵌的 JSON 讀取 project_id 和認證資訊 — 不需要 AD_SysConfig 設定。

步驟六:設定 iDempiere

執行 SQL:執行 tw.idempiere.firebase plugin 中的 sql/create_tables.sql 建立 TW_FCM_Token 資料表。

Plugin Bundle 結構

插件使用 WAB(Web Application Bundle)模式——跟一般的 OSGi bundle 不同,WAB 自帶 web.xml 可以直接當 servlet 用:

tw.idempiere.firebase/
├── META-INF/MANIFEST.MF        # Web-ContextPath: fcm, Jetty-Environment: ee8
├── WEB-INF/web.xml              # Maps FcmTokenServlet to /*
├── OSGI-INF/
│   ├── process_factory.xml
│   └── workflow_fcm_event_handler.xml
├── resources/
│   └── firebase-service-account.json
└── src/tw/idempiere/firebase/
    ├── ChatActivator.java           # OSGi activator(chat servlet, WebSocket)
    ├── FcmService.java              # OAuth2 + FCM HTTP v1 API 發送器
    ├── FcmTokenServlet.java         # POST/DELETE /fcm/token(WAB servlet)
    ├── TokenUtil.java               # JWT token 解碼
    ├── WorkflowFcmEventHandler.java # DS 組件,監聽 workflow 事件
    └── WorkflowFcmValidator.java    # 驗證哪些事件需觸發 FCM

關鍵 MANIFEST.MF headers:

  • Web-ContextPath: fcm — endpoint 路徑為 /fcm/*
  • Jetty-Environment: ee8 — 讓 iDempiere 的 Jetty 識別並載入此 WAB

部署 Plugin:tw.idempiere.firebase OSGi bundle 部署至 iDempiere 並重啟(或 hot-deploy)。

步驟七:驗證

  1. 登入 Flutter App — 在 debug console 確認出現 [FCM] Token registered with server
  2. 查詢 TW_FCM_Token 資料表確認裝置 token 已儲存
  3. 從其他使用者發送聊天訊息(App 處於背景)
  4. 確認系統通知列出現推播通知
  5. 點擊通知 → App 開啟並導向正確畫面

FCM 疑難排解

問題 解決方案
iOS 模擬器沒有通知 iOS Simulator 不支援推播 — 請使用實體裝置測試
[FCM] Failed to register token 確認 iDempiere 已部署 tw.idempiere.firebase plugin,且 /fcm/token endpoint 可存取
iOS 出現 [FCM] No FCM token APNs token 必須先到達才能取得 FCM token。確認 Xcode 已啟用 Push Notifications capability,且 entitlements 中有 aps-environment
THIRD_PARTY_AUTH_ERROR APNs key 未上傳或環境不符 — 在 Firebase Console 上傳 .p8 並確認匹配建置類型(Sandbox 對 debug、Production 對 release)
BadEnvironmentKeyInToken APNs key 環境不匹配 — debug builds 需要 Sandbox key,release builds 需要 Production key。搞混了就全部重來(別問我怎麼知道的)
FCM 403 Permission Denied Service account 需要 Google Cloud IAM 中的 Editor 角色
伺服器端 FCM 發送失敗 確認插件 bundle 中 resources/firebase-service-account.json 有效;檢查 iDempiere server log 中是否有 OAuth2 錯誤
通知出現但點擊無法導航 確認 data payload 包含 typeconversationUuid(聊天)或 activityId(簽核)
/fcm/token 回傳 404 確認 MANIFEST.MF 有 Web-ContextPath: fcmJetty-Environment: ee8;確認 bundle 內有 WEB-INF/web.xml

FCM Token 管理

// ApiClient 提供 FCM token 註冊/取消註冊:
await api.registerFcmToken(token);
await api.unregisterFcmToken(token);

通知偏好設定(TW_NotificationPref)

使用者可在 設定 → 通知偏好 中控制各類推播通知的開關。偏好儲存於 iDempiere 自訂表 TW_NotificationPref,每位使用者一筆記錄。

建表 SQL

CREATE TABLE TW_NotificationPref (
    TW_NotificationPref_ID   NUMERIC(10,0)   NOT NULL,
    TW_NotificationPref_UU   VARCHAR(36)     DEFAULT uuid_generate_v4(),
    AD_Client_ID             NUMERIC(10,0)   NOT NULL,
    AD_Org_ID                NUMERIC(10,0)   NOT NULL,
    IsActive                 CHAR(1)         DEFAULT 'Y' NOT NULL,
    Created                  TIMESTAMP       DEFAULT statement_timestamp() NOT NULL,
    CreatedBy                NUMERIC(10,0)   NOT NULL,
    Updated                  TIMESTAMP       DEFAULT statement_timestamp() NOT NULL,
    UpdatedBy                NUMERIC(10,0)   NOT NULL,
    AD_User_ID               NUMERIC(10,0)   NOT NULL,
    IsApprovalEnabled        CHAR(1)         DEFAULT 'Y' NOT NULL,
    IsAnnouncementEnabled    CHAR(1)         DEFAULT 'Y' NOT NULL,
    IsRequestEnabled         CHAR(1)         DEFAULT 'Y' NOT NULL,
    IsPollEnabled            CHAR(1)         DEFAULT 'Y' NOT NULL,
    IsBadgeEnabled           CHAR(1)         DEFAULT 'Y' NOT NULL,
    IsMissionEnabled         CHAR(1)         DEFAULT 'Y' NOT NULL,
    IsBirthdayEnabled        CHAR(1)         DEFAULT 'N' NOT NULL,
    CONSTRAINT tw_notificationpref_pkey PRIMARY KEY (TW_NotificationPref_ID)
);

CREATE UNIQUE INDEX tw_notificationpref_user_uk
    ON TW_NotificationPref (AD_Client_ID, AD_User_ID);

欄位說明

欄位 預設 說明
IsApprovalEnabled Y 簽核推播通知
IsAnnouncementEnabled Y 公告推播通知
IsRequestEnabled Y Request 推播通知
IsPollEnabled Y 投票推播通知
IsBadgeEnabled Y 榮譽徽章推播通知
IsMissionEnabled Y 任務推播通知
IsBirthdayEnabled N 生日推播通知(預設關閉)

容錯機制:TW_NotificationPref 表尚未建立,APP 會 catch HTTP 404 並使用內建預設值(全部開啟,生日除外),不會當機。

iDempiere AD 註冊

  1. Table and Column 視窗 → 新增 Table Name TW_NotificationPref,勾選 REST API Access
  2. 點選 Create Columns from DB 自動從實體表建立欄位
  3. AD_User_ID 欄位 Reference 設為 Table Direct
  4. 所有 Is* 欄位 Reference 設為 Yes-No

簽核模組增強功能

簽核模組在基礎的核准/駁回操作之上,提供四項增強功能:

1. 滑動手勢(Swipe Gestures)

使用 flutter_slidable 套件,在待簽核清單的每個項目上加入滑動操作:

  • 右滑:綠色核准按鈕 + 完整滑動直接核准
  • 左滑:橘色轉簽 + 紅色駁回
  • 多選模式下自動停用

2. 工作流程進度條(Workflow Stepper)

垂直進度條呈現工作流程各節點狀態,資料來自 workflowDiagramProvider

  • 依 Transition SeqNo 排序節點(處理循環)
  • 節點類型圖示:UserChoice = person、DocAction = settings
  • 狀態顏色:CC = 綠色 ✓、OS = 藍色 ●、AB = 紅色 ✕、未處理 = 灰色 ○

3. 每日簽核提醒(Daily Reminder)

使用 flutter_local_notificationszonedSchedule 排程每日 09:00 本機通知:

  • 啟用/停用偏好存於 SharedPreferences,不依賴伺服器表
  • 有待簽核項目時排程通知,無待簽核時取消
  • 登出時自動取消排程
  • 通知 payload {"type":"approval"} 路由至簽核畫面

4. 工作流程確認(Workflow Acknowledge)

呼叫 PUT /api/v1/workflow/acknowledge/{AD_WF_Activity_ID} 執行確認動作:

  • 與 approve/reject 遵循相同的 pattern:ApprovalResultType.acknowledged
  • 特殊處理 HTTP 304(伺服器回傳「無法確認」)轉換為 AppException
  • 操作成功後顯示 SnackBar 並刷新列表

可重複使用 UI 元件

下列元件設計為 table-agnostic,可在不同模組中重複使用:

AttachmentSection

通用附件管理區塊(attachment_section.dart),接收 tableNamerecordId 參數:

  • 顯示附件數量標題、上傳(檔案選擇器/相機)、下載預覽、刪除
  • 使用處:Window 記錄詳情、R_Request 詳情
  • 用法:AttachmentSection(tableName: 'R_Request', recordId: id, isReadWrite: true)

TimelineTab

通用變更歷程/評論時間軸(timeline_tab.dart),接收 tableNamerecordId 參數:

  • AD_ChangeLogChatMessage API 合併取得欄位變更及評論紀錄
  • 依時間倒序排列,欄位變更顯示 old→new,評論以對話泡泡呈現
  • 底部提供評論輸入列

動態必填邏輯(Dynamic Mandatory Logic)

Window 模組的編輯表單支援動態必填邏輯,語法與 DisplayLogic 相同:

@ColumnName@='value' & @OtherColumn@!='X' | @Flag@='Y'
  • FieldInfo.mandatoryLogic 從 AD_Column MandatoryLogic 欄位載入
  • effectiveMandatory 結合靜態 isMandatory 與動態 evaluateLogic()
  • 必填時:標籤加星號、validator 檢查非空
  • evaluateLogic() 支援 =!=&(AND)、|(OR)運算子

記錄表格檢視(Record Grid View)

記錄列表支援清單/表格兩種檢視模式切換:

  • RecordGridView widget 使用 DataTable,支援欄位排序與水平捲動
  • 最多顯示 8 個可見欄位,依使用者欄位偏好排列
  • 切換按鈕位於 AppBar 工具列(表格/清單圖示)

報表頁面導覽(Report Page Navigation)

PDF 報表檢視器支援完整頁面導覽:

  • 導覽列:首頁 → 上頁 → 頁碼指示器 → 下頁 → 末頁
  • 點擊頁碼指示器彈出跳頁對話框,驗證頁碼範圍
  • _pdfController!.setPage(pageIndex) 執行跳頁

疑難排解

症狀 原因 解決方案
DioException [connection error] API base URL 錯誤或伺服器無法連線 確認 baseUrl 設定正確,伺服器已啟動
XMLHttpRequest onError(Web) iDempiere 伺服器未設定 CORS 在 iDempiere 設定 CORS 標頭或使用反向代理
invalid_annotation_target 警告 freezed + json_serializable 已知問題 已在 analysis_options.yaml 中抑制
Locator 模糊匯入 與 state_notifier 的名稱衝突 對 flutter_riverpod 匯入加上 hide Locator
xcodebuild not found 僅安裝了 Xcode CLI 工具 從 App Store 安裝完整版 Xcode
產生的檔案遺失 修改模型後未執行 build_runner 執行 dart run build_runner build --delete-conflicting-outputs
白牌設定未生效 AD_SysConfig 尚未設定 在 iDempiere 的 AD_SysConfig 中新增對應的 key
推播通知無作用 Firebase 設定檔遺失 確認 google-services.json / GoogleService-Info.plist 已放置正確
iOS 模擬器建置失敗 CocoaPods 版本過舊 執行 cd ios && pod install --repo-update
Android Gradle 建置失敗 Gradle 版本不符 確認 android/gradle/wrapper/gradle-wrapper.properties 版本
No match found for window ID: XXXX(從簽核跳轉原始文件) 視窗的 slug 是純數字(如 "7005"),resolveSlug() 將其誤判為 Window ID 已在 window_repository.dart 修復:在拋出錯誤前,先以 _slugById.containsValue(slugOrId) 檢查是否為有效 slug

R&D 網路存取限制(部署端)

本功能限制研發 (R&D) 模組僅能在公司網路環境下使用,以保護研發機密資料。採用雙層防護架構:APP 端偵測 WiFi IP 提供 UX 提示,Nginx 端設定 IP 白名單進行安全強制。

完整功能說明(含 APP 端操作、運作流程、常見問題)請參閱使用手冊(Page 616)。本節僅說明系統管理員需執行的伺服器端部署步驟

步驟一:iDempiere SysConfig 設定

在 iDempiere 後台的 System Admin → General Rules → System Rules → System Configurator 新增兩筆設定:

Name Value(範例) 說明
RND_ALLOWED_IP_RANGES 192.168.1.0/24,10.10.0.0/16 允許存取 R&D 模組的 IP 網段(CIDR 格式,逗號分隔)
RND_RESTRICTED_ROUTES /dashboard/rnd/project,/dashboard/rnd/experiment,/dashboard/rnd/formula,... 需要網路限制的 APP 模組路由前綴

CIDR 格式說明:

  • 192.168.1.0/24 — 192.168.1.x(C 類子網)
  • 10.0.0.0/8 — 10.x.x.x(A 類子網)
  • 192.168.1.100/32 — 僅允許單一 IP

若未設定或 Value 為空,則不啟用任何網路限制。AD_Client_IDAD_Org_ID 通常設為 0(全系統適用)。

步驟二:Nginx 反向代理 IP 限制

對 R&D API 路徑設定 IP 白名單。此 location 區塊必須放在一般 API location 之前(Nginx 以先匹配的 regex 為準):

# R&D API 路徑 — IP 限制(放在一般 API proxy 之前)
location ~ ^/api/v1/models/RND_ {
    # 公司網路 IP 範圍 — 需與 SysConfig RND_ALLOWED_IP_RANGES 一致
    allow 192.168.1.0/24;
    allow 10.10.0.0/16;
    # 如有 VPN,加入 VPN 網段
    # allow 172.16.0.0/12;
    deny all;

    proxy_pass http://idempiere_backend;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

# 其他 API 路徑 — 不受限
location /api/v1/ {
    proxy_pass http://idempiere_backend;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

設定完成後執行:

# 測試設定語法
sudo nginx -t

# 重新載入
sudo nginx -s reload

重要:Nginx 的 allow 指令和 iDempiere SysConfig 的 RND_ALLOWED_IP_RANGES 必須手動保持一致。修改任一邊時,記得同步更新另一邊。

步驟三:APP 部署注意事項

平台 權限需求 說明
iOS NSLocationWhenInUseUsageDescription + WiFi Info Entitlement Apple 要求讀取 WiFi 資訊須有位置權限;需在 Apple Developer 後台啟用 Access WiFi Information capability
Android ACCESS_WIFI_STATE 已包含在 AndroidManifest.xml 中,不需使用者手動授權

驗證

# 從公司網路測試 — 應回傳 200
curl -I https://your-server/api/v1/models/RND_Project

# 從外部網路測試 — 應回傳 403
curl -I https://your-server/api/v1/models/RND_Project

安全層級

層級 防護方式 安全等級
APP 端 WiFi IP 偵測 + CIDR 比對 UX 層(可被繞過)
Nginx 端 來源 IP 白名單 安全強制層(伺服器端控制)
雙層合作 APP 提供 UX + Nginx 提供安全 完整防護

運作方式

看完部署步驟,你可能在想:「所以資料到底怎麼流的?」以下用流程圖回答你,省得你自己畫在白板上然後拍照傳 LINE 群組。

登入流程

使用者登入
  ↓
選擇 Client → Role → Org
  ↓
認證成功
  ↓
自動從 iDempiere 取得 SysConfig
(RND_ALLOWED_IP_RANGES + RND_RESTRICTED_ROUTES)
  ↓
偵測目前 WiFi IP
  ↓
比對 IP 是否在允許的網段內
  ↓
更新 APP 內部狀態

模組存取流程

使用者點擊 R&D 模組
  ↓
APP 檢查是否在公司網路
  ├─ 是 → 正常進入模組
  └─ 否 → 導向「需要公司網路」畫面
            ├─ [重新偵測網路] → 重新檢查 WiFi IP
            └─ [我知道了] → 返回首頁

重新偵測時機

  • APP 從背景回到前景時自動重新偵測
  • 使用者在鎖定畫面點擊「重新偵測網路」
  • 當 Nginx 回傳 403 時自動更新狀態

常見問題(FAQ)

以下集結了開發者和管理員最常問的七個問題。如果你的問題不在裡面,恭喜你發現了新的邊界案例。

問題 回答
如果不設定 SysConfig 會怎樣? 不設定就不啟用限制,研發模組照常開放。
如果只設定 SysConfig 不設定 Nginx? APP 端會顯示鎖定畫面,但可直接呼叫 API 繞過。只鎖前門不鎖後門。
使用者用行動數據(4G/5G)會怎樣? 沒有 WiFi IP,APP 判定不在公司網路。
家裡 WiFi 跟公司同網段? APP 可能被騙,但 Nginx 看公網 IP 會擋住。雙層防護的價值。
修改 SysConfig 後需重新登入? 是的。SysConfig 在登入時讀取並快取。
iOS 拒絕位置權限? 無法偵測 WiFi IP,直接判定不在公司網路。
如何暫時關閉限制? 清空 RND_ALLOWED_IP_RANGES Value。

Nginx 反向代理補充設定

前面的步驟二已設定基本 IP 白名單。這裡補充進階設定。

目的

阻擋公司外部請求存取 R&D API endpoints。

location ~ ^/api/v1/models/RND_ {
    allow 192.168.1.0/24;
    allow 10.10.0.0/16;
    deny all;
    proxy_pass http://idempiere_backend;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}
# 公司網路(預期 200)
curl -s -o /dev/null -w "%{http_code}" https://your-server/api/v1/models/RND_Project
# 外部網路(預期 403)
curl -s -o /dev/null -w "%{http_code}" https://your-server/api/v1/models/RND_Project

macOS 桌面版安全性

macOS Keychain 需要 Apple Developer 簽名。不想每年花 $99 USD?我們有替代方案。

項目 行動版 macOS 桌面版
儲存機制 FlutterSecureStorage SharedPreferences + AES-256
原因 系統級安全儲存 Keychain 需簽名

加密策略

  • 敏感欄位access_tokenrefresh_tokenpassword):AES-256 加密後存入 SharedPreferences
  • Encryption Key:由 tw.idempiere.mobile 衍生
  • 非敏感欄位:維持明文儲存

Shake-to-Report 回饋機制

搖一搖手機就能回報 Bug——比起在 LINE 群組裡打一大段「那個功能怪怪的」,這個方式顯然更有建設性。

架構

MaterialApp.router
  └── builder:
        └── ShakeFeedbackWrapper          ← Screenshot + ShakeDetector
              └── _OfflineBannerWrapper
                    └── Navigator (GoRouter)

ShakeFeedbackWrapper 位於 MaterialApp.routerbuilder callback 內,確保可存取 LocalizationsNavigatorTheme

檔案結構

lib/features/feedback/
├── data/
│   ├── device_info_collector.dart       # 靜態工具:收集裝置/App 資訊
│   └── feedback_repository.dart         # 建立 R_Request + 上傳截圖
├── domain/
│   └── feedback_notifier.dart           # FeedbackState + FeedbackNotifier
├── presentation/
│   └── feedback_sheet.dart              # Modal BottomSheet UI
└── shake/
    ├── shake_feedback_settings.dart     # 偏好開關(SharedPreferences)
    └── shake_feedback_wrapper.dart      # App 層級 wrapper

資料流程

User shakes phone
    │
    ▼
ShakeDetector.onPhoneShake
    │  ← check: enabled? cooldown? sheet open?
    ▼
ScreenshotController.capture()
    │
    ▼
DeviceInfoCollector.collect()
    │  ← device model, OS, app version, route, locale
    ▼
FeedbackSheet.show()
    │  ← user fills: problem type + description
    ▼
FeedbackNotifier.submitFeedback()
    │
    ▼
FeedbackRepository.submitFeedback()
    ├── ApiClient.createRecord('R_Request', data)
    └── UploadApi.uploadAndAttach(screenshot.png)

R_Request 欄位對應

R_Request 欄位 來源
Summary [問題類型] 描述前 50 字元
Result 使用者描述 + 自動收集的裝置資訊
R_RequestType_ID (尚未對應——類型標籤僅在 Summary 中)
附件 截圖 PNG,透過 Upload API

搖晃行為參數

參數
閾值 shakeThresholdGravity: 2.7(預設)
冷卻時間 3 秒
僅前景 AppLifecycleState.paused 暫停,.resumed 恢復
Sheet 保護 回饋 Sheet 已開啟時忽略搖晃
開關 SharedPreferences key shake_feedback_enabled,預設 true

套件依賴

套件 用途
shake ^3.0.0 加速度計搖晃偵測
screenshot ^3.0.0 Widget 截圖
device_info_plus ^11.0.0 裝置型號/OS 資訊
package_info_plus ^8.0.0 App 版本資訊

錯誤處理

情境 行為
截圖失敗 表單開啟但無預覽,允許純文字提交
網路錯誤 SnackBar 顯示本地化錯誤,表單保持開啟可重試
R_Request 建立失敗 SnackBar 顯示 AppException.localizedMessage(l10n)

Android Build & Deploy (Complete Guide)

Getting your Flutter App onto an Android phone — easier than you think, as long as your USB cable actually supports data transfer.

Prerequisites

Item Requirement
Flutter SDK ≥ 3.38
Android SDK Via Android Studio
Java JDK 17 (required by Flutter 3.x)
USB Cable One that actually transfers data

1. Enable Developer Mode on Phone

  1. Go to Settings → About Phone
  2. Tap Build Number 7 times → “You are now a developer!”
  3. Go to Settings → System → Developer Options
  4. Enable USB Debugging
  5. When connected via USB, tap Allow on the phone prompt

2. Verify Device Connection

flutter devices
# Expected: Pixel 7 (mobile) • abc12345 • android-arm64 • Android 14

3. Debug Mode (Development)

flutter run              # Auto-select device
flutter run -d abc12345  # Specify device

4. Build Release APK

# APK (universal, direct install)
flutter build apk --release

# App Bundle (Google Play)
flutter build appbundle --release

5. Install APK

adb install -r build/app/outputs/flutter-apk/app-release.apk
# Or: flutter install --release

6. One-Liner: Build + Install

flutter build apk --release && adb install -r build/app/outputs/flutter-apk/app-release.apk

7. APK Signing (Production Release)

# Generate keystore
keytool -genkey -v -keystore ~/idempiere-release.jks 
  -keyalias idempiere -keyalg RSA -keysize 2048 -validity 10000

Create android/key.properties (don’t commit to git), configure build.gradle.kts with signingConfigs, then build with flutter build apk --release.

Android FAQ

Issue Solution
flutter devices empty Check USB debugging, try a different cable, run adb devices
“App not installed” adb uninstall tw.idempiere.app then reinstall
Gradle build failure flutter clean && flutter pub get && flutter build apk --release

Shake-to-Report Feedback

Shake the phone to report bugs — much more constructive than typing “something’s broken” in a group chat.

Architecture

MaterialApp.router
  └── builder:
        └── ShakeFeedbackWrapper          ← Screenshot + ShakeDetector
              └── _OfflineBannerWrapper
                    └── Navigator (GoRouter)

ShakeFeedbackWrapper sits inside MaterialApp.router‘s builder callback to access Localizations, Navigator, and Theme.

Data Flow

User shakes phone → ShakeDetector → ScreenshotController.capture()
→ DeviceInfoCollector.collect() → FeedbackSheet.show()
→ FeedbackNotifier.submitFeedback()
→ FeedbackRepository: createRecord('R_Request') + uploadAndAttach(screenshot)

Shake Behavior

Parameter Value
Threshold shakeThresholdGravity: 2.7
Cooldown 3 seconds between triggers
Foreground only Pauses on AppLifecycleState.paused
Sheet guard Ignored if feedback sheet is already open
Toggle SharedPreferences key shake_feedback_enabled, default true

Error Handling

Scenario Behavior
Screenshot fails Form opens without preview, text-only submission
Network error SnackBar with localized error, form stays open for retry
R_Request creation fails SnackBar with AppException.localizedMessage(l10n)

Build Commands

flutter build apk          # Android APK
flutter build appbundle     # Android AAB (Play Store)
flutter build ios           # iOS (requires macOS + Xcode)
flutter build web           # Web (static files)
flutter build macos         # macOS
flutter build windows       # Windows

Android Deployment

Build with flutter build appbundle --release for Play Store. Upload the AAB to Google Play Console.

iOS Deployment

Build with flutter build ios --release, then archive in Xcode and distribute to App Store Connect.

Web Deployment

Build with flutter build web. Deploy the static files in build/web/ to any web server (Nginx, Apache, Firebase Hosting, etc.).

White-Label Configuration

Customize the app via iDempiere AD_SysConfig keys — no code changes needed:

Key Default Description
MOBILE_APP_NAME iDempiere Mobile App bar title
MOBILE_SEED_COLOR 2196F3 Material 3 theme seed color (hex)
MOBILE_FEATURE_CHAT Y Enable/disable chat module
MOBILE_FEATURE_BOOKING Y Enable/disable booking module
MOBILE_FEATURE_ATTENDANCE Y Enable/disable attendance module

Firebase Cloud Messaging (FCM) Setup

Push notifications (chat messages, approval reminders) require Firebase Cloud Messaging (FCM). The app works without Firebase — FCM features are gracefully disabled.

Architecture

┌─────────────┐     FCM HTTP v1 API     ┌───────────┐     APNs / FCM     ┌─────────────┐
│  iDempiere   │ ──────────────────────→ │  Google    │ ────────────────→ │  Flutter App │
│  Server      │   OAuth2 + JSON         │  FCM       │                   │  (iOS/Android)│
│              │                         └───────────┘                   └─────────────┘
│ tw.idempiere.firebase plugin                                           │
│  ├─ WorkflowFcmEventHandler (DS)                                      │ FCM token
│  ├─ FcmService (OAuth2 + send)                                        │ registration
│  └─ FcmTokenServlet (WAB)  ←──────────────────────────────────────────┘ POST /fcm/token
└─────────────┘

Server: The tw.idempiere.firebase OSGi plugin uses a WAB (Web Application Bundle) to serve the /fcm/token endpoint. WorkflowFcmEventHandler listens for workflow state changes and triggers FcmService to send notifications via FCM HTTP v1 API with OAuth2.

Client: The Flutter app registers its FCM device token on login via POST /fcm/token. Foreground notifications use flutter_local_notifications; background/terminated handled by Firebase SDK.

Steps 1-3: Create Firebase Project & Add Apps

  1. Create a Firebase project at Firebase Console
  2. Add Android app (package: tw.idempiere.app) — download google-services.json
  3. Add iOS app (bundle ID: tw.idempiere.mobile) — download GoogleService-Info.plist

APNs Authentication Key (Detailed Steps)

The APNs Key is the soul of iOS push notifications.

  1. Go to Apple Developer → Keys
  2. Create new Key, check APNs
  3. Environment: Sandbox (dev) vs Production (App Store/TestFlight)
  4. Download .p8one-time only!
  5. Note Key ID (10 chars)
  6. Firebase Console → Cloud Messaging → Apple config: upload .p8, enter Key ID + Team ID

Tip: Same .p8 works for both Sandbox and Production slots.

iOS Entitlements

Add to ios/Runner/Runner.entitlements:

<key>aps-environment</key>
<string>development</string>

Change to production for release. Enable Push Notifications in Xcode → Signing & Capabilities.

Step 5: Service Account Key

Service account JSON is bundled inside the plugin at resources/firebase-service-account.json. To regenerate:

  1. Google Cloud Console → IAM & Admin → Service Accounts
  2. Select firebase-adminsdk service account
  3. Assign roles: Editor (FCM v1 API access) + Service Account Token Creator (JWT/OAuth2 token generation)
  4. Keys tab → Add Key → Create new key → JSON
  5. Replace resources/firebase-service-account.json in the plugin and rebuild

FcmService reads project_id and credentials directly from the bundled JSON — no SysConfig needed. Uses OAuth2 scope https://www.googleapis.com/auth/cloud-platform.

Plugin Bundle Structure (WAB)

tw.idempiere.firebase/
├── META-INF/MANIFEST.MF        # Web-ContextPath: fcm, Jetty-Environment: ee8
├── WEB-INF/web.xml              # Maps FcmTokenServlet to /*
├── OSGI-INF/
│   ├── process_factory.xml
│   └── workflow_fcm_event_handler.xml
├── resources/
│   └── firebase-service-account.json
└── src/tw/idempiere/firebase/
    ├── ChatActivator.java           # OSGi activator (chat servlet, WebSocket)
    ├── FcmService.java              # OAuth2 + FCM HTTP v1 API sender
    ├── FcmTokenServlet.java         # POST/DELETE /fcm/token (WAB servlet)
    ├── TokenUtil.java               # JWT token decoding
    ├── WorkflowFcmEventHandler.java # DS component, listens for workflow events
    └── WorkflowFcmValidator.java    # Validates which events trigger FCM

Troubleshooting

Symptom Cause Fix
DioException [connection error] Wrong API URL or server unreachable Update baseUrl
XMLHttpRequest onError (web) CORS not configured Add CORS headers to iDempiere
Locator ambiguous import Name conflict with state_notifier Add hide Locator to riverpod import
Generated files missing build_runner not run Run dart run build_runner build
iOS Simulator no notifications Simulator doesn’t support push Use a real device
[FCM] Failed to register token Plugin not deployed Check tw.idempiere.firebase is deployed and /fcm/token is accessible
[FCM] No FCM token on iOS APNs token must arrive first Enable Push Notifications capability in Xcode; set aps-environment in entitlements
THIRD_PARTY_AUTH_ERROR APNs key not uploaded or wrong environment Upload .p8 to Firebase Console matching build type (Sandbox=debug, Production=release)
BadEnvironmentKeyInToken APNs key environment mismatch Debug builds need Sandbox key, release builds need Production key
FCM 403 Permission Denied Service account missing role Assign Editor role in Google Cloud IAM
FCM sends fail on server Invalid service account JSON Check resources/firebase-service-account.json in plugin bundle; check server logs for OAuth2 errors
Notification tap doesn’t navigate Missing data payload Verify data includes type + conversationUuid (chat) or activityId (approval)
/fcm/token returns 404 WAB not configured Check MANIFEST.MF has Web-ContextPath: fcm and Jetty-Environment: ee8; check WEB-INF/web.xml exists
iOS build failure CocoaPods outdated Run cd ios && pod install --repo-update
No match found for window ID: XXXX Window has numeric slug — resolveSlug() misidentifies as ID Fixed: fallback checks _slugById.containsValue(slugOrId)

Notification Preferences (TW_NotificationPref)

Users can control notification toggles in Settings → Notification Preferences. Preferences are stored in the iDempiere custom table TW_NotificationPref, one record per user.

Column Default Description
IsApprovalEnabled Y Approval push notifications
IsAnnouncementEnabled Y Announcement push notifications
IsRequestEnabled Y Request push notifications
IsPollEnabled Y Poll push notifications
IsBadgeEnabled Y Badge push notifications
IsMissionEnabled Y Mission push notifications
IsBirthdayEnabled N Birthday push notifications (off by default)

Fallback: If the TW_NotificationPref table does not exist, the app catches HTTP 404 and uses built-in defaults (all enabled except birthday).

Approval Enhancements

  • Swipe Gestures: Right-swipe to approve, left-swipe for forward/reject using flutter_slidable
  • Workflow Stepper: Vertical progress bar showing workflow node states (CC=green, OS=blue, AB=red)
  • Daily Reminder: Local notification at 09:00 daily when pending approvals exist
  • Workflow Acknowledge: PUT /api/v1/workflow/acknowledge/{activityId} for acknowledge action

Reusable UI Components

  • AttachmentSection: Table-agnostic attachment management (upload, download, delete). Usage: AttachmentSection(tableName: 'R_Request', recordId: id)
  • TimelineTab: Change log + comment timeline from AD_ChangeLog and ChatMessage APIs

Dynamic Mandatory Logic

Window edit forms support dynamic mandatory logic with the same syntax as DisplayLogic: @ColumnName@='value' & @OtherColumn@!='X'. Labels show asterisk when dynamically mandatory.

Record Grid View

Record lists support list/grid toggle. RecordGridView uses DataTable with column sorting and horizontal scrolling, showing up to 8 visible fields.

Report Page Navigation

PDF report viewer with full page navigation: first/previous/next/last page buttons, page indicator with jump-to-page dialog.

R&D Network Access Restriction

Restricts R&D module to company network. Two-layer defense: APP WiFi IP detection + Nginx IP whitelisting.

How It Works

Login → Fetch SysConfig → Detect WiFi IP → Match CIDRs → Update state

User taps R&D module → Check network
  ├─ Yes → Enter module
  └─ No  → "Company network required" screen

FAQ

Question Answer
SysConfig not set? No restrictions — R&D freely accessible.
Only SysConfig, no Nginx? APP lock screen shown, but API still callable.
Mobile data? No WiFi IP — treated as outside network.
Home WiFi same subnet? APP fooled, Nginx blocks by public IP.
Re-login after change? Yes. SysConfig cached at login.
iOS denies location? Cannot detect WiFi IP — locked out.
Disable temporarily? Clear RND_ALLOWED_IP_RANGES value.

macOS Desktop Security

macOS Keychain requires Apple Developer signing. Alternative: SharedPreferences + AES-256.

Item Mobile macOS Desktop
Storage FlutterSecureStorage SharedPreferences + AES-256
  • Sensitive (access_token, refresh_token, password): AES-256 encrypted
  • Key: derived from tw.idempiere.mobile
  • Non-sensitive: plaintext

Android ビルド & デプロイ(完全ガイド)

Flutter アプリを Android 端末にインストールする方法 — USB ケーブルがデータ転送対応であれば、思ったより簡単です。

前提条件

項目 要件
Flutter SDK ≥ 3.38
Android SDK Android Studio 経由
Java JDK 17(Flutter 3.x に必要)
USB ケーブル データ転送対応のもの

1. 開発者モードを有効にする

  1. 設定 → 端末情報に移動
  2. ビルド番号を 7 回タップ → 「デベロッパーになりました」
  3. 設定 → システム → 開発者向けオプション
  4. USB デバッグを有効にする
  5. USB 接続時、端末の確認ダイアログで許可をタップ

2. デバイス接続確認

flutter devices

3. デバッグモード実行

flutter run

4. リリース APK ビルド

flutter build apk --release    # APK(汎用)
flutter build appbundle --release  # App Bundle(Google Play)

5. APK インストール

adb install -r build/app/outputs/flutter-apk/app-release.apk

6. ワンライナー:ビルド + インストール

flutter build apk --release && adb install -r build/app/outputs/flutter-apk/app-release.apk

7. APK 署名(本番リリース用)

keytool -genkey -v -keystore ~/idempiere-release.jks 
  -keyalias idempiere -keyalg RSA -keysize 2048 -validity 10000

android/key.properties を作成し(git にコミットしないこと)、build.gradle.kts に signingConfigs を設定してください。

Android FAQ

問題 解決方法
flutter devices に表示されない USB デバッグ確認、ケーブル交換、adb devices で確認
「アプリはインストールされません」 adb uninstall tw.idempiere.app して再インストール
Gradle ビルドエラー flutter clean && flutter pub get && flutter build apk --release

Shake-to-Report フィードバック

端末を振ってバグを報告 — グループチャットで「なんかおかしい」と書くよりずっと建設的です。

アーキテクチャ

MaterialApp.router
  └── builder:
        └── ShakeFeedbackWrapper          ← Screenshot + ShakeDetector
              └── _OfflineBannerWrapper
                    └── Navigator (GoRouter)

ShakeFeedbackWrapperMaterialApp.routerbuilder コールバック内に配置され、LocalizationsNavigatorTheme にアクセスできます。

データフロー

端末を振る → ShakeDetector → ScreenshotController.capture()
→ DeviceInfoCollector.collect() → FeedbackSheet.show()
→ FeedbackNotifier.submitFeedback()
→ FeedbackRepository: createRecord('R_Request') + uploadAndAttach(screenshot)

シェイク動作

パラメータ
閾値 shakeThresholdGravity: 2.7
クールダウン 3 秒
フォアグラウンドのみ AppLifecycleState.paused で一時停止
シートガード フィードバックシートが開いている場合は無視
トグル SharedPreferences キー shake_feedback_enabled、デフォルト true

エラー処理

シナリオ 動作
スクリーンショット失敗 プレビューなしでフォームを表示、テキストのみの送信が可能
ネットワークエラー ローカライズされたエラーの SnackBar、フォームはリトライのため開いたまま
R_Request 作成失敗 AppException.localizedMessage(l10n) の SnackBar

ビルドコマンド

flutter build apk          # Android APK
flutter build appbundle     # Android AAB(Play Store 向け)
flutter build ios           # iOS(macOS + Xcode が必要)
flutter build web           # Web(静的ファイル)
flutter build macos         # macOS
flutter build windows       # Windows

Android デプロイ

Play Store 向けには flutter build appbundle --release でビルドします。AAB ファイルを Google Play Console にアップロードしてください。

iOS デプロイ

flutter build ios --release でビルド後、Xcode でアーカイブし、App Store Connect に配信します。

Web デプロイ

flutter build web でビルドします。build/web/ 内の静的ファイルを任意の Web サーバー(Nginx、Apache、Firebase Hosting など)にデプロイしてください。

ホワイトラベル設定

iDempiere の AD_SysConfig キーを使用してアプリをカスタマイズできます。コードの変更は不要です。

キー デフォルト値 説明
MOBILE_APP_NAME iDempiere Mobile アプリバーのタイトル
MOBILE_SEED_COLOR 2196F3 Material 3 テーマのシードカラー(16進数)
MOBILE_FEATURE_CHAT Y チャットモジュールの有効/無効
MOBILE_FEATURE_BOOKING Y 予約モジュールの有効/無効
MOBILE_FEATURE_ATTENDANCE Y 勤怠モジュールの有効/無効

Firebase Cloud Messaging(FCM)設定

プッシュ通知(チャットメッセージ、承認リマインダー)には FCM が必要です。Firebase なしでもアプリは正常動作 — FCM 機能は自動無効化されます。

アーキテクチャ

┌─────────────┐     FCM HTTP v1 API     ┌───────────┐     APNs / FCM     ┌─────────────┐
│  iDempiere   │ ──────────────────────→ │  Google    │ ────────────────→ │  Flutter App │
│  Server      │   OAuth2 + JSON         │  FCM       │                   │  (iOS/Android)│
│              │                         └───────────┘                   └─────────────┘
│ tw.idempiere.firebase plugin                                           │
│  ├─ WorkflowFcmEventHandler (DS)                                      │ FCM token
│  ├─ FcmService (OAuth2 + send)                                        │ registration
│  └─ FcmTokenServlet (WAB)  ←──────────────────────────────────────────┘ POST /fcm/token
└─────────────┘

サーバー側:tw.idempiere.firebase OSGi プラグインは WAB パターンで /fcm/token エンドポイントを提供。WorkflowFcmEventHandler がワークフロー状態変更を監視し、FcmService が FCM HTTP v1 API + OAuth2 で送信。

クライアント側:Flutter アプリはログイン時に POST /fcm/token でデバイストークンを登録。フォアグラウンド通知は flutter_local_notifications、バックグラウンド/終了状態は Firebase SDK が処理。

ステップ 1-3:Firebase プロジェクト作成 & アプリ追加

  1. Firebase Console でプロジェクトを作成
  2. Android アプリ追加(パッケージ:tw.idempiere.app)→ google-services.json をダウンロード
  3. iOS アプリ追加(Bundle ID:tw.idempiere.mobile)→ GoogleService-Info.plist をダウンロード

APNs Authentication Key(詳細手順)

APNs Key は iOS プッシュ通知の魂です。

  1. Apple Developer → Keys に移動
  2. 新 Key 作成、APNs にチェック
  3. 環境選択:Sandbox(開発)vs Production(App Store/TestFlight)
  4. .p8 ダウンロード — 一度だけ!
  5. Key ID(10文字)をメモ
  6. Firebase Console → Cloud Messaging → Apple config:.p8 アップロード、Key ID + Team ID 入力

ヒント:同じ .p8 は Sandbox/Production 両方で使用可能。

iOS Entitlements

ios/Runner/Runner.entitlements に追加:

<key>aps-environment</key>
<string>development</string>

リリースでは production。Xcode → Signing & Capabilities → Push Notifications を有効に。

ステップ 5:Service Account Key

Service Account JSON はプラグインの resources/firebase-service-account.json に内蔵されています。再生成するには:

  1. Google Cloud Console → IAM & Admin → Service Accounts
  2. firebase-adminsdk service account を選択
  3. ロールを割り当て:Editor(FCM v1 API アクセス)+ Service Account Token Creator(JWT/OAuth2 トークン生成)
  4. Keys タブ → Add Key → JSON
  5. プラグイン内の resources/firebase-service-account.json を置き換えて再ビルド

FcmService は内蔵 JSON から project_id と認証情報を直接読み取ります — SysConfig 設定不要。

プラグイン Bundle 構造(WAB)

tw.idempiere.firebase/
├── META-INF/MANIFEST.MF        # Web-ContextPath: fcm, Jetty-Environment: ee8
├── WEB-INF/web.xml              # FcmTokenServlet を /* にマッピング
├── OSGI-INF/
│   ├── process_factory.xml
│   └── workflow_fcm_event_handler.xml
├── resources/
│   └── firebase-service-account.json
└── src/tw/idempiere/firebase/
    ├── ChatActivator.java           # OSGi activator
    ├── FcmService.java              # OAuth2 + FCM HTTP v1 API 送信
    ├── FcmTokenServlet.java         # POST/DELETE /fcm/token
    ├── TokenUtil.java               # JWT トークンデコード
    ├── WorkflowFcmEventHandler.java # DS コンポーネント、ワークフローイベント監視
    └── WorkflowFcmValidator.java    # FCM トリガーイベントの検証

トラブルシューティング

症状 原因 解決策
DioException [connection error] API URL の誤りまたはサーバー接続不可 baseUrl を更新
XMLHttpRequest onError(Web) CORS が未設定 iDempiere に CORS ヘッダーを追加
Locator のあいまいなインポート state_notifier との名前衝突 riverpod インポートに hide Locator を追加
生成ファイルが見つからない build_runner が未実行 dart run build_runner build を実行
iOS シミュレータで通知なし シミュレータはプッシュ未対応 実機でテスト
[FCM] Failed to register token プラグイン未デプロイ tw.idempiere.firebase がデプロイ済みで /fcm/token がアクセス可能か確認
iOS で [FCM] No FCM token APNs トークンが先に必要 Xcode で Push Notifications capability を有効化、entitlements に aps-environment を設定
THIRD_PARTY_AUTH_ERROR APNs キー未アップロードまたは環境不一致 ビルドタイプに合わせて .p8 をアップロード(Sandbox=debug、Production=release)
BadEnvironmentKeyInToken APNs キー環境の不一致 debug ビルドは Sandbox キー、release ビルドは Production キーが必要
FCM 403 Permission Denied Service account のロール不足 Google Cloud IAM で Editor ロールを割り当て
サーバー側 FCM 送信失敗 Service Account JSON が無効 プラグイン bundle の resources/firebase-service-account.json を確認;サーバーログで OAuth2 エラーを確認
通知タップで画面遷移しない data payload 不足 datatype + conversationUuid(チャット)or activityId(承認)を含めること
/fcm/token が 404 WAB 未設定 MANIFEST.MF に Web-ContextPath: fcmJetty-Environment: ee8WEB-INF/web.xml を確認
iOS ビルドの失敗 CocoaPods のバージョンが古い cd ios && pod install --repo-update を実行
No match found for window ID: XXXX 数値のみの slug を Window ID と誤認 修正済み:_slugById.containsValue(slugOrId) で先にチェック

通知設定(TW_NotificationPref)

ユーザーは設定 → 通知設定で各種プッシュ通知のオン/オフを制御できます。設定は iDempiere カスタムテーブル TW_NotificationPref に保存されます(ユーザーごとに1レコード)。

カラム デフォルト 説明
IsApprovalEnabled Y 承認プッシュ通知
IsAnnouncementEnabled Y お知らせプッシュ通知
IsRequestEnabled Y リクエストプッシュ通知
IsPollEnabled Y 投票プッシュ通知
IsBadgeEnabled Y バッジプッシュ通知
IsMissionEnabled Y ミッションプッシュ通知
IsBirthdayEnabled N 誕生日プッシュ通知(デフォルトオフ)

フォールバック:TW_NotificationPref テーブルが存在しない場合、アプリは HTTP 404 をキャッチし、組み込みデフォルト値を使用します(誕生日以外すべて有効)。

承認モジュール拡張機能

  • スワイプジェスチャー:右スワイプで承認、左スワイプで転送/却下(flutter_slidable 使用)
  • ワークフローステッパー:ワークフローノード状態を表示する縦型プログレスバー(CC=緑、OS=青、AB=赤)
  • 毎日のリマインダー:保留中の承認がある場合、毎日 09:00 にローカル通知
  • ワークフロー確認:確認アクション用 PUT /api/v1/workflow/acknowledge/{activityId}

再利用可能な UI コンポーネント

  • AttachmentSection:テーブル非依存の添付ファイル管理(アップロード、ダウンロード、削除)
  • TimelineTab:AD_ChangeLogChatMessage API からの変更ログ+コメントタイムライン

動的必須ロジック

ウィンドウ編集フォームは DisplayLogic と同じ構文の動的必須ロジックをサポート:@ColumnName@='value' & @OtherColumn@!='X'。動的に必須の場合、ラベルにアスタリスクが表示されます。

レコードグリッドビュー

レコード一覧はリスト/グリッド切替をサポート。RecordGridViewDataTable を使用し、カラムソートと水平スクロールに対応、最大8個の表示フィールド。

レポートページナビゲーション

PDF レポートビューワーにフルページナビゲーション:先頭/前/次/末尾ページボタン、ジャンプダイアログ付きページインジケーター。

R&D ネットワークアクセス制限

R&D モジュールを社内ネットワークのみに制限。二層防御:APP WiFi IP 検出 + Nginx IP ホワイトリスト。

動作の仕組み

ログイン → SysConfig 取得 → WiFi IP 検出 → CIDR 照合 → 状態更新

R&D モジュールタップ → ネットワークチェック
  ├─ はい → モジュールに入る
  └─ いいえ → 「社内ネットワークが必要」画面

FAQ

質問 回答
SysConfig 未設定? 制限なし。R&D 自由アクセス。
SysConfig のみ? APP ロック画面表示、API 直接呼出可。
モバイルデータ? WiFi IP なし — 社外判定。
自宅 WiFi 同サブネット? APP 騙される、Nginx パブリック IP でブロック。
変更後再ログイン? はい。ログイン時キャッシュ。
iOS 位置情報拒否? WiFi IP 検出不可 — ロックアウト。
一時無効化? RND_ALLOWED_IP_RANGES クリア。

macOS デスクトップセキュリティ

macOS Keychain は Apple Developer 署名が必要。代替:SharedPreferences + AES-256。

項目 モバイル macOS
ストレージ FlutterSecureStorage SharedPreferences + AES-256
  • 機密access_tokenrefresh_tokenpassword):AES-256 暗号化
  • キーtw.idempiere.mobile から派生
  • 非機密:平文

按 Enter 搜尋,ESC 關閉