diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 536c901..dac7995 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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 { diff --git a/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt b/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt index d8110ee..ccda86e 100644 --- a/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt +++ b/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt @@ -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" } } diff --git a/assets/l10n/intl_ar.json b/assets/l10n/intl_ar.json index 3b23f69..418a000 100644 --- a/assets/l10n/intl_ar.json +++ b/assets/l10n/intl_ar.json @@ -33,7 +33,6 @@ "profilePhoto": "صورة الملف الشخصي", "noHistoricalRecordsAvailable": "لا توجد سجلات تاريخية متاحة.", "inviteNewUsersToEarnCoins": "ادعُ مستخدمين جدد لكسب العملات", - "inviteActivity": "نشاط الدعوة", "crateMyRoom": "أنشئ غرفتك الخاصة.", "casualInteraction": "التفاعل العفوي", "haveGamePlayingTips": "لديك لعبة جارية، يرجى الخروج من اللعبة الحالية أولاً، هل أنت متأكد من الخروج؟", diff --git a/assets/l10n/intl_bn.json b/assets/l10n/intl_bn.json index fe14204..5e07503 100644 --- a/assets/l10n/intl_bn.json +++ b/assets/l10n/intl_bn.json @@ -32,7 +32,6 @@ "aboutMe": "আমার সম্পর্কে", "noHistoricalRecordsAvailable": "কোনও ঐতিহাসিক রেকর্ড পাওয়া যায়নি।", "inviteNewUsersToEarnCoins": "নতুন ব্যবহারকারীকে আমন্ত্রণ করুন কয়েন উপার্জনের জন্য", - "inviteActivity": "আমন্ত্রণ কার্যক্রম", "crateMyRoom": "নিজের রুম তৈরি করুন।", "myRoom": "আমার রুম", "recent": "সাম্প্রতিক", diff --git a/assets/l10n/intl_en.json b/assets/l10n/intl_en.json index 426a68d..17d61e3 100644 --- a/assets/l10n/intl_en.json +++ b/assets/l10n/intl_en.json @@ -33,7 +33,6 @@ "myRoom": "My room", "noHistoricalRecordsAvailable": "No historical records available.", "inviteNewUsersToEarnCoins": "Invite new users to earn coins", - "inviteActivity": "Invite Activity", "crateMyRoom": "CREATE YOUR OWN ROOM.", "casualInteraction": "Casual Interaction", "haveGamePlayingTips": "You have a game in progress. Please exit the current game. Are you sure you want to exit?", diff --git a/assets/l10n/intl_tr.json b/assets/l10n/intl_tr.json index 3bc6365..cd27750 100644 --- a/assets/l10n/intl_tr.json +++ b/assets/l10n/intl_tr.json @@ -31,7 +31,6 @@ "roomPassword": "Oda Şifresi", "noHistoricalRecordsAvailable": "Geçmiş kayıt bulunamadı.", "inviteNewUsersToEarnCoins": "Yeni kullanıcıları davet ederek coin kazanın", - "inviteActivity": "Davet Etkinliği", "crateMyRoom": "KENDİ ODANIZI OLUŞTURUN.", "event": "Etkinlik", "clearCacheSuccessfully": "Önbellek başarıyla temizlendi", diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1df3b09..77837a9 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ""; diff --git a/lib/app_localizations.dart b/lib/app_localizations.dart index 9b2d4c3..43c1ce5 100644 --- a/lib/app_localizations.dart +++ b/lib/app_localizations.dart @@ -73,11 +73,6 @@ class SCAppLocalizations { String get inviteNewUsersToEarnCoins => translate('inviteNewUsersToEarnCoins'); - String get inviteActivity { - final value = translate('inviteActivity'); - return value == 'inviteActivity' ? 'Invite Activity' : value; - } - String get avoidBeingKicked => translate('avoidBeingKicked'); String get vipEntranceEffect => translate('vipEntranceEffect'); diff --git a/lib/modules/auth/edit/sc_edit_profile_page.dart b/lib/modules/auth/edit/sc_edit_profile_page.dart index 4444bc3..fe942c5 100644 --- a/lib/modules/auth/edit/sc_edit_profile_page.dart +++ b/lib/modules/auth/edit/sc_edit_profile_page.dart @@ -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 { ); Provider.of(context, listen: false).selectCountryInfo = null; + _fillInvitationCodeFromInstallReferrer(); } @override @@ -529,6 +531,22 @@ class _SCEditProfilePageState extends State { return eighteenYearsAgo; } + Future _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; diff --git a/lib/modules/user/me_page2.dart b/lib/modules/user/me_page2.dart index 6a2f780..a4f9414 100644 --- a/lib/modules/user/me_page2.dart +++ b/lib/modules/user/me_page2.dart @@ -15,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}); @@ -26,6 +27,7 @@ class MePage2 extends StatefulWidget { class _MePage2State extends State { static const String _inviteActivityUrl = 'https://h5.haiyihy.com/app-invite/index.html'; + static const double _inviteActivityHorizontalScale = 750 / 662; Map _counterMap = {}; @@ -105,7 +107,7 @@ class _MePage2State extends State { _buildEntryRow(), SizedBox(height: 12.w), _buildWalletCard(), - SizedBox(height: 12.w), + _buildInviteActivityBanner(), _buildMenuCard2(), SizedBox(height: 12.w), _buildMenuCard([ @@ -320,6 +322,66 @@ class _MePage2State extends State { ); } + Widget _buildInviteActivityBanner() { + return SCDebounceWidget( + 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, + ), + ), + ), + ], + ), + ); + }, + ), + ); + } + + 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() { final userProfile = context.watch(); final items = <_MenuRowData>[]; @@ -355,14 +417,6 @@ class _MePage2State extends State { ), ); } - items.add( - _MenuRowData( - title: SCAppLocalizations.of(context)!.inviteActivity, - assetIcon: 'sc_images/index/sc_icon_invite_activity.png', - iconSize: 20, - onTap: _openInviteActivity, - ), - ); } // 2. BD 相关(非管理员时) @@ -451,17 +505,11 @@ class _MePage2State extends State { height: 46.w, child: Row( children: [ - SizedBox( + Image.asset( + item.assetIcon!, width: 24.w, height: 24.w, - child: Center( - child: Image.asset( - item.assetIcon!, - width: item.iconSize.w, - height: item.iconSize.w, - color: Colors.white, - ), - ), + color: Colors.white, ), SizedBox(width: 12.w), @@ -611,11 +659,9 @@ class _MenuRowData { required this.title, required this.onTap, this.assetIcon, - this.iconSize = 24, }); final String title; final String? assetIcon; - final double iconSize; final VoidCallback onTap; } diff --git a/lib/shared/tools/sc_install_referrer_utils.dart b/lib/shared/tools/sc_install_referrer_utils.dart new file mode 100644 index 0000000..5684eaf --- /dev/null +++ b/lib/shared/tools/sc_install_referrer_utils.dart @@ -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 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( + '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 _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 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 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; + } + } +} diff --git a/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart b/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart index eb006e2..f832558 100644 --- a/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart +++ b/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart @@ -210,6 +210,10 @@ class _SCSvgaAssetWidgetState extends State clearsAfterStop: widget.clearsAfterStop, filterQuality: widget.filterQuality, allowDrawingOverflow: widget.allowDrawingOverflow, + preferredSize: + widget.width != null && widget.height != null + ? Size(widget.width!, widget.height!) + : null, ), ), ); diff --git a/pubspec.yaml b/pubspec.yaml index d413af6..5cbedaa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: diff --git a/sc_images/index/sc_icon_invite_activity.png b/sc_images/index/sc_icon_invite_activity.png deleted file mode 100644 index 072647b..0000000 Binary files a/sc_images/index/sc_icon_invite_activity.png and /dev/null differ diff --git a/sc_images/index/sc_invite_activity.svga b/sc_images/index/sc_invite_activity.svga new file mode 100644 index 0000000..cae8541 Binary files /dev/null and b/sc_images/index/sc_invite_activity.svga differ diff --git a/sc_images/index/sc_invite_activity_fa.svga b/sc_images/index/sc_invite_activity_fa.svga new file mode 100644 index 0000000..e204e75 Binary files /dev/null and b/sc_images/index/sc_invite_activity_fa.svga differ diff --git a/sc_images/index/sc_invite_activity_pt.svga b/sc_images/index/sc_invite_activity_pt.svga new file mode 100644 index 0000000..39c9187 Binary files /dev/null and b/sc_images/index/sc_invite_activity_pt.svga differ diff --git a/sc_images/index/sc_invite_activity_rtl.svga b/sc_images/index/sc_invite_activity_rtl.svga new file mode 100644 index 0000000..7d3a676 Binary files /dev/null and b/sc_images/index/sc_invite_activity_rtl.svga differ diff --git a/test/install_referrer_utils_test.dart b/test/install_referrer_utils_test.dart new file mode 100644 index 0000000..0f24172 --- /dev/null +++ b/test/install_referrer_utils_test.dart @@ -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', + ); + }); + }); +}