feat(mobile): implement invite friends share page with APK QR code & referral marketing

- SharePage: QR code now encodes actual APK download URL from admin-service
  (GET /api/v1/app/version/download/{id} via Kong), referral code shown
  separately below QR for manual entry on registration
- Add referral reward marketing section: direct bonus / team levels (50
  layers) / new user welcome package — drives viral growth
- Show referrer info card when current user was invited via referral code
- Share text updated to marketing copy with APK link + referral code
- VersionChecker.getLatestApkUrl(): fetches latest APK URL regardless of
  needUpdate (passes version=0.0.0 to force server response)
- UpdateService.getLatestApkUrl(): exposes APK URL fetch for UI layer
- i18n (zh-CN/zh-TW/en/ja): add 14 new keys for marketing content,
  APK download hints, reward program descriptions, and referrer info

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-07 10:17:59 -08:00
parent 192bc476c4
commit 9bcb1f864d
7 changed files with 351 additions and 145 deletions

View File

@ -811,6 +811,19 @@ const Map<String, String> en = {
'share.retry': 'Retry',
'share.inviteBanner': 'Invite Friends',
'share.inviteBannerSub': 'Earn rewards for every referral',
'share.scanToDownload': 'Scan to Download Genex App',
'share.apkUrlLoading': 'Fetching download link...',
'share.rewardPlanTitle': 'Referral Reward Program',
'share.rewardDirect': 'Direct Referral Bonus',
'share.rewardDirectDesc': 'Earn a reward for each friend who successfully registers',
'share.rewardTeam': 'Team Level Earnings',
'share.rewardTeamDesc': 'Earn from multi-level team performance, up to 50 referral levels',
'share.rewardNewbie': 'New User Exclusive Perks',
'share.rewardNewbieDesc': 'Friends who register with your code receive an exclusive welcome package',
'share.myReferrer': 'My Referrer',
'share.noReferrer': 'No referrer',
'share.joinWith': 'Use my referral code {code} to register on Genex. Scan to download:',
'share.shareTextApk': '[Genex Invite] {name} invites you to the digital coupon finance platform!\nSign up with referral code {code} and get exclusive new user rewards.\n📱 Scan or tap to download the App: {link}',
// ============ Notification ============
'notification.system': 'System',

View File

@ -812,6 +812,19 @@ const Map<String, String> ja = {
'share.retry': '再試行',
'share.inviteBanner': '友達を招待',
'share.inviteBannerSub': '友達を招待してポイントをゲット',
'share.scanToDownload': 'スキャンしてGenex Appをダウンロード',
'share.apkUrlLoading': 'ダウンロードリンクを取得中...',
'share.rewardPlanTitle': '紹介報酬プログラム',
'share.rewardDirect': '直接紹介ボーナス',
'share.rewardDirectDesc': '友達の登録成功ごとに紹介報酬を獲得',
'share.rewardTeam': 'チームレベル収益',
'share.rewardTeamDesc': '最大50レベルのチーム紹介チェーンで収益獲得',
'share.rewardNewbie': '新規ユーザー限定特典',
'share.rewardNewbieDesc': '紹介コードで登録した友達は新規ウェルカムパッケージを受け取れます',
'share.myReferrer': '紹介者',
'share.noReferrer': '紹介者なし',
'share.joinWith': '紹介コード {code} でGenexに登録。スキャンしてダウンロード',
'share.shareTextApk': '【Genexへのご招待】{name}さんがデジタルクーポン金融プラットフォームに招待しています!\n紹介コード {code} で登録すると新規特典がもらえます。\n📱 スキャンまたはタップしてダウンロード:{link}',
// ============ Notification ============
'notification.system': 'システム',

View File

@ -812,6 +812,19 @@ const Map<String, String> zhCN = {
'share.retry': '重试',
'share.inviteBanner': '邀请好友',
'share.inviteBannerSub': '推荐好友注册,双方均享福利',
'share.scanToDownload': '扫码下载 Genex App',
'share.apkUrlLoading': '正在获取下载链接...',
'share.rewardPlanTitle': '推荐奖励计划',
'share.rewardDirect': '直接推荐奖励',
'share.rewardDirectDesc': '每成功邀请一位好友注册,立即获得推荐奖励',
'share.rewardTeam': '团队层级收益',
'share.rewardTeamDesc': '多层团队业绩奖励,最高可追溯 50 层关系链',
'share.rewardNewbie': '新人专属福利',
'share.rewardNewbieDesc': '通过推荐码注册的用户可获得新人专属礼包',
'share.myReferrer': '我的推荐人',
'share.noReferrer': '暂无推荐人',
'share.joinWith': '使用我的推荐码 {code} 注册 Genex扫码立即下载',
'share.shareTextApk': '【Genex 邀请】{name} 邀请你加入数字券金融平台!\n使用推荐码 {code} 注册,即享新人专属福利。\n📱 扫码或点击下载 App{link}',
// ============ Notification ============
'notification.system': '系统通知',

View File

@ -812,6 +812,19 @@ const Map<String, String> zhTW = {
'share.retry': '重試',
'share.inviteBanner': '邀請好友',
'share.inviteBannerSub': '推薦好友註冊,雙方均享福利',
'share.scanToDownload': '掃碼下載 Genex App',
'share.apkUrlLoading': '正在獲取下載連結...',
'share.rewardPlanTitle': '推薦獎勵計劃',
'share.rewardDirect': '直接推薦獎勵',
'share.rewardDirectDesc': '每成功邀請一位好友註冊,立即獲得推薦獎勵',
'share.rewardTeam': '團隊層級收益',
'share.rewardTeamDesc': '多層團隊業績獎勵,最高可追溯 50 層關係鏈',
'share.rewardNewbie': '新人專屬福利',
'share.rewardNewbieDesc': '透過推薦碼註冊的用戶可獲得新人專屬禮包',
'share.myReferrer': '我的推薦人',
'share.noReferrer': '暫無推薦人',
'share.joinWith': '使用我的推薦碼 {code} 註冊 Genex掃碼立即下載',
'share.shareTextApk': '【Genex 邀請】{name} 邀請你加入數位券金融平台!\n使用推薦碼 {code} 註冊,即享新人專屬福利。\n📱 掃碼或點擊下載 App{link}',
// ============ Notification ============
'notification.system': '系統通知',

View File

@ -105,6 +105,15 @@ class UpdateService {
);
}
/// Android APK
///
/// version=0.0.0 downloadUrl
/// Android + selfHosted null
Future<String?> getLatestApkUrl() async {
if (!_isInitialized || _config.channel != UpdateChannel.selfHosted) return null;
return await _selfHostedUpdater?.versionChecker.getLatestApkUrl();
}
Future<void> manualCheckUpdate(BuildContext context) async {
if (!_isInitialized) return;

View File

@ -97,6 +97,37 @@ class VersionChecker {
return latestInfo?.forceUpdate ?? false;
}
/// APK needUpdate
///
/// version=0.0.0
/// Android iOS APK null
Future<String?> getLatestApkUrl() async {
if (!Platform.isAndroid) return null;
try {
final response = await _dio.get(
'/api/v1/app/version/check',
queryParameters: {
'app_type': 'GENEX_MOBILE',
'platform': 'ANDROID',
'current_version': '0.0.0',
'current_version_code': '0',
},
);
if (response.statusCode == 200 && response.data != null) {
final responseMap = response.data as Map<String, dynamic>;
final data = (responseMap['data'] as Map<String, dynamic>?) ?? responseMap;
final rawUrl = data['downloadUrl'] as String? ?? '';
if (rawUrl.isEmpty) return null;
if (!rawUrl.startsWith('http')) return '$apiBaseUrl$rawUrl';
return rawUrl;
}
return null;
} catch (e) {
debugPrint('[VersionChecker] 获取APK下载链接失败: $e');
return null;
}
}
///
Future<int> getVersionDiff() async {
try {

View File

@ -4,30 +4,25 @@
// /share ProfilePage
//
//
// 1. Hero Card
// · qr_flutter URL
// 1. Hero Card
// · qr_flutter APK
// ·
// 2. +
// 3. / /
// 4.
// 3. / /
// 4.
// 5. / /
// 6.
//
// App
// / QQ / WhatsApp / Telegram / Line / Twitter/X
// / / AirDrop / /
//
// admin-service APK
// https://api.gogenex.com/api/v1/app/version/download/{id}
//
//
//
// GET /api/v1/referral/my ReferralService.getMyInfo()
// Kafka
// Loading
//
//
// https://app.gogenex.com/download?ref={referralCode}
// App Store / Google Play /
// ref
// + APK 广
//
//
// qr_flutter ^4.1.0
// share_plus ^10.0.2 iOS Share Sheet / Android Sharesheet
// share_plus ^10.0.2
// ============================================================
import 'package:flutter/material.dart';
@ -38,23 +33,11 @@ import '../../../../app/theme/app_colors.dart';
import '../../../../app/theme/app_typography.dart';
import '../../../../app/theme/app_spacing.dart';
import '../../../../core/services/referral_service.dart';
import '../../../../core/updater/update_service.dart';
import '../../../../shared/widgets/genex_button.dart';
import '../../../../app/i18n/app_localizations.dart';
/// A9. / 广
///
///
/// initState _loadInfo() [Loading] [Content | Error]
///
/// Widget
/// ```
/// Scaffold
/// SingleChildScrollView
/// _buildHeroCard() QR +
/// _buildStatsCard() |
/// _buildShareActions() / /
/// GenexButton
/// ```
class SharePage extends StatefulWidget {
const SharePage({super.key});
@ -65,66 +48,67 @@ class SharePage extends StatefulWidget {
class _SharePageState extends State<SharePage> {
//
/// referral-service null
ReferralInfo? _info;
///
bool _loading = true;
///
String? _error;
//
/// App
///
///
/// 1. UAiOS / Android /
/// 2. iOS App Store
/// 3. Android Google Play APK
/// 4. `ref` localStorage
static const String _baseInviteUrl = 'https://app.gogenex.com/download';
/// admin-service APK
///
String? _apkUrl;
//
/// + 使
///
/// https://app.gogenex.com/download?ref=GNXAB2C3
/// https://app.gogenex.com/download
String get _inviteLink => _info != null
? '$_baseInviteUrl?ref=${_info!.referralCode}'
: _baseInviteUrl;
/// APK fallback API
String get _qrContent => _apkUrl ?? 'https://api.gogenex.com';
/// APK fallback
String get _shareLink => _apkUrl ?? 'https://api.gogenex.com';
//
@override
void initState() {
super.initState();
_loadInfo();
_loadData();
}
//
/// referral-service
///
/// [_info] loading
/// [_error]
Future<void> _loadInfo() async {
/// + APK
Future<void> _loadData() async {
setState(() {
_loading = true;
_error = null;
});
try {
final info = await ReferralService.instance.getMyInfo();
if (mounted) setState(() { _info = info; _loading = false; });
} catch (e) {
if (mounted) setState(() { _error = e.toString(); _loading = false; });
// referral + APK
final results = await Future.wait([
ReferralService.instance.getMyInfo().then<Object?>((v) => v).catchError((e) => e),
UpdateService().getLatestApkUrl().then<Object?>((v) => v).catchError((_) => null),
]);
if (!mounted) return;
final referralResult = results[0];
final apkUrlResult = results[1];
if (referralResult is ReferralInfo) {
setState(() {
_info = referralResult;
_apkUrl = apkUrlResult as String?;
_loading = false;
});
} else {
setState(() {
_error = referralResult.toString();
_apkUrl = apkUrlResult as String?;
_loading = false;
});
}
}
//
/// SnackBar +
void _showCopied(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@ -145,39 +129,26 @@ class _SharePageState extends State<SharePage> {
);
}
///
///
/// GNXAB2C3
Future<void> _copyCode() async {
if (_info == null) return;
await Clipboard.setData(ClipboardData(text: _info!.referralCode));
if (mounted) _showCopied(context.t('share.codeCopied'));
}
///
///
/// https://app.gogenex.com/download?ref=GNXAB2C3
Future<void> _copyLink() async {
await Clipboard.setData(ClipboardData(text: _inviteLink));
await Clipboard.setData(ClipboardData(text: _shareLink));
if (mounted) _showCopied(context.t('share.linkCopied'));
}
///
///
/// +
///
/// iOS Share SheetAirDrop
/// Android SharesheetWhatsAppTelegram
///
/// zh-CN
/// Genex 使 {code}
/// https://app.gogenex.com/download?ref={code}
/// = APK + +
Future<void> _shareNative() async {
final code = _info?.referralCode ?? '';
final text = context
.t('share.shareText')
// {name} name 'Genex 用户'
final template = context.t('share.shareTextApk');
final text = template
.replaceAll('{name}', 'Genex 用户')
.replaceAll('{code}', code)
.replaceAll('{link}', _inviteLink);
.replaceAll('{link}', _shareLink);
await Share.share(text, subject: context.t('share.nativeShareTitle'));
}
@ -199,7 +170,7 @@ class _SharePageState extends State<SharePage> {
),
body: _loading
? _buildLoading()
: _error != null
: _error != null && _info == null
? _buildError()
: _buildContent(),
);
@ -225,7 +196,6 @@ class _SharePageState extends State<SharePage> {
//
/// + +
Widget _buildError() {
return Center(
child: Padding(
@ -242,7 +212,7 @@ class _SharePageState extends State<SharePage> {
),
const SizedBox(height: 20),
TextButton.icon(
onPressed: _loadInfo,
onPressed: _loadData,
icon: const Icon(Icons.refresh_rounded),
label: Text(context.t('share.retry')),
style: TextButton.styleFrom(foregroundColor: AppColors.primary),
@ -261,13 +231,19 @@ class _SharePageState extends State<SharePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildHeroCard(), // QR +
_buildHeroCard(), // QR APK +
const SizedBox(height: 16),
_buildStatsCard(), //
_buildStatsCard(), //
const SizedBox(height: 16),
_buildShareActions(), //
_buildRewardPlanCard(), //
const SizedBox(height: 16),
if (_info?.usedCode != null) ...[
_buildReferrerCard(), //
const SizedBox(height: 16),
],
_buildShareActions(), //
const SizedBox(height: 24),
GenexButton( //
GenexButton(
label: context.t('share.shareToFriend'),
onPressed: _shareNative,
),
@ -277,18 +253,10 @@ class _SharePageState extends State<SharePage> {
);
}
// Hero Card +
// Hero CardAPK +
//
//
// Container[ + 20 + ]
// Column
// + "扫码下载 Genex"
// "Genex" 3FontWeight.w800
// Container[] QrImageView180×180
// +
//
// QR _inviteLink https://app.gogenex.com/download?ref=GNXAB2C3
// QR eye= #4834D4module= #6C5CE7=
// = APK admin-service
// Android iOS
Widget _buildHeroCard() {
return Container(
@ -307,14 +275,14 @@ class _SharePageState extends State<SharePage> {
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28),
child: Column(
children: [
//
//
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.auto_awesome_rounded, color: Colors.white60, size: 16),
const SizedBox(width: 6),
Text(
context.t('share.scanToJoin'),
context.t('share.scanToDownload'),
style: AppTypography.bodyMedium.copyWith(color: Colors.white70),
),
],
@ -328,10 +296,35 @@ class _SharePageState extends State<SharePage> {
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 24),
const SizedBox(height: 8),
//
// QrImageView
// APK
if (_apkUrl == null)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
width: 10,
height: 10,
child: CircularProgressIndicator(
color: Colors.white54,
strokeWidth: 1.5,
),
),
const SizedBox(width: 6),
Text(
context.t('share.apkUrlLoading'),
style: AppTypography.caption.copyWith(color: Colors.white54),
),
],
),
),
const SizedBox(height: 8),
// APK
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
@ -346,26 +339,23 @@ class _SharePageState extends State<SharePage> {
],
),
child: QrImageView(
data: _inviteLink, // URL
version: QrVersions.auto, //
data: _qrContent,
version: QrVersions.auto,
size: 180,
backgroundColor: Colors.white,
//
eyeStyle: const QrEyeStyle(
eyeShape: QrEyeShape.square,
color: Color(0xFF4834D4),
),
//
dataModuleStyle: const QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Color(0xFF6C5CE7),
),
),
),
const SizedBox(height: 24),
const SizedBox(height: 20),
//
//
//
Text(
context.t('share.myReferralCode'),
style: AppTypography.caption.copyWith(color: Colors.white60),
@ -376,14 +366,13 @@ class _SharePageState extends State<SharePage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15), //
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(40),
border: Border.all(color: Colors.white30),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// + 4
Text(
_info?.referralCode ?? '------',
style: AppTypography.h2.copyWith(
@ -394,7 +383,6 @@ class _SharePageState extends State<SharePage> {
),
),
const SizedBox(width: 12),
//
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
@ -420,6 +408,15 @@ class _SharePageState extends State<SharePage> {
),
),
),
//
const SizedBox(height: 12),
Text(
context.t('share.joinWith')
.replaceAll('{code}', _info?.referralCode ?? ''),
style: AppTypography.caption.copyWith(color: Colors.white54),
textAlign: TextAlign.center,
),
],
),
),
@ -427,13 +424,6 @@ class _SharePageState extends State<SharePage> {
}
//
//
// 线
// Icons.people
// Icons.groups
//
// ReferralInfo.directReferralCount / totalTeamCount
// 0
Widget _buildStatsCard() {
return Container(
@ -453,7 +443,6 @@ class _SharePageState extends State<SharePage> {
label: context.t('share.directReferrals'),
),
),
// 线
Container(width: 1, height: 44, color: AppColors.borderLight),
Expanded(
child: _buildStatItem(
@ -467,7 +456,6 @@ class _SharePageState extends State<SharePage> {
);
}
/// + +
Widget _buildStatItem({
required IconData icon,
required String value,
@ -489,18 +477,151 @@ class _SharePageState extends State<SharePage> {
);
}
//
//
//
// 1.
// 2. 50
// 3.
Widget _buildRewardPlanCard() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 10),
child: Text(
context.t('share.rewardPlanTitle'),
style: AppTypography.labelSmall.copyWith(color: AppColors.textTertiary),
),
),
Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: AppSpacing.borderRadiusMd,
border: Border.all(color: AppColors.borderLight),
boxShadow: AppSpacing.shadowSm,
),
child: Column(
children: [
_buildRewardItem(
icon: Icons.monetization_on_rounded,
iconBg: const Color(0xFFFFF3E0),
iconColor: const Color(0xFFFF9800),
title: context.t('share.rewardDirect'),
desc: context.t('share.rewardDirectDesc'),
),
const Divider(indent: 60, height: 1),
_buildRewardItem(
icon: Icons.account_tree_rounded,
iconBg: AppColors.infoLight,
iconColor: AppColors.info,
title: context.t('share.rewardTeam'),
desc: context.t('share.rewardTeamDesc'),
),
const Divider(indent: 60, height: 1),
_buildRewardItem(
icon: Icons.card_giftcard_rounded,
iconBg: AppColors.successLight,
iconColor: AppColors.success,
title: context.t('share.rewardNewbie'),
desc: context.t('share.rewardNewbieDesc'),
),
],
),
),
],
);
}
Widget _buildRewardItem({
required IconData icon,
required Color iconBg,
required Color iconColor,
required String title,
required String desc,
}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(color: iconBg, borderRadius: BorderRadius.circular(10)),
child: Icon(icon, size: 20, color: iconColor),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: AppTypography.bodyMedium.copyWith(fontWeight: FontWeight.w600)),
const SizedBox(height: 2),
Text(
desc,
style: AppTypography.caption.copyWith(color: AppColors.textSecondary),
),
],
),
),
],
),
);
}
//
//
// _info.usedCode != null
Widget _buildReferrerCard() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: AppSpacing.borderRadiusMd,
border: Border.all(color: AppColors.borderLight),
boxShadow: AppSpacing.shadowSm,
),
child: Row(
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: AppColors.primaryContainer,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.person_add_rounded, size: 20, color: AppColors.primary),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.t('share.myReferrer'),
style: AppTypography.caption.copyWith(color: AppColors.textTertiary),
),
const SizedBox(height: 2),
Text(
_info?.usedCode ?? context.t('share.noReferrer'),
style: AppTypography.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 1.5,
fontFamily: 'monospace',
),
),
],
),
),
],
),
);
}
//
//
// ListTile Divider
//
// [] GNXAB2C3 >
// [] https://... >
// [绿] /WhatsApp >
//
//
// _copyCode() + SnackBar
// _copyLink() + SnackBar
// _shareNative()
Widget _buildShareActions() {
return Column(
@ -522,7 +643,6 @@ class _SharePageState extends State<SharePage> {
),
child: Column(
children: [
//
_buildActionTile(
icon: Icons.qr_code_rounded,
iconBg: AppColors.primaryContainer,
@ -532,17 +652,15 @@ class _SharePageState extends State<SharePage> {
onTap: _copyCode,
),
const Divider(indent: 60, height: 1),
//
_buildActionTile(
icon: Icons.link_rounded,
iconBg: AppColors.infoLight,
iconColor: AppColors.info,
title: context.t('share.copyLink'),
subtitle: _inviteLink,
subtitle: _shareLink,
onTap: _copyLink,
),
const Divider(indent: 60, height: 1),
//
_buildActionTile(
icon: Icons.share_rounded,
iconBg: AppColors.successLight,
@ -558,10 +676,6 @@ class _SharePageState extends State<SharePage> {
);
}
///
///
/// - [iconBg] 使
/// - [subtitle] /
Widget _buildActionTile({
required IconData icon,
required Color iconBg,