隔离:最终版本

This commit is contained in:
ivan 2026-05-08 19:21:17 +08:00
parent 6747e97cf9
commit cb8f9dc085
8 changed files with 1275 additions and 18 deletions

View File

@ -10,6 +10,12 @@
android:maxSdkVersion="32" android:maxSdkVersion="32"
tools:replace="android:maxSdkVersion" /> tools:replace="android:maxSdkVersion" />
<uses-permission android:name="com.android.vending.BILLING" /> <uses-permission android:name="com.android.vending.BILLING" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="fb" />
</intent>
</queries>
<application <application
android:usesCleartextTraffic="${usesCleartextTraffic}" android:usesCleartextTraffic="${usesCleartextTraffic}"
android:label="PetsHero AI" android:label="PetsHero AI"

View File

@ -1,5 +1,5 @@
{ {
"version": "2.9", "version": "2.11",
"children": [ "children": [
{ {
"type": "frame", "type": "frame",
@ -499,7 +499,7 @@
"fill": { "fill": {
"type": "image", "type": "image",
"enabled": true, "enabled": true,
"url": "./images/generated-1773156213563.png", "url": "images/generated-1773156213563.png",
"mode": "fill" "mode": "fill"
}, },
"layout": "none", "layout": "none",
@ -667,7 +667,7 @@
"fill": { "fill": {
"type": "image", "type": "image",
"enabled": true, "enabled": true,
"url": "./images/generated-1773156252029.png", "url": "images/generated-1773156252029.png",
"mode": "fill" "mode": "fill"
}, },
"layout": "none", "layout": "none",
@ -825,7 +825,7 @@
"fill": { "fill": {
"type": "image", "type": "image",
"enabled": true, "enabled": true,
"url": "./images/generated-1773156289353.png", "url": "images/generated-1773156289353.png",
"mode": "fill" "mode": "fill"
}, },
"layout": "none", "layout": "none",
@ -993,7 +993,7 @@
"fill": { "fill": {
"type": "image", "type": "image",
"enabled": true, "enabled": true,
"url": "./images/generated-1773156316903.png", "url": "images/generated-1773156316903.png",
"mode": "fill" "mode": "fill"
}, },
"layout": "none", "layout": "none",
@ -1151,7 +1151,7 @@
"fill": { "fill": {
"type": "image", "type": "image",
"enabled": true, "enabled": true,
"url": "./images/generated-1773156352910.png", "url": "images/generated-1773156352910.png",
"mode": "fill" "mode": "fill"
}, },
"layout": "none", "layout": "none",
@ -1708,7 +1708,7 @@
"fill": { "fill": {
"type": "image", "type": "image",
"enabled": true, "enabled": true,
"url": "./images/generated-1773156157712.png", "url": "images/generated-1773156157712.png",
"mode": "fill" "mode": "fill"
}, },
"cornerRadius": 24, "cornerRadius": 24,
@ -1805,7 +1805,7 @@
"fill": { "fill": {
"type": "image", "type": "image",
"enabled": true, "enabled": true,
"url": "./images/generated-1773156363524.png", "url": "images/generated-1773156363524.png",
"mode": "fill" "mode": "fill"
}, },
"cornerRadius": 24, "cornerRadius": 24,
@ -1912,7 +1912,7 @@
"fill": { "fill": {
"type": "image", "type": "image",
"enabled": true, "enabled": true,
"url": "./images/generated-1773156393690.png", "url": "images/generated-1773156393690.png",
"mode": "fill" "mode": "fill"
}, },
"cornerRadius": 24, "cornerRadius": 24,
@ -2009,7 +2009,7 @@
"fill": { "fill": {
"type": "image", "type": "image",
"enabled": true, "enabled": true,
"url": "./images/generated-1773134913667.png", "url": "images/generated-1773134913667.png",
"mode": "fill" "mode": "fill"
}, },
"cornerRadius": 24, "cornerRadius": 24,
@ -2948,10 +2948,37 @@
}, },
{ {
"type": "frame", "type": "frame",
"id": "Lt3TV", "id": "omZCh",
"name": "navSpacer", "name": "recordEntry",
"width": 40, "height": 40,
"height": 40 "gap": 4,
"padding": [
0,
4
],
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "X2yWW",
"name": "recordEntryIcon",
"width": 16,
"height": 16,
"iconFontName": "list",
"iconFontFamily": "lucide",
"fill": "#8B5CF6"
},
{
"type": "text",
"id": "fjE0S",
"name": "recordEntryText",
"fill": "#8B5CF6",
"content": "History",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "600"
}
]
} }
] ]
}, },
@ -3941,7 +3968,7 @@
"fill": { "fill": {
"type": "image", "type": "image",
"enabled": true, "enabled": true,
"url": "./images/pet-hero-1.png", "url": "images/pet-hero-1.png",
"mode": "fill" "mode": "fill"
}, },
"cornerRadius": 16, "cornerRadius": 16,
@ -4281,7 +4308,7 @@
"fill": { "fill": {
"type": "image", "type": "image",
"enabled": true, "enabled": true,
"url": "./images/generated-1773156472555.png", "url": "images/generated-1773156472555.png",
"mode": "fill" "mode": "fill"
}, },
"cornerRadius": 16, "cornerRadius": 16,
@ -4513,7 +4540,7 @@
"fill": { "fill": {
"type": "image", "type": "image",
"enabled": true, "enabled": true,
"url": "./images/generated-1773156511265.png", "url": "images/generated-1773156511265.png",
"mode": "fill" "mode": "fill"
}, },
"cornerRadius": 16, "cornerRadius": 16,
@ -5017,6 +5044,373 @@
] ]
} }
] ]
},
{
"type": "frame",
"id": "HhcCM",
"x": 3560,
"y": 0,
"name": "积分记录页面",
"clip": true,
"width": 390,
"height": 844,
"fill": "#FAFAFA",
"layout": "vertical",
"children": [
{
"type": "frame",
"id": "QtoLk",
"name": "statusBar",
"width": "fill_container",
"height": 40,
"fill": "#FFFFFF",
"padding": [
0,
12,
0,
20
],
"justifyContent": "space_between",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "b5Ogx",
"name": "recordTime",
"fill": "#18181B",
"content": "9:41",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "600"
},
{
"type": "frame",
"id": "Su906",
"name": "recordStatusIcons",
"gap": 6,
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "MTrqx",
"name": "recordSignal",
"width": 21,
"height": 15,
"iconFontName": "signal",
"iconFontFamily": "lucide",
"fill": "#18181B"
},
{
"type": "icon_font",
"id": "fnm42",
"name": "recordWifi",
"width": 21,
"height": 15,
"iconFontName": "wifi",
"iconFontFamily": "lucide",
"fill": "#18181B"
},
{
"type": "icon_font",
"id": "nx60Z",
"name": "recordBattery",
"width": 30,
"height": 15,
"iconFontName": "battery-medium",
"iconFontFamily": "lucide",
"fill": "#18181B"
}
]
}
]
},
{
"type": "frame",
"id": "YfTGP",
"name": "topNav",
"width": "fill_container",
"height": 56,
"fill": "#FFFFFF",
"effect": {
"type": "shadow",
"shadowType": "outer",
"color": "#00000008",
"offset": {
"x": 0,
"y": 2
},
"blur": 8
},
"padding": [
0,
20
],
"justifyContent": "space_between",
"alignItems": "center",
"children": [
{
"type": "frame",
"id": "Napgn",
"name": "backBtn",
"width": 40,
"height": 40,
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "JQ8oi",
"name": "recordBackIcon",
"width": 24,
"height": 24,
"iconFontName": "arrow-left",
"iconFontFamily": "lucide",
"fill": "#18181B"
}
]
},
{
"type": "text",
"id": "vRkK0",
"name": "recordTitle",
"fill": "#18181B",
"content": "Points History",
"fontFamily": "Plus Jakarta Sans",
"fontSize": 20,
"fontWeight": "700"
},
{
"type": "frame",
"id": "hyBiG",
"name": "recordSpacer",
"width": 40,
"height": 40
}
]
},
{
"type": "frame",
"id": "yc0Rz",
"name": "content",
"width": "fill_container",
"height": "fill_container",
"fill": "#FAFAFA",
"layout": "vertical",
"gap": 16,
"padding": [
20,
20,
24,
20
],
"children": [
{
"type": "frame",
"id": "pBfPr",
"name": "summaryCard",
"width": "fill_container",
"fill": "#FFFFFF",
"cornerRadius": 16,
"stroke": {
"thickness": 1,
"fill": "#E4E4E7"
},
"effect": {
"type": "shadow",
"shadowType": "outer",
"color": "#00000008",
"offset": {
"x": 0,
"y": 2
},
"blur": 6
},
"layout": "vertical",
"gap": 8,
"padding": [
16,
18
],
"children": [
{
"type": "text",
"id": "aNlTU",
"name": "recordSummaryTitle",
"fill": "#71717A",
"content": "Current Points",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "500"
},
{
"type": "text",
"id": "Od6F1",
"name": "recordSummaryValue",
"fill": "#18181B",
"content": "1,280",
"fontFamily": "Inter",
"fontSize": 30,
"fontWeight": "700"
}
]
},
{
"type": "frame",
"id": "v25H1",
"name": "recordList",
"width": "fill_container",
"layout": "vertical",
"gap": 8,
"children": [
{
"type": "frame",
"id": "BzXpQ",
"name": "recordItem1",
"width": "fill_container",
"fill": "#FFFFFF",
"cornerRadius": 12,
"stroke": {
"thickness": 1,
"fill": "#E4E4E7"
},
"padding": [
14,
16
],
"justifyContent": "space_between",
"alignItems": "center",
"children": [
{
"type": "frame",
"id": "r3dpe",
"name": "recordItem1Left",
"layout": "vertical",
"gap": 4,
"children": [
{
"type": "text",
"id": "py0Ct",
"name": "recordItem1Time",
"fill": "#71717A",
"content": "2026-04-28 09:12",
"fontFamily": "Inter",
"fontSize": 12
}
]
},
{
"type": "text",
"id": "tWrM8",
"name": "recordItem1Change",
"fill": "#16A34A",
"content": "+20",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "700"
}
]
},
{
"type": "frame",
"id": "Jb2ZB",
"name": "recordItem2",
"width": "fill_container",
"fill": "#FFFFFF",
"cornerRadius": 12,
"stroke": {
"thickness": 1,
"fill": "#E4E4E7"
},
"padding": [
14,
16
],
"justifyContent": "space_between",
"alignItems": "center",
"children": [
{
"type": "frame",
"id": "elr68",
"name": "recordItem2Left",
"layout": "vertical",
"gap": 4,
"children": [
{
"type": "text",
"id": "riFsx",
"name": "recordItem2Time",
"fill": "#71717A",
"content": "2026-04-27 22:45",
"fontFamily": "Inter",
"fontSize": 12
}
]
},
{
"type": "text",
"id": "j8PFQw",
"name": "recordItem2Change",
"fill": "#DC2626",
"content": "-120",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "700"
}
]
},
{
"type": "frame",
"id": "gIDh0",
"name": "recordItem3",
"width": "fill_container",
"fill": "#FFFFFF",
"cornerRadius": 12,
"stroke": {
"thickness": 1,
"fill": "#E4E4E7"
},
"padding": [
14,
16
],
"justifyContent": "space_between",
"alignItems": "center",
"children": [
{
"type": "frame",
"id": "q8BKB8",
"name": "recordItem3Left",
"layout": "vertical",
"gap": 4,
"children": [
{
"type": "text",
"id": "q7lwb",
"name": "recordItem3Time",
"fill": "#71717A",
"content": "2026-04-26 14:08",
"fontFamily": "Inter",
"fontSize": 12
}
]
},
{
"type": "text",
"id": "zskzV",
"name": "recordItem3Change",
"fill": "#16A34A",
"content": "+500",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "700"
}
]
}
]
}
]
}
]
} }
] ]
} }

View File

@ -0,0 +1,679 @@
# Flutter Project Rewrite Refactor Playbook
## Purpose
This document adapts the generic "rewrite by a different engineer" refactor template to the current `pets_hero_ai` Flutter app.
The goal is a structural rewrite that makes the project look and feel newly organized while preserving behavior:
- UI output stays visually equivalent.
- API requests, parameters, encryption, authentication, payment, attribution, and analytics behavior stay equivalent.
- Existing app startup, login, referrer, home loading, generation, gallery, recharge, and profile flows keep working.
- The project compiles after each module migration.
This is not a feature rewrite. It is a controlled architecture migration.
## Current Project Shape
Current code is already partially feature-first:
- `lib/core/`
- API client, proxy encryption, auth, token storage, referrer, Adjust/Facebook events, theme, logging, user state.
- `lib/features/`
- `home`, `gallery`, `generate_video`, `recharge`, `profile`.
- `lib/shared/`
- shared tab selector and common widgets.
- Entrypoints:
- `lib/main.dart`
- `lib/app.dart`
Because the app already uses a `core/features/shared` layout, a plain Feature-first migration would not be different enough. The recommended target architecture is therefore a Clean Architecture-lite layout with vertical modules.
## Recommended Architecture
Choose this as the default unless there is a strong reason not to:
**Clean Architecture-lite + Vertical Modules**
Target layout:
```text
lib/
bootstrap/
main_bootstrap.dart
platform_startup.dart
app_shell/
pets_hero_app.dart
routes/
route_table.dart
route_observer.dart
lifecycle/
facebook_lifecycle_reporter.dart
foundation/
analytics/
adjust_event_gateway.dart
facebook_event_gateway.dart
auth/
device_identity_provider.dart
session_bootstrapper.dart
token_vault.dart
config/
api_environment.dart
facebook_environment.dart
logging/
tagged_log.dart
network/
api_response.dart
encrypted_proxy_client.dart
proxy_crypto.dart
service_locator.dart
referrer/
install_referrer_probe.dart
attribution_digest_builder.dart
attribution_polling_coordinator.dart
theme/
colors.dart
spacing.dart
typography.dart
theme_factory.dart
user/
account_state.dart
account_refresh_use_case.dart
device/
memory_profile.dart
modules/
home/
domain/
entities/
policies/
data/
home_catalog_api.dart
mappers/
application/
home_catalog_controller.dart
playback_visibility_coordinator.dart
presentation/
home_view.dart
components/
gallery/
domain/
data/
application/
presentation/
creation/
domain/
data/
application/
presentation/
generate_view.dart
progress_view.dart
result_view.dart
camera_view.dart
components/
billing/
domain/
data/
application/
presentation/
recharge_view.dart
points_history_view.dart
payment_webview_view.dart
profile/
domain/
data/
application/
presentation/
ui_kit/
navigation/
media/
badges/
tabs/
```
Mapping from current structure:
| Current | Target |
| --- | --- |
| `core/api/*` | `foundation/network/*` and module-specific `data/*_api.dart` |
| `core/auth/*` | `foundation/auth/*` |
| `core/referrer/*` | `foundation/referrer/*` |
| `core/adjust/*` | `foundation/analytics/*` |
| `core/theme/*` | `foundation/theme/*` |
| `core/user/*` | `foundation/user/*` |
| `shared/widgets/*` | `ui_kit/*` or module `presentation/components/*` |
| `features/home/*` | `modules/home/{domain,data,application,presentation}` |
| `features/generate_video/*` | `modules/creation/{domain,data,application,presentation}` |
| `features/recharge/*` | `modules/billing/{domain,data,application,presentation}` |
| `features/gallery/*` | `modules/gallery/{domain,data,application,presentation}` |
| `features/profile/*` | `modules/profile/{domain,data,application,presentation}` |
| `app.dart` | `app_shell/pets_hero_app.dart` |
| `main.dart` | `bootstrap/main_bootstrap.dart` plus thin `main.dart` |
## Architecture Choice Gate
Before implementation, choose one final target:
1. **Recommended: Clean Architecture-lite + Vertical Modules**
- Best fit because current project is already feature-first.
- Produces a clearly different layout while avoiding heavy dependency injection frameworks.
2. **Strict Clean Architecture**
- Adds explicit repository interfaces and use cases for every module.
- More ceremony, higher migration cost.
3. **MVVM + Repository**
- Good if UI state should be centralized in view models.
- Useful if future code will replace large `StatefulWidget` files with controllers.
4. **Custom**
- Must still define boundaries for app shell, foundation, modules, and UI kit before code is moved.
If uncertain, use option 1.
## Non-Negotiable Behavior Invariants
Preserve these exactly:
- Startup order:
- `ensureDeviceMemoryProfileInitialized`
- Adjust initialization
- first frame and native splash removal
- `AuthService` equivalent login bootstrap
- Android Google Play pending purchase listener
- order recovery after successful login
- API transport:
- encrypted proxy request wrapper
- V2 `sanctum` payload structure
- existing path, method, query, body semantics
- timeout and error behavior
- Authentication:
- device ID selection
- MD5 sign calculation
- local token restore/write
- fast login retry and failure telemetry
- Attribution:
- Adjust init and attribution callback
- Play Install Referrer fallback
- organic polling behavior
- post-login referrer reporting
- `common_info` refresh and home reload behavior
- Analytics:
- Adjust event tokens
- Facebook App Events names and parameters
- manual `activateApp`
- Facebook app install check log
- UI:
- same routes
- same navigation tabs
- same home/category/task loading behavior
- same recharge, gallery, generation, and profile behavior
- Payments:
- Google Play purchase stream subscription timing
- pending order recovery
- verification payloads
- webview payment polling behavior
## Call Sequence Invariants
Directory moves, class renames, and file splitting must not change async ordering. Treat the following sequences as contract tests.
### Cold Startup Sequence
Current order to preserve:
1. `WidgetsFlutterBinding.ensureInitialized()`.
2. Device memory profile initialization.
3. Adjust SDK initialization and attribution callback registration.
4. System UI overlay style setup.
5. `runApp(...)`.
6. Authentication bootstrap starts after `runApp`.
7. Android-only pending Google Play purchase listener starts immediately after auth bootstrap is kicked off.
8. Order recovery waits for login completion and only runs when startup succeeded.
9. First frame removes native splash, logs Facebook app install state, then reports Facebook `activateApp`.
Do not move auth bootstrap before `runApp`, do not delay the Google Play listener until after login, and do not run order recovery before login completion.
### Login and Initial API Sequence
Current order to preserve:
1. Short startup delay.
2. Resolve device ID.
3. Compute MD5 sign from device ID.
4. Resolve first referrer digest:
- race Adjust attribution timeout/callback;
- fallback to Play Install Referrer on Android.
5. Restore local auth token into the API client.
6. Call `POST /v1/user/fast_login` with:
- `origin`;
- `resolution`;
- `digest`.
7. On success:
- persist returned user token if present;
- send register events only when first-register flag is true;
- update credits/user/country state;
- start post-login referrer/common-info work in the background;
- mark startup succeeded.
8. On failure:
- mark startup failed;
- emit `LoginFaild` Facebook event once from the startup failure path.
Do not call authenticated business APIs before token restore and `fast_login` handling have reached the same point as before.
### Post-Login Referrer and Common Info Sequence
Current order to preserve:
1. Call `GET/SDK Adjust.getAttribution()` equivalent and build the Adjust digest.
2. Read Play Install Referrer digest.
3. Call `POST /v1/user/referrer` with `accolade=android_adjust`.
4. Call `POST /v1/user/referrer` with `accolade=gg`.
5. Call `GET /v1/user/common_info`.
6. Apply `common_info` fields into user/global state.
7. If home structure changed, request full home reload.
8. Start organic attribution polling only after this first post-login sequence has completed.
Organic polling end behavior to preserve:
1. Poll every 5 seconds.
2. Stop when attribution becomes non-organic or 5 minutes elapsed.
3. After polling stops, always rerun:
- `POST /v1/user/referrer` with `accolade=android_adjust`;
- `POST /v1/user/referrer` with `accolade=gg`;
- `GET /v1/user/common_info`.
4. Only after the rerun finishes, trigger home full reload so `GET /v1/image/img2video/categories` is requested with the refreshed server-side state.
Do not make the category refresh race ahead of referrer/common-info reporting.
### Home Loading Sequence
Current order to preserve:
1. Home view may be created before login finishes.
2. Category loading waits for login completion.
3. Call `GET /v1/image/img2video/categories`.
4. If `need_wait == true`, append the local `pets` pseudo-category.
5. Select the first category.
6. If the selected category is not `pets`, call `GET /v1/image/img2video/tasks`.
7. If the selected category is `pets`, use `extConfig.items` and do not call task list for `pets`.
8. After category/task updates, refresh playback visibility.
`UserState.requestHomeFullReload()` or its renamed equivalent must still re-enter this category loading sequence.
### Payment Sequence
Current order to preserve:
1. Android purchase stream listener starts early during app startup.
2. Google Play order recovery runs after login completion and only after successful startup.
3. Recharge purchase actions still emit tier/purchase events at the same user action points.
4. Successful purchase verification updates account state before user-visible balance-dependent UI relies on it.
5. Purchase success/failure Adjust and Facebook events keep the same event names/tokens and relative timing.
6. Third-party payment webview polling keeps the same retry intervals and terminal status handling.
### Navigation and Lifecycle Sequence
Current order to preserve:
1. Route names remain stable unless every navigation caller and documentation reference is migrated together.
2. Returning to home still resumes playback through the home playback nonce flow.
3. Foreground resume still reports Facebook `activateApp`.
4. Media picker flows still temporarily disable screen protection and restore it afterward.
## Refactor Rules
### Allowed
- Move files to new directories.
- Split large files by concern.
- Rename private classes, helpers, and variables when imports are updated.
- Rename public widget classes if all route references and imports are updated in the same step.
- Introduce barrel files only when they reduce noisy imports.
- Extract controllers/coordinators from large `StatefulWidget` files.
- Replace direct helper calls with small application-layer coordinator classes when behavior is unchanged.
### Not Allowed
- Changing endpoint paths, request parameters, event names, token keys, or analytics event tokens.
- Changing local storage keys unless a migration is added.
- Changing route names without updating all navigation callers.
- Changing product IDs, purchase completion behavior, or order recovery timing.
- Changing image/video playback rules while moving code.
- Adding new state-management packages only for the refactor.
- Reformatting or rewriting generated/platform files unrelated to the migration.
## File Splitting Targets
Prioritize these files because they currently carry several responsibilities:
- `lib/app.dart`
- App widget, route table, lifecycle reporting, startup overlay, main scaffold.
- Split into app shell, routes, lifecycle reporter, startup overlay, main scaffold.
- `lib/core/auth/auth_service.dart`
- Device identity, token restore, login retry, common info, referrer reporting, screen security.
- Split into session bootstrapper, device identity provider, token vault, common info synchronizer, referrer post-login coordinator.
- `lib/core/referrer/referrer_service.dart`
- Adjust digest, Play referrer, organic polling.
- Split into attribution digest builder, install referrer probe, attribution polling coordinator.
- `lib/features/home/home_screen.dart`
- Category loading, task loading, ext config parsing, playback visibility, UI layout.
- Split into home controller, catalog data source, playback visibility coordinator, home view components.
- `lib/features/generate_video/generate_video_screen.dart`
- UI, upload, creation request, media selection, camera/album coordination.
- Split into creation flow controller, upload coordinator, prompt/media components.
- `lib/features/recharge/recharge_screen.dart`
- Product rendering, purchase flow, third-party payment, event reporting.
- Split into billing controller, purchase coordinator, payment option components.
## Suggested Migration Order
Work in small batches. After each batch, run analysis and at least one release/debug build smoke check.
### Phase 0: Baseline
Capture current behavior before moving code:
- Run `flutter analyze`.
- Run the app with trace logs:
- `flutter run --release --dart-define=APP_LOG_LEVEL=trace`
- Verify logs for:
- Adjust init and attribution callback.
- Facebook install check.
- fast login.
- referrer reporting.
- `common_info`.
- categories and first task load.
- purchase listener startup on Android.
### Phase 1: App Shell
Move without changing behavior:
- `main.dart` becomes a thin entrypoint.
- App widget moves to `app_shell/pets_hero_app.dart`.
- Route definitions move to `app_shell/routes/route_table.dart`.
- Route observer moves to `app_shell/routes/route_observer.dart`.
- Startup overlay moves to `app_shell/lifecycle/startup_overlay.dart`.
- Facebook lifecycle/install-check logic moves to `app_shell/lifecycle/facebook_lifecycle_reporter.dart`.
Validation:
- App opens to the same tab.
- Native splash is removed at the same time.
- `activateApp` and install check logs still appear.
### Phase 2: Foundation Network and Config
Move:
- API config, API client, proxy client, crypto, response type.
- Logging.
- Facebook/Adjust config.
Validation:
- `fast_login`, `common_info`, category/task requests still show identical paths and body shapes in trace logs.
- No endpoint path changes.
### Phase 3: Foundation Auth and Attribution
Move and split:
- Auth/session bootstrapper.
- Device identity and sign generation.
- Token vault.
- Referrer digest and polling.
- Adjust/Facebook event gateways.
Validation:
- New install and returning-user startup both work.
- Natural/organic attribution polling still runs every 5s and stops after 5min or non-organic attribution.
- After polling stops, referrer is reported, `common_info` refreshes, then categories reload.
### Phase 4: UI Kit
Move shared visual widgets:
- Bottom navigation.
- Top navigation.
- Credits badge.
- Tab selector scope or replacement shell coordinator.
Validation:
- Bottom tabs, top bar, credits display, and route behavior are unchanged.
### Phase 5: Home Module
Move and split:
- Entities:
- category item
- task item
- ext config item
- Data:
- category/task API wrappers
- JSON mappers
- Application:
- category/task loading coordinator
- ext config policy
- playback visibility coordinator
- Presentation:
- home view
- tab row
- video card
Validation:
- `need_wait` behavior unchanged.
- `pets` pseudo-category behavior unchanged.
- Video autoplay visibility behavior unchanged.
- `UserState.requestHomeFullReload()` still reloads categories and tasks.
### Phase 6: Creation Module
Move:
- Generate screen.
- Progress screen.
- Result screen.
- In-app camera page.
- Album picker.
- Image compression helpers if only used by creation.
Validation:
- Album/camera selection works.
- Upload and task creation request shapes are unchanged.
- Progress polling and result navigation are unchanged.
### Phase 7: Gallery Module
Move:
- Gallery screen.
- Gallery task item.
- Upload cover store.
- Thumbnail cache.
Validation:
- Gallery loads the same data.
- Video thumbnails/cache behavior unchanged.
- Navigation to result/generate flows unchanged.
### Phase 8: Billing Module
Move:
- Recharge screen.
- Payment webview.
- Points history.
- Google Play purchase service.
- Payment models.
Validation:
- Products render the same.
- Google Play purchase and pending purchase recovery work.
- Third-party payment webview flow works.
- Purchase success/failure Adjust and Facebook events are unchanged.
### Phase 9: Profile Module
Move:
- Profile screen.
- Account deletion / account display dependencies if any.
Validation:
- Profile data, recharge navigation, account actions are unchanged.
### Phase 10: Cleanup
- Remove old empty directories.
- Remove transitional imports.
- Keep barrel files minimal.
- Update docs that reference old paths:
- `docs/app_startup.md`
- `docs/home.md`
- `docs/payment_flow.md`
- `docs/generate_video.md`
- `docs/user_login.md`
## Import Strategy
Use package imports for cross-module references:
```dart
import 'package:pets_hero_ai/foundation/network/api_response.dart';
import 'package:pets_hero_ai/modules/home/presentation/home_view.dart';
```
Avoid long chains of relative imports across modules.
Optional barrel files:
```text
foundation/network/network.dart
modules/home/home.dart
modules/billing/billing.dart
ui_kit/ui_kit.dart
```
Use a barrel only when at least three external files import the same group.
## Naming Style
Use a noticeably different naming style while keeping semantics clear:
- `AuthService``SessionBootstrapper`
- `ReferrerService``AttributionCoordinator`
- `ImageApi` category/task calls → `HomeCatalogApi`
- `UserState``AccountState`
- `AdjustEvents``MarketingEventGateway`
- `HomeScreen``HomeView`
- `GenerateVideoScreen``CreationView`
- `RechargeScreen``BillingView`
Prefer:
- `Coordinator` for orchestration.
- `Gateway` for third-party SDK/event wrappers.
- `Probe` for platform capability checks.
- `Vault` for local secure-ish persistence.
- `Policy` for pure decision logic.
- `Mapper` for JSON conversion.
## State Management Guidance
Do not add Bloc/Riverpod/Provider just to make the rewrite look different.
For this project, prefer lightweight application-layer controllers:
- Keep existing `ValueNotifier`-based global state where behavior depends on it.
- Extract complex `StatefulWidget` logic into controllers that are owned by the widget state.
- Use `ListenableBuilder`/`ValueListenableBuilder` where the existing behavior already relies on notifiers.
- Only replace `setState` when the extracted controller makes ownership clearer.
## Verification Checklist Per Phase
Run:
```sh
flutter analyze
```
For Android smoke checks:
```sh
flutter run --release --dart-define=APP_LOG_LEVEL=trace
```
Check logs for changed phases:
- Request paths and query/body fields.
- Auth/referrer/common_info sequence.
- Adjust/Facebook events.
- Home category/task reloads.
- Purchase listener and recovery startup.
For every phase that touches startup, auth, referrer, home, or billing, compare the trace log against a pre-refactor baseline. The comparison must verify order, not only presence:
```text
Adjust init
runApp
Auth bootstrap
Google Play listener
fast_login
post-login referrer(android_adjust)
post-login referrer(gg)
common_info
categories
tasks, when applicable
organic polling stop
referrer(android_adjust)
referrer(gg)
common_info
categories
```
If any request appears earlier/later than the baseline, stop the migration and decide explicitly whether the order change is acceptable. The default answer is no.
Manual UI checks:
- App launch.
- Home list and tabs.
- Generate flow.
- Gallery flow.
- Recharge flow.
- Profile flow.
## Recommended Work Protocol
1. Move one module or one foundation slice at a time.
2. Keep old behavior tests/manual traces open while migrating.
3. Update imports immediately after every move.
4. Run analyzer before continuing to the next phase.
5. Do not combine architecture moves with feature fixes.
6. If a behavior bug is discovered, document it separately and decide whether to fix before or after migration.
7. Commit after each compiling phase if using git commits.
## Completion Criteria
The refactor is complete only when:
- No imports reference the old `core/`, `features/`, or `shared/` directories unless those directories intentionally remain as compatibility shims.
- The app compiles cleanly.
- Startup, login, attribution, home reload, generation, gallery, recharge, and profile flows match baseline behavior.
- Trace logs show the same API paths and event names as before.
- Old large files have been split along the target architecture boundaries.
- Docs and any route/import examples use the new paths.

View File

@ -26,6 +26,10 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>FacebookAutoLogAppEventsEnabled</key> <key>FacebookAutoLogAppEventsEnabled</key>
<false/> <false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>fb</string>
</array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:facebook_app_events/facebook_app_events.dart'; import 'package:facebook_app_events/facebook_app_events.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:url_launcher/url_launcher.dart';
import 'core/auth/auth_service.dart'; import 'core/auth/auth_service.dart';
import 'core/nav/app_route_observer.dart'; import 'core/nav/app_route_observer.dart';
@ -46,6 +47,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// Remove native splash; Flutter login overlay takes over if startup is still running. // Remove native splash; Flutter login overlay takes over if startup is still running.
FlutterNativeSplash.remove(); FlutterNativeSplash.remove();
unawaited(_logFacebookInstallState('first_frame'));
_reportFacebookActivateApp('first_frame'); _reportFacebookActivateApp('first_frame');
}); });
} }
@ -65,6 +67,22 @@ class _AppState extends State<App> with WidgetsBindingObserver {
}); });
} }
/// Detect whether a local app can handle Facebook's URL scheme.
///
/// iOS requires `LSApplicationQueriesSchemes` to include `fb`; Android 11+
/// requires a matching `<queries>` entry in AndroidManifest.
Future<void> _logFacebookInstallState(String reason) async {
const scheme = 'fb://';
try {
final installed = await canLaunchUrl(Uri.parse(scheme));
_fbLog.d(
'Facebook app install check ($reason): installed=$installed scheme=$scheme',
);
} catch (e, st) {
_fbLog.e('Facebook app install check failed ($reason)', e, st);
}
}
/// Foreground resume; cold start also uses addPostFrameCallback in initState. /// Foreground resume; cold start also uses addPostFrameCallback in initState.
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {

View File

@ -553,6 +553,20 @@ class AuthService {
static Future<void> _runPostLoginReferrerWork(String uid, String deviceId) async { static Future<void> _runPostLoginReferrerWork(String uid, String deviceId) async {
try { try {
await _reportBothReferrersAndRefreshCommonInfo(uid, deviceId); await _reportBothReferrersAndRefreshCommonInfo(uid, deviceId);
// 5s 5min
// + common_info
// _loadCategories
unawaited(ReferrerService.startOrganicPolling(
onStopped: (detectedNonOrganic) async {
try {
await _reportBothReferrersAndRefreshCommonInfo(uid, deviceId);
} catch (e, st) {
_logMsg('organic poll: 重新上报归因/common_info 异常: $e');
_logMsg('organic poll: 堆栈 $st');
}
UserState.requestHomeFullReload();
},
));
} catch (e, st) { } catch (e, st) {
_logMsg('referrer/common_info 后台任务异常: $e'); _logMsg('referrer/common_info 后台任务异常: $e');
_logMsg('referrer/common_info 堆栈: $st'); _logMsg('referrer/common_info 堆栈: $st');

View File

@ -6,10 +6,14 @@ import 'package:adjust_sdk/adjust.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:play_install_referrer/play_install_referrer.dart'; import 'package:play_install_referrer/play_install_referrer.dart';
import '../log/app_logger.dart';
/// Adjust fallback 使 Play Install Referrer /// Adjust fallback 使 Play Install Referrer
class ReferrerService { class ReferrerService {
ReferrerService._(); ReferrerService._();
static final _log = AppLogger('ReferrerService');
static String? _cachedReferrer; static String? _cachedReferrer;
static String _referrerSource = 'gg'; static String _referrerSource = 'gg';
static final Completer<String?> _completer = Completer<String?>(); static final Completer<String?> _completer = Completer<String?>();
@ -27,6 +31,15 @@ class ReferrerService {
/// Play Install Referrer / /// Play Install Referrer /
static const Duration _playInstallReferrerTimeout = Duration(seconds: 10); static const Duration _playInstallReferrerTimeout = Duration(seconds: 10);
///
static const Duration _organicPollInterval = Duration(seconds: 5);
///
static const Duration _organicPollMaxDuration = Duration(minutes: 5);
static bool _organicPollRunning = false;
static Timer? _organicPollTimer;
/// Adjust attributionCallback getAttributionWithTimeout /// Adjust attributionCallback getAttributionWithTimeout
static void receiveAttributionFromCallback(AdjustAttribution attribution) { static void receiveAttributionFromCallback(AdjustAttribution attribution) {
if (!_attributionCallbackCompleter.isCompleted) { if (!_attributionCallbackCompleter.isCompleted) {
@ -143,4 +156,133 @@ class ReferrerService {
return ''; return '';
} }
} }
/// Adjust Play Install Referrer
/// [_organicPollInterval]
/// [_organicPollMaxDuration] [onStopped]
///
///
static Future<void> startOrganicPolling({
required Future<void> Function(bool detectedNonOrganic) onStopped,
}) async {
if (_organicPollRunning) {
_log.d('startOrganicPolling: already running, skip');
return;
}
final initiallyOrganic = await _isCurrentlyOrganic();
if (!initiallyOrganic) {
_log.d('startOrganicPolling: 初始归因即为渠道,无需轮询');
return;
}
_organicPollRunning = true;
final startedAt = DateTime.now();
var tickCount = 0;
_log.d(
'startOrganicPolling: 启动自然量轮询,每 ${_organicPollInterval.inSeconds}s 一次,最长 ${_organicPollMaxDuration.inMinutes}min');
Future<void> finish(bool detectedNonOrganic, {required String reason}) async {
_stopOrganicPoll();
_log.d('startOrganicPolling: 结束,原因=$reason changed=$detectedNonOrganic');
try {
await onStopped(detectedNonOrganic);
} catch (e, st) {
_log.d('startOrganicPolling: onStopped 回调异常: $e');
_log.d('startOrganicPolling: stack $st');
}
}
Future<void> tick() async {
if (!_organicPollRunning) return;
tickCount++;
final elapsed = DateTime.now().difference(startedAt);
if (elapsed >= _organicPollMaxDuration) {
await finish(false, reason: 'timeout');
return;
}
final adjustNetwork = await _readAdjustNetwork();
final adjustOrganic = _attributionIsOrganicByNetwork(adjustNetwork);
final pirRef = await _readPlayInstallReferrerRaw();
final pirOrganic = _installReferrerIsOrganic(pirRef);
final stillOrganic = adjustOrganic && pirOrganic;
_log.d(
'organic poll tick#$tickCount elapsed=${elapsed.inSeconds}s '
'adjustNetwork="${adjustNetwork ?? '<null>'}" adjustOrganic=$adjustOrganic '
'pir="$pirRef" pirOrganic=$pirOrganic stillOrganic=$stillOrganic');
if (!_organicPollRunning) return;
if (!stillOrganic) {
await finish(true, reason: 'detectedNonOrganic');
return;
}
_organicPollTimer = Timer(_organicPollInterval, () {
if (!_organicPollRunning) return;
unawaited(tick());
});
}
_organicPollTimer = Timer(_organicPollInterval, () {
if (!_organicPollRunning) return;
unawaited(tick());
});
}
static void _stopOrganicPoll() {
_organicPollTimer?.cancel();
_organicPollTimer = null;
_organicPollRunning = false;
}
/// Adjust Play Install Referrer true
static Future<bool> _isCurrentlyOrganic() async {
final adjustNetwork = await _readAdjustNetwork();
final adjustOrganic = _attributionIsOrganicByNetwork(adjustNetwork);
final pirRef = await _readPlayInstallReferrerRaw();
final pirOrganic = _installReferrerIsOrganic(pirRef);
return adjustOrganic && pirOrganic;
}
/// Adjust network null
static Future<String?> _readAdjustNetwork() async {
try {
final attr = await Adjust.getAttribution();
return attr.network;
} catch (_) {
return null;
}
}
/// network / 'organic'
static bool _attributionIsOrganicByNetwork(String? network) {
final n = (network ?? '').trim().toLowerCase();
return n.isEmpty || n == 'organic';
}
/// Play Install Referrer Android /
static Future<String> _readPlayInstallReferrerRaw() async {
if (defaultTargetPlatform != TargetPlatform.android) return '';
try {
final details = await PlayInstallReferrer.installReferrer
.timeout(_playInstallReferrerTimeout);
return details.installReferrer ?? '';
} catch (_) {
return '';
}
}
/// Google Play referrer`utm_source=google-play&utm_medium=organic`
/// / `utm_medium=`
static bool _installReferrerIsOrganic(String ref) {
final s = ref.trim();
if (s.isEmpty) return true;
final lower = s.toLowerCase();
if (lower.contains('utm_medium=organic')) return true;
if (!lower.contains('utm_medium=')) return true;
return false;
}
} }

View File

@ -1,7 +1,7 @@
name: pets_hero_ai name: pets_hero_ai
description: PetsHero AI Application. description: PetsHero AI Application.
publish_to: 'none' publish_to: 'none'
version: 1.2.0+120 version: 1.2.1+121
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'