Compare commits

...

3 Commits

Author SHA1 Message Date
roxy
78c3a95160 信息类型增加_邀请奖励 2026-04-28 10:07:29 +08:00
roxy
24c4a4b70c 注册邀请码 2026-04-27 19:30:20 +08:00
roxy
ddd50a65dc 邀请活动 2026-04-27 14:59:22 +08:00
22 changed files with 667 additions and 164 deletions

View File

@ -85,13 +85,14 @@ android {
}
}
}
dependencies {
implementation("androidx.multidex:multidex:2.0.1")
// implementation(platform("com.google.firebase:firebase-bom:34.0.0"))
// implementation("com.google.firebase:firebase-auth")
// implementation("com.google.firebase:firebase-core:16.0.8")
// implementation("com.google.android.gms:play-services-auth:21.1.1")
}
dependencies {
implementation("androidx.multidex:multidex:2.0.1")
implementation("com.android.installreferrer:installreferrer:2.2")
// implementation(platform("com.google.firebase:firebase-bom:34.0.0"))
// implementation("com.google.firebase:firebase-auth")
// implementation("com.google.firebase:firebase-core:16.0.8")
// implementation("com.google.android.gms:play-services-auth:21.1.1")
}
flutter {

View File

@ -1,10 +1,83 @@
package com.org.yumiparty
import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerStateListener
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private var installReferrerClient: InstallReferrerClient? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) // 这行必须存在!
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
INSTALL_REFERRER_CHANNEL
).setMethodCallHandler { call, result ->
when (call.method) {
"getInstallReferrer" -> getInstallReferrer(result)
else -> result.notImplemented()
}
}
}
override fun onDestroy() {
installReferrerClient?.endConnection()
installReferrerClient = null
super.onDestroy()
}
private fun getInstallReferrer(result: MethodChannel.Result) {
val client = InstallReferrerClient.newBuilder(this).build()
installReferrerClient = client
var hasResponded = false
fun respondOnce(value: String?) {
if (hasResponded) return
hasResponded = true
result.success(value)
client.endConnection()
if (installReferrerClient === client) {
installReferrerClient = null
}
}
fun respondError(errorCode: String, message: String?) {
if (hasResponded) return
hasResponded = true
result.error(errorCode, message, null)
client.endConnection()
if (installReferrerClient === client) {
installReferrerClient = null
}
}
try {
client.startConnection(object : InstallReferrerStateListener {
override fun onInstallReferrerSetupFinished(responseCode: Int) {
when (responseCode) {
InstallReferrerClient.InstallReferrerResponse.OK -> {
try {
respondOnce(client.installReferrer.installReferrer)
} catch (e: Exception) {
respondError("INSTALL_REFERRER_ERROR", e.message)
}
}
else -> respondOnce(null)
}
}
override fun onInstallReferrerServiceDisconnected() {
respondOnce(null)
}
})
} catch (e: Exception) {
respondError("INSTALL_REFERRER_CONNECTION_ERROR", e.message)
}
}
companion object {
private const val INSTALL_REFERRER_CHANNEL = "com.org.yumiparty/install_referrer"
}
}

View File

@ -32,7 +32,7 @@
"pleaseEnterContent": "يرجى إدخال المحتوى",
"profilePhoto": "صورة الملف الشخصي",
"noHistoricalRecordsAvailable": "لا توجد سجلات تاريخية متاحة.",
"inviteNewUsersToEarnCoins": "ادعُ مستخدمين جدد لكسب العملات",
"inviteNewUsersToEarnCoins": "ادعُ مستخدمين جدد لكسب العملات",
"crateMyRoom": "أنشئ غرفتك الخاصة.",
"casualInteraction": "التفاعل العفوي",
"haveGamePlayingTips": "لديك لعبة جارية، يرجى الخروج من اللعبة الحالية أولاً، هل أنت متأكد من الخروج؟",
@ -308,6 +308,7 @@
"ownerSendTheRedEnvelope": "أرسل مالك الغرفة عملات المكافأة.",
"rewardCoins": "عملات المكافأة:{1} عملة",
"signInRewardReceived": "تم تسجيل الدخول بنجاح. المكافأة: {1}",
"registerRewardReceived": "تم استلام مكافأة التسجيل: {1}",
"lastWeekProgress": "تقدم الأسبوع الماضي",
"currentProgress": "التقدم الحالي",
"coins2": "{1} عملات",

View File

@ -162,6 +162,7 @@
"ownerSendTheRedEnvelope": "মালিক পুরস্কার কয়েন পাঠিয়েছেন।",
"rewardCoins": "পুরস্কার কয়েন:{1} কয়েন",
"signInRewardReceived": "সাইন ইন সফল হয়েছে। পুরস্কার: {1}",
"registerRewardReceived": "রেজিস্ট্রেশন পুরস্কার পাওয়া গেছে: {1}",
"lastWeekProgress": "গত সপ্তাহের অগ্রগতি",
"redEnvelopeTips2": "*লাল খাম সময়সীমার মধ্যে দাবি না করলে, বাকি কয়েন প্রেরক ব্যবহারকারীকে ফেরত দেওয়া হবে।",
"goToRecharge": "রিচার্জ করতে যান",

View File

@ -134,6 +134,7 @@
"ownerSendTheRedEnvelope": "The owner sent reward coins.",
"rewardCoins": "Reward coins:{1} coins",
"signInRewardReceived": "Signed in successfully. Reward: {1}",
"registerRewardReceived": "Registration reward received: {1}",
"lastWeekProgress": "Last week's progress",
"redEnvelopeTips2": "*If the red envelope is not claimed within the time limit, the remaining coins will be returned to the user who sent the red envelope.",
"goToRecharge": "Go to recharge",

View File

@ -30,7 +30,7 @@
"background": "Arka Plan",
"roomPassword": "Oda Şifresi",
"noHistoricalRecordsAvailable": "Geçmiş kayıt bulunamadı.",
"inviteNewUsersToEarnCoins": "Yeni kullanıcıları davet ederek coin kazanın",
"inviteNewUsersToEarnCoins": "Yeni kullanıcıları davet ederek coin kazanın",
"crateMyRoom": "KENDİ ODANIZI OLUŞTURUN.",
"event": "Etkinlik",
"clearCacheSuccessfully": "Önbellek başarıyla temizlendi",
@ -123,6 +123,7 @@
"ownerSendTheRedEnvelope": "Sahip ödül jettonlarını gönderdi.",
"rewardCoins": "Ödül jettonları:{1} jetton",
"signInRewardReceived": "Giriş başarılı. Ödül: {1}",
"registerRewardReceived": "Kayıt ödülü alındı: {1}",
"lastWeekProgress": "Geçen Haftanın İlerlemesi",
"redEnvelopeTips2": "*Kırmızı zarf zaman sınırı içinde talep edilmezse, kalan jettonlar gönderen kullanıcıya iade edilecektir.",
"goToRecharge": "Yüklemeye Git",

View File

@ -502,7 +502,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5;
CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = S9X2AJ2US9;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -511,7 +511,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.1;
MARKETING_VERSION = 1.2.2;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -693,7 +693,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5;
CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -702,7 +702,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.1;
MARKETING_VERSION = 1.2.2;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -722,7 +722,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5;
CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -731,7 +731,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.1;
MARKETING_VERSION = 1.2.2;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -239,20 +239,20 @@ class SCAppLocalizations {
String get operationFail => translate('operationFail');
String get enterRoomFailedRetry {
final value = translate('enterRoomFailedRetry');
return value == 'enterRoomFailedRetry'
? 'Failed to enter the room. Please try again.'
: value;
}
String get voiceConnectionFailedRetry {
final value = translate('voiceConnectionFailedRetry');
return value == 'voiceConnectionFailedRetry'
? 'Voice connection failed. Please try again.'
: value;
}
String get enterRoomFailedRetry {
final value = translate('enterRoomFailedRetry');
return value == 'enterRoomFailedRetry'
? 'Failed to enter the room. Please try again.'
: value;
}
String get voiceConnectionFailedRetry {
final value = translate('voiceConnectionFailedRetry');
return value == 'voiceConnectionFailedRetry'
? 'Voice connection failed. Please try again.'
: value;
}
String get forMoreRewardsPleaseCheckTheTaskCenter =>
translate('forMoreRewardsPleaseCheckTheTaskCenter');
@ -1612,6 +1612,9 @@ class SCAppLocalizations {
String signInRewardReceived(String name) =>
translate('signInRewardReceived').replaceAll('{1}', name);
String registerRewardReceived(String name) =>
translate('registerRewardReceived').replaceAll('{1}', name);
String deleteAccount2(String name) =>
translate('deleteAccount2').replaceAll('{1}', name);

View File

@ -23,6 +23,7 @@ import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/tools/sc_install_referrer_utils.dart';
import '../../../shared/business_logic/usecases/sc_custom_filtering_textinput_formatter.dart';
import '../../country/country_route.dart';
@ -53,6 +54,7 @@ class _SCEditProfilePageState extends State<SCEditProfilePage> {
);
Provider.of<SCAppGeneralManager>(context, listen: false).selectCountryInfo =
null;
_fillInvitationCodeFromInstallReferrer();
}
@override
@ -529,6 +531,22 @@ class _SCEditProfilePageState extends State<SCEditProfilePage> {
return eighteenYearsAgo;
}
Future<void> _fillInvitationCodeFromInstallReferrer() async {
final inviteCode = await SCInstallReferrerUtils.getInviteCode();
if (!mounted ||
inviteCode.isEmpty ||
invitationCodeController.text.trim().isNotEmpty) {
return;
}
final normalizedInviteCode =
inviteCode.length > 10 ? inviteCode.substring(0, 10) : inviteCode;
invitationCodeController.value = TextEditingValue(
text: normalizedInviteCode,
selection: TextSelection.collapsed(offset: normalizedInviteCode.length),
);
}
String? _preferNonEmpty(String? primary, String? fallback) {
if (primary != null && primary.isNotEmpty) {
return primary;

View File

@ -584,6 +584,13 @@ class _MessageItem extends StatelessWidget {
) {
_showMsgItemMenu(ct, content);
}, context);
} else if (_isRegisterRewardGrantedType(type)) {
return SCSystemMessageUtils.buildRegisterRewardGrantedMessage(data, (
ct,
content,
) {
_showMsgItemMenu(ct, content);
}, context);
} else if (type == SCSysytemMessageType.USER_SPECIAL_AUDIT_RESULTS.name) {
if (data["content"] != null) {
return Builder(
@ -883,6 +890,11 @@ class _MessageItem extends StatelessWidget {
);
}
bool _isRegisterRewardGrantedType(String type) {
return type.trim().toUpperCase().replaceAll(' ', '_') ==
SCSysytemMessageType.REGISTER_REWARD_GRANTED.name;
}
void _showMsgItemMenu(BuildContext ct, String content) {
SmartDialog.showAttach(
tag: "showMsgItemMenu",

View File

@ -7,7 +7,6 @@ import 'package:yumi/app_localizations.dart';
import 'package:yumi/modules/index/main_route.dart';
import 'package:yumi/modules/store/store_route.dart';
import 'package:yumi/modules/user/settings/settings_route.dart';
import 'package:yumi/modules/user/vip/vip_route.dart';
import 'package:yumi/modules/wallet/wallet_route.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart';
@ -16,6 +15,7 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository
import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
import 'package:yumi/ui_kit/widgets/id/sc_special_id_badge.dart';
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
class MePage2 extends StatefulWidget {
const MePage2({super.key});
@ -25,6 +25,10 @@ class MePage2 extends StatefulWidget {
}
class _MePage2State extends State<MePage2> {
static const String _inviteActivityUrl =
'https://h5.haiyihy.com/app-invite/index.html';
static const double _inviteActivityHorizontalScale = 750 / 662;
Map<String, SCUserCounterRes> _counterMap = {};
@override
@ -62,6 +66,28 @@ class _MePage2State extends State<MePage2> {
} catch (_) {}
}
void _openInviteActivity() {
final inviteActivityUrl = _appendToken(_inviteActivityUrl);
if (inviteActivityUrl.isEmpty) return;
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(inviteActivityUrl)}&showTitle=false",
);
}
String _appendToken(String url) {
final token = AccountStorage().getToken();
if (token.isEmpty) return url;
final uri = Uri.tryParse(url);
if (uri == null) return url;
final queryParameters = Map<String, String>.from(uri.queryParameters);
queryParameters['token'] = token;
return uri.replace(queryParameters: queryParameters).toString();
}
@override
Widget build(BuildContext context) {
return SafeArea(
@ -80,8 +106,8 @@ class _MePage2State extends State<MePage2> {
SizedBox(height: 12.w),
_buildEntryRow(),
SizedBox(height: 12.w),
_buildWalletVipRow(),
SizedBox(height: 12.w),
_buildWalletCard(),
_buildInviteActivityBanner(),
_buildMenuCard2(),
SizedBox(height: 12.w),
_buildMenuCard([
@ -228,21 +254,12 @@ class _MePage2State extends State<MePage2> {
);
}
Widget _buildWalletVipRow() {
return Row(
children: [
Expanded(child: _buildVipEntryCard()),
SizedBox(width: 10.w),
Expanded(child: _buildWalletCard()),
],
);
}
Widget _buildWalletCard() {
return SCDebounceWidget(
onTap: () => SCNavigatorUtils.push(context, WalletRoute.recharge),
child: Container(
height: 68.w,
width: double.infinity,
height: 90.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.w),
image: const DecorationImage(
@ -250,53 +267,44 @@ class _MePage2State extends State<MePage2> {
fit: BoxFit.fill,
),
),
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
SCAppLocalizations.of(context)!.wallet,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18.sp,
fontSize: 24.sp,
fontWeight: FontWeight.w800,
fontStyle: FontStyle.italic,
color: Colors.white,
height: 1,
),
),
SizedBox(height: 2.w),
SizedBox(height: 4.w),
Row(
children: [
Image.asset(
'sc_images/general/sc_icon_wallet_coin.png',
width: 18.w,
height: 18.w,
'sc_images/general/sc_icon_jb.png',
width: 28.w,
height: 28.w,
),
SizedBox(width: 4.w),
Expanded(
child: Consumer<SocialChatUserProfileManager>(
builder: (context, ref, child) {
return Text(
':${_balanceText(ref.myBalance)}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 18.sp,
fontWeight: FontWeight.w700,
fontStyle: FontStyle.italic,
height: 1,
),
);
},
),
SizedBox(width: 6.w),
Consumer<SocialChatUserProfileManager>(
builder: (context, ref, child) {
return Text(
':${_balanceText(ref.myBalance)}',
style: TextStyle(
color: Colors.white,
fontSize: 24.sp,
fontWeight: FontWeight.w700,
fontStyle: FontStyle.italic,
),
);
},
),
],
),
@ -305,8 +313,8 @@ class _MePage2State extends State<MePage2> {
),
Image.asset(
'sc_images/index/sc_icon_wallet_icon.png',
width: 40.w,
height: 40.w,
width: 68.w,
height: 68.w,
),
],
),
@ -314,101 +322,64 @@ class _MePage2State extends State<MePage2> {
);
}
Widget _buildVipEntryCard() {
Widget _buildInviteActivityBanner() {
return SCDebounceWidget(
onTap: () => SCNavigatorUtils.push(context, VipRoute.detail),
child: Container(
height: 68.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.w),
border: Border.all(color: const Color(0xFFFFD37A), width: 1.w),
image: const DecorationImage(
image: AssetImage('sc_images/vip/sc_vip_entry_bg.png'),
fit: BoxFit.fill,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.18),
blurRadius: 10.w,
offset: Offset(0, 4.w),
),
],
),
clipBehavior: Clip.antiAlias,
child: Stack(
children: [
PositionedDirectional(
top: 12.w,
start: 14.w,
child: Text(
'VIP',
style: TextStyle(
color: Colors.white,
fontFamily: 'Source Han Sans SC',
fontSize: 16.sp,
fontWeight: FontWeight.w700,
height: 1,
onTap: _openInviteActivity,
child: LayoutBuilder(
builder: (context, constraints) {
final width =
constraints.maxWidth.isFinite
? constraints.maxWidth
: ScreenUtil().screenWidth - 20.w;
final height = width / 5;
return SizedBox(
width: width,
height: height,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Positioned.fill(
child: Transform(
alignment: Alignment.center,
transform: Matrix4.diagonal3Values(
_inviteActivityHorizontalScale,
1.0,
1.0,
),
child: SCSvgaAssetWidget(
assetPath: _inviteActivitySvgaAsset,
width: width,
height: height,
active: true,
loop: true,
fit: BoxFit.fill,
allowDrawingOverflow: true,
),
),
),
),
],
),
PositionedDirectional(
start: 13.w,
bottom: 10.w,
child: _buildVipLevelMark(5),
),
PositionedDirectional(
end: 2.w,
top: 8.w,
child: Image.asset(
'sc_images/vip/sc_vip_badge_5.png',
width: 52.w,
height: 52.w,
fit: BoxFit.contain,
),
),
],
),
);
},
),
);
}
Widget _buildVipLevelMark(int level) {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(
'sc_images/vip/sc_vip_letter_v.png',
width: 18.1.w,
height: 15.3.w,
fit: BoxFit.contain,
),
Image.asset(
'sc_images/vip/sc_vip_letter_i.png',
width: 8.3.w,
height: 15.3.w,
fit: BoxFit.contain,
),
Image.asset(
'sc_images/vip/sc_vip_letter_p.png',
width: 18.1.w,
height: 15.3.w,
fit: BoxFit.contain,
),
SizedBox(width: 3.w),
_buildVipLevelNumber(level),
],
);
}
Widget _buildVipLevelNumber(int level) {
return Image.asset(
'sc_images/vip/sc_vip_number_${level}_filled.png',
height: 15.247.w,
fit: BoxFit.fill,
color: const Color(0xFFFFD155),
colorBlendMode: BlendMode.srcIn,
);
String get _inviteActivitySvgaAsset {
switch (SCGlobalConfig.lang) {
case 'ar':
return 'sc_images/index/sc_invite_activity_rtl.svga';
case 'bn':
case 'fa':
return 'sc_images/index/sc_invite_activity_fa.svga';
case 'tr':
case 'pt':
return 'sc_images/index/sc_invite_activity_pt.svga';
default:
return 'sc_images/index/sc_invite_activity.svga';
}
}
Widget _buildMenuCard2() {

View File

@ -15,4 +15,5 @@ enum SCSysytemMessageType{
CP_LOVE_LETTER,
USER_COINS_RECEIVED,
COIN_SELLER_COINS_RECEIVED,
}
REGISTER_REWARD_GRANTED,
}

View File

@ -0,0 +1,131 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
class SCInstallReferrerUtils {
static const MethodChannel _channel = MethodChannel(
'com.org.yumiparty/install_referrer',
);
static const String _cachedInviteCodeKey = 'install_referrer_invite_code';
static const String _cachedRawReferrerKey = 'install_referrer_raw';
static Future<String> getInviteCode() async {
final cachedInviteCode = DataPersistence.getString(_cachedInviteCodeKey);
if (cachedInviteCode.isNotEmpty) {
return cachedInviteCode;
}
final cachedRawReferrer = DataPersistence.getString(_cachedRawReferrerKey);
if (cachedRawReferrer.isNotEmpty) {
return extractInviteCode(cachedRawReferrer);
}
if (!Platform.isAndroid) {
return '';
}
try {
final referrer = await _channel.invokeMethod<String>(
'getInstallReferrer',
);
if (referrer == null || referrer.trim().isEmpty) {
return '';
}
await DataPersistence.setString(_cachedRawReferrerKey, referrer);
final inviteCode = extractInviteCode(referrer);
if (inviteCode.isNotEmpty) {
await DataPersistence.setString(_cachedInviteCodeKey, inviteCode);
}
return inviteCode;
} on PlatformException catch (e) {
debugPrint('get install referrer failed: ${e.code} ${e.message}');
return '';
} catch (e) {
debugPrint('get install referrer failed: $e');
return '';
}
}
static String extractInviteCode(String referrer) {
return _extractInviteCode(referrer, 0);
}
static String _extractInviteCode(String input, int depth) {
if (depth > 4) {
return '';
}
final value = input.trim();
if (value.isEmpty) {
return '';
}
final params = _queryParameters(value);
final inviteCode = _inviteCodeFromParams(params);
if (inviteCode.isNotEmpty) {
return inviteCode;
}
final nestedReferrer = _nestedReferrerFromParams(params);
if (nestedReferrer != null && nestedReferrer.isNotEmpty) {
final nestedInviteCode = _extractInviteCode(nestedReferrer, depth + 1);
if (nestedInviteCode.isNotEmpty) {
return nestedInviteCode;
}
}
final decodedValue = _decodeComponent(value);
if (decodedValue != value) {
return _extractInviteCode(decodedValue, depth + 1);
}
return '';
}
static Map<String, String> _queryParameters(String value) {
final uri = Uri.tryParse(value);
if (uri != null && uri.hasQuery) {
return uri.queryParameters;
}
if (!value.contains('=')) {
return const {};
}
try {
return Uri.splitQueryString(value);
} catch (_) {
return const {};
}
}
static String _inviteCodeFromParams(Map<String, String> params) {
for (final entry in params.entries) {
final key = entry.key.toLowerCase();
if (key == 'invite_code' || key == 'invitecode') {
return entry.value.trim();
}
}
return '';
}
static String? _nestedReferrerFromParams(Map<String, String> params) {
for (final entry in params.entries) {
if (entry.key.toLowerCase() == 'referrer') {
return entry.value;
}
}
return null;
}
static String _decodeComponent(String value) {
try {
return Uri.decodeComponent(value);
} catch (_) {
return value;
}
}
}

View File

@ -1425,4 +1425,243 @@ class SCSystemMessageUtils {
}
return Container();
}
static Widget buildRegisterRewardGrantedMessage(
data,
Function(BuildContext ct, String content) showMsgItemMenu,
BuildContext context,
) {
final content = _registerRewardGrantedContent(data, context);
return Builder(
builder: (ct) {
return GestureDetector(
onLongPress: () {
showMsgItemMenu(ct, content);
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [BoxShadow(blurRadius: 2.w, color: Colors.black26)],
borderRadius: BorderRadius.only(
topLeft: Radius.circular(0),
topRight: Radius.circular(8.w),
bottomLeft: Radius.circular(8.w),
bottomRight: Radius.circular(8),
),
),
padding: EdgeInsets.all(8.0.w),
child: ExtendedText(
content,
style: TextStyle(fontSize: 14.sp, color: Colors.black54),
),
),
);
},
);
}
static String _registerRewardGrantedContent(
dynamic data,
BuildContext context,
) {
final l10n = SCAppLocalizations.of(context)!;
final rewards =
_extractRegisterRewardItems(data)
.map((item) => _registerRewardItemLabel(l10n, item))
.where((item) => item.isNotEmpty)
.toList();
if (rewards.isNotEmpty) {
return l10n.registerRewardReceived(rewards.join(', '));
}
final plainContent = _plainRegisterRewardContent(data);
if (plainContent.isNotEmpty) {
return l10n.registerRewardReceived(plainContent);
}
return l10n.receiveSucc;
}
static List<dynamic> _extractRegisterRewardItems(dynamic value) {
final decoded = _decodeJsonValue(value);
if (decoded is List) {
return decoded;
}
if (decoded is Map) {
final nestedItems = _extractNestedRegisterRewardItems(decoded);
if (nestedItems.isNotEmpty) {
return nestedItems;
}
if (_looksLikeRewardItem(decoded)) {
return [decoded];
}
}
return const <dynamic>[];
}
static String _registerRewardItemLabel(
SCAppLocalizations l10n,
dynamic item,
) {
final decoded = _decodeJsonValue(item);
if (decoded is! Map) {
return _readPlainText(decoded);
}
if (!_looksLikeRewardItem(decoded)) {
final nestedItems = _extractNestedRegisterRewardItems(decoded);
if (nestedItems.isNotEmpty) {
return nestedItems
.map((item) => _registerRewardItemLabel(l10n, item))
.where((item) => item.isNotEmpty)
.join(', ');
}
}
final type = _readPlainText(decoded['type']).trim().toUpperCase();
final detailType =
_readPlainText(decoded['detailType']).trim().toUpperCase();
final quantity = _rewardQuantity(decoded);
if (type == 'GOLD' ||
type == 'COIN' ||
type == 'COINS' ||
detailType == 'GOLD' ||
detailType == 'COIN' ||
detailType == 'COINS') {
return l10n.coins2(quantity.isEmpty ? '0' : quantity);
}
final name = _pickFirstNonEmpty([
_readPlainText(decoded['name']),
_readPlainText(decoded['badgeName']),
_readPlainText(decoded['remark']),
_readPlainText(decoded['title']),
_readPlainText(decoded['content']),
detailType,
type,
]);
if (name.isEmpty) {
return '';
}
final count = _asInt(decoded['quantity']);
if (count > 1) {
return '$name x$count';
}
return name;
}
static List<dynamic> _extractNestedRegisterRewardItems(
Map<dynamic, dynamic> value,
) {
const nestedKeys = [
'rewardItems',
'rewards',
'items',
'activityRewardProps',
'rewardConfig',
'propsGroup',
'propsGroups',
'data',
'content',
];
for (final key in nestedKeys) {
final items = _extractRegisterRewardItems(value[key]);
if (items.isNotEmpty) {
return items;
}
}
return const <dynamic>[];
}
static String _rewardQuantity(Map<dynamic, dynamic> item) {
final quantity = _asInt(item['quantity']);
if (quantity > 0) {
return quantity.toString();
}
return _pickFirstNonEmpty([
_readPlainText(item['amount']),
_readPlainText(item['content']),
]);
}
static String _plainRegisterRewardContent(dynamic data) {
final decoded = _decodeJsonValue(data);
if (decoded is Map) {
return _pickFirstNonEmpty([
_readPlainText(decoded['message']),
_readPlainText(decoded['title']),
_readPlainText(decoded['content']),
]);
}
return _readPlainText(decoded);
}
static dynamic _decodeJsonValue(dynamic value) {
if (value is! String) {
return value;
}
final text = value.trim();
if (!_looksLikeStructuredText(text)) {
return value;
}
try {
return jsonDecode(text);
} catch (_) {
return value;
}
}
static bool _looksLikeRewardItem(Map<dynamic, dynamic> value) {
const rewardKeys = [
'type',
'detailType',
'name',
'quantity',
'amount',
'remark',
'badgeName',
];
return rewardKeys.any(value.containsKey);
}
static String _pickFirstNonEmpty(List<String> values) {
for (final value in values) {
final text = value.trim();
if (text.isNotEmpty) {
return text;
}
}
return '';
}
static String _readPlainText(dynamic value) {
final text = _asString(value).trim();
if (text.isEmpty || _looksLikeStructuredText(text)) {
return '';
}
return text;
}
static bool _looksLikeStructuredText(String value) {
final text = value.trimLeft();
return text.startsWith('{') || text.startsWith('[');
}
static String _asString(dynamic value) {
if (value == null) {
return '';
}
return value.toString();
}
static int _asInt(dynamic value) {
if (value is int) {
return value;
}
if (value is num) {
return value.toInt();
}
return int.tryParse(_asString(value)) ?? 0;
}
}

View File

@ -37,6 +37,7 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
static const int _menuSound = 4;
static const int _menuReport = 5;
static const int _menuBackground = 6;
static bool get _showBackgroundFeature => false;
List<RoomMenu> items1 = [];
List<RoomMenu> items2 = [];
@ -119,7 +120,8 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
: "sc_icon_mic_open.png",
),
);
if (Provider.of<RtcProvider>(context, listen: false).isFz()) {
if (_showBackgroundFeature &&
Provider.of<RtcProvider>(context, listen: false).isFz()) {
items2.add(
RoomMenu(
_menuBackground,

View File

@ -210,6 +210,10 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
clearsAfterStop: widget.clearsAfterStop,
filterQuality: widget.filterQuality,
allowDrawingOverflow: widget.allowDrawingOverflow,
preferredSize:
widget.width != null && widget.height != null
? Size(widget.width!, widget.height!)
: null,
),
),
);

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.2.1+5
version: 1.2.2+6
environment:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,44 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yumi/shared/tools/sc_install_referrer_utils.dart';
void main() {
group('SCInstallReferrerUtils.extractInviteCode', () {
test('extracts invite code from decoded referrer', () {
expect(
SCInstallReferrerUtils.extractInviteCode(
'invite_code=LZSWI6GO&source=share',
),
'LZSWI6GO',
);
});
test('extracts invite code from encoded referrer', () {
expect(
SCInstallReferrerUtils.extractInviteCode(
'invite_code%3DLZSWI6GO%26source%3Dshare',
),
'LZSWI6GO',
);
});
test('extracts invite code from play store url', () {
expect(
SCInstallReferrerUtils.extractInviteCode(
'https://play.google.com/store/apps/details'
'?id=com.org.yumiparty'
'&referrer=invite_code%3DLZSWI6GO%26source%3Dshare',
),
'LZSWI6GO',
);
});
test('extracts invite code from camel case key', () {
expect(
SCInstallReferrerUtils.extractInviteCode(
'inviteCode=LZSWI6GO&source=share',
),
'LZSWI6GO',
);
});
});
}