Skip to content

Commit

Permalink
Sparrow SDK (#1282)
Browse files Browse the repository at this point in the history
* added plugin for sparrow.

* Created survey service.

* Added spot check survey for testing.

* Add spot check support in homscreen.

* Write survey config on file.

* Use vpn status as enum to avoid mistakes.

* Update home.dart

* Add country segmentation for survey.

* Added localisation support in survey.

* Fix Di issue.

* Complete country segment.

* Update app.env step in CI.

* Fix merge conflicts.

* Added support for macos survey.

* Add default testing survey.

* Dynamic survey handling.

* Read config changes.

* Hide survey in windows and Linux.

* add default contry.
  • Loading branch information
jigar-f authored Jan 28, 2025
1 parent bacfed9 commit 6b2b9a6
Show file tree
Hide file tree
Showing 17 changed files with 446 additions and 192 deletions.
16 changes: 5 additions & 11 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -237,18 +237,12 @@ jobs:
fileDir: './android/app'
encodedString: ${{ secrets.KEYSTORE }}

- name: Generate app.env
env:
ANDROID_INTERSTITIAL_AD_ID: ${{ secrets.INTERSTITIAL_AD_UNIT_ID }}
IOS_INTERSTITIAL_AD_ID: ${{ secrets.INTERSTITIAL_AD_UNIT_ID_IOS }}
TAPSELL_VIDEO_INTERSTITIAL_ZONE_ID: ${{ secrets.TAPSELL_VIDEO_INTERSTITIAL_ZONE_ID }}
TAPSELL_INTERSTITIAL_ZONE_ID: ${{ secrets.TAPSELL_INTERSTITIAL_ZONE_ID }}

- name: Decode APP_ENV and write to app.env
run: |
touch app.env
echo "Android_interstitialAd=$ANDROID_INTERSTITIAL_AD_ID" > app.env
echo "IOS_interstitialAd=$IOS_INTERSTITIAL_AD_ID" >> app.env
echo "VideoInterstitialZoneId=$TAPSELL_VIDEO_INTERSTITIAL_ZONE_ID" >> app.env
echo "InterstitialZoneId=$TAPSELL_INTERSTITIAL_ZONE_ID" >> app.env
echo "${{ secrets.APP_ENV }}" | base64 --decode > app.env
echo "File 'app.env' created."
ls -lh app.env
- name: Extract app version from pubspec.yaml
id: extract_version
Expand Down
2 changes: 2 additions & 0 deletions desktop/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type ConfigOptions struct {
ExpirationDate string `json:"expirationDate"`
Chat ChatOptions `json:"chat"`
ProxyAll bool `json:"proxyAll"`
Country string `json:"country"`
}

func (s *configService) StartService(channel ws.UIChannel) (err error) {
Expand Down Expand Up @@ -105,6 +106,7 @@ func (app *App) sendConfigOptions() {
ExpirationDate: app.settings.GetExpirationDate(),
Devices: app.devices(),
ProxyAll: app.settings.GetProxyAll(),
Country: app.settings.GetCountry(),
Chat: ChatOptions{
AcceptedTermsVersion: 0,
OnBoardingStatus: false,
Expand Down
7 changes: 7 additions & 0 deletions lib/core/app/app_enums.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,10 @@ extension AuthFlowExtension on AuthFlow {
bool get isUpdateAccount => this == AuthFlow.updateAccount;
bool get isRestoreAccount => this == AuthFlow.restoreAccount;
}

enum VpnStatus{
connected,
disconnected,
connecting,
disconnecting
}
13 changes: 11 additions & 2 deletions lib/core/app/app_secret.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
class AppSecret {
static String androidAdsAppId = dotenv.get('Android_interstitialAd');
static String iOSAdsAppId = dotenv.get('IOS_interstitialAd');
static String testingSpotCheckTargetToken = dotenv.get('TESTING_SPOTCHECK_TARGET_TOKEN');
static String iranSpotCheckTargetToken = dotenv.get('IRAN_SPOTCHECK_TARGET_TOKEN');
static String russiaSpotCheckTargetToken = dotenv.get('RUSSIA_SPOTCHECK_TARGET_TOKEN');
static String ukraineSpotCheckTargetToken = dotenv.get('UKRAINE_SPOTCHECK_TARGET_TOKEN');
static String belarusSpotCheckTargetToken = dotenv.get('BELARUS_SPOTCHECK_TARGET_TOKEN');
static String chinaSpotCheckTargetToken = dotenv.get('CHINA_SPOTCHECK_TARGET_TOKEN');
static String UAEspotCheckTargetToken = dotenv.get('UAE_SPOTCHECK_TARGET_TOKEN');
static String myanmarSpotCheckTargetToken = dotenv.get('MYANMAR_SPOTCHECK_TARGET_TOKEN');



static String tos = 'https://s3.amazonaws.com/lantern/Lantern-TOS.pdf';
static String privacyPolicy = 'https://s3.amazonaws.com/lantern/LanternPrivacyPolicy.pdf';
static String privacyPolicyV2 = 'https://lantern.io/privacy';
static String tosV2 = 'https://lantern.io/terms';
static String videoInterstitialZoneId = dotenv.get('VideoInterstitialZoneId');
static String interstitialZoneId =dotenv.get('InterstitialZoneId');


static String dnsConfig() {
Expand Down
11 changes: 9 additions & 2 deletions lib/core/service/injection_container.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import 'package:get_it/get_it.dart';
import 'package:lantern/core/service/app_purchase.dart';
import 'package:lantern/core/service/survey_service.dart';
import 'package:lantern/core/utils/common.dart';

final GetIt sl = GetIt.instance;

void initServices() {
//Inject
sl.registerLazySingleton(() => AppPurchase());
sl<AppPurchase>().init();
if (isMobile()) {
sl.registerLazySingleton(() => AppPurchase());
sl<AppPurchase>().init();
}


sl.registerLazySingleton(() => SurveyService());
}
177 changes: 177 additions & 0 deletions lib/core/service/survey_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import 'package:path_provider/path_provider.dart';
import 'package:surveysparrow_flutter_sdk/surveysparrow.dart';

import '../utils/common.dart';

enum SurveyScreens { homeScreen }

enum SurveyCountry {
russia('ru'),
belarus('by'),
ukraine('ua'),
china('cn'),
iran('ir'),
uae('uae'),
myanmar('mm'),
testing('testing');

const SurveyCountry(this.countryCode);

final String countryCode;
}

//This class use spot check service for survey
class SurveyService {
// Need to have spot check for each region
// Russia, Belarus, Ukraine, China, Iran, UAE, Myanmar

SpotCheck? spotCheck;
final int _VPNCONNECTED_COUNT = 10;

SurveyService() {
if (Platform.isWindows || Platform.isLinux) {
return;
}
_createConfigIfNeeded();
_countryListener();
}

void _countryListener() {
if (sessionModel.country.value!.isNotEmpty) {
createSpotCheckByCountry(sessionModel.country.value!.toLowerCase());
return;
}
sessionModel.country.addListener(() {
final country = sessionModel.country.value;
if (country != null && country.isNotEmpty) {
appLogger.d('Country found $country');
createSpotCheckByCountry(country.toLowerCase());
sessionModel.country
.removeListener(() {}); // Remove listener after getting value
}
});
}

//Create method to create spot check by country
//argument by string and use enum for country
//make sure when create country should not be null or empty
SpotCheck createSpotCheckByCountry(String country) {
appLogger.d('Create spot check for country $country');
if (spotCheck != null) {
return spotCheck!;
}
final surveyCountry = SurveyCountry.values.firstWhere(
(e) => e.countryCode == country,
orElse: () => SurveyCountry.testing,
);
String targetToken;
switch (surveyCountry) {
case SurveyCountry.russia:
targetToken = AppSecret.russiaSpotCheckTargetToken;
break;
case SurveyCountry.belarus:
targetToken = AppSecret.belarusSpotCheckTargetToken;
break;
case SurveyCountry.ukraine:
targetToken = AppSecret.ukraineSpotCheckTargetToken;
break;
case SurveyCountry.china:
targetToken = AppSecret.chinaSpotCheckTargetToken;
break;
case SurveyCountry.iran:
targetToken = AppSecret.iranSpotCheckTargetToken;
break;
case SurveyCountry.uae:
targetToken = AppSecret.UAEspotCheckTargetToken;
break;
case SurveyCountry.myanmar:
targetToken = AppSecret.myanmarSpotCheckTargetToken;
break;
case SurveyCountry.testing:
default:
targetToken = AppSecret.testingSpotCheckTargetToken;
appLogger.d('${country.toUpperCase()} not found, using testing token');
break;
}
spotCheck = SpotCheck(
domainName: "lantern.surveysparrow.com",
targetToken: targetToken,
userDetails: {});
return spotCheck!;
}

void trackScreen(SurveyScreens screen) {
appLogger.d('Track screen $screen');
spotCheck?.trackScreen(screen.name);
}

Widget surveyWidget() {
if (Platform.isWindows || Platform.isLinux) {
return const SizedBox();
}
return spotCheck!;
}

Future<String> get _surveyConfigPath async {
final cacheDir = await getApplicationCacheDirectory();
final filePath = '${cacheDir.path}/survey_config.json';
return filePath;
}

Future<void> _createConfigIfNeeded() async {
final filePath = await _surveyConfigPath;
final file = File(filePath);
try {
if (!await file.exists()) {
await file.create(recursive: true);
const surveyConfig = {"vpnConnectCount": 0};
final jsonString = jsonEncode(surveyConfig);
await file.writeAsString(jsonString);
appLogger.d("Write init config done $filePath");
}
} catch (e) {
appLogger.e("Error while creating config");
}
}

Future<void> incrementVpnConnectCount() async {
try {
final content = await readSurveyConfig();
final surveyConfig = jsonDecode(content.$2) as Map<String, dynamic>;
// Increment the vpnConnectCount field
surveyConfig['vpnConnectCount'] =
(surveyConfig['vpnConnectCount'] ?? 0) + 1;
final updatedJsonString = jsonEncode(surveyConfig);
await content.$1.writeAsString(updatedJsonString);
appLogger.i('vpnConnectCount updated successfully.');
} catch (e) {
appLogger.i('Failed to update vpnConnectCount: $e');
}
}

Future<bool> surveyAvailable() async {
try {
final content = await readSurveyConfig();
final Map<String, dynamic> surveyConfig = jsonDecode(content.$2);
final vpnConnectCount = surveyConfig['vpnConnectCount'] ?? 0;
appLogger.i('Survey config. ${surveyConfig.toString()}');
if (vpnConnectCount >= _VPNCONNECTED_COUNT) {
appLogger.d('Survey is available.');
return true;
}
appLogger.i('Survey is not available.');
return false;
} catch (e) {
appLogger.e('Failed to check survey availability: $e');
return false;
}
}

//this read survey config method will return file and string
Future<(File, String)> readSurveyConfig() async {
final filePath = await _surveyConfigPath;
final file = File(filePath);
final content = await file.readAsString();
return (file, content);
}
}
1 change: 1 addition & 0 deletions lib/core/service/websocket_subscriber.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class WebsocketSubscriber {

sessionModel.isAuthEnabled.value = config.authEnabled;
sessionModel.configNotifier.value = config;
sessionModel.country.value = config.country;
_updatePlans(config.plans);
_updatePaymentMethods(config.paymentMethods);
break;
Expand Down
5 changes: 2 additions & 3 deletions lib/core/utils/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,9 @@ final appLogger = Logger(
errorMethodCount: 5,
colors: true,
printEmojis: true,
printTime: true,

),
filter: ProductionFilter(),
output: ConsoleOutput(),
level: Level.debug,
);

bool isMobile() {
Expand Down
42 changes: 22 additions & 20 deletions lib/core/utils/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class ConfigOptions {

final Map<String, PaymentMethod>? paymentMethods;
final _ChatOptions chat;
final String country;

ConfigOptions({
this.developmentMode = false,
Expand All @@ -62,10 +63,11 @@ class ConfigOptions {
this.httpProxyAddr = '',
this.socksProxyAddr = '',
this.deviceId = '',
this.plans = null,
this.paymentMethods = null,
this.plans,
this.paymentMethods,
required this.devices,
this.chat = const _ChatOptions(),
required this.country,
});

bool get startupReady =>
Expand All @@ -83,24 +85,24 @@ class ConfigOptions {
final paymentMethods = paymentMethodsFromJson(parsedJson['paymentMethods']);

return ConfigOptions(
developmentMode: parsedJson['developmentMode'],
authEnabled: parsedJson['authEnabled'],
chatEnabled: parsedJson['chatEnabled'],
httpProxyAddr: parsedJson['httpProxyAddr'],
socksProxyAddr: parsedJson['socksProxyAddr'],
splitTunneling: parsedJson['splitTunneling'],
hasSucceedingProxy: parsedJson['hasSucceedingProxy'],
fetchedGlobalConfig: parsedJson['fetchedGlobalConfig'],
fetchedProxiesConfig: parsedJson['fetchedProxiesConfig'],
plans: plans,
chat: _ChatOptions.fromJson(parsedJson['chat']),
paymentMethods: paymentMethods,
devices: _parseDevices(parsedJson),
replicaAddr: parsedJson['replicaAddr'].toString(),
deviceId: parsedJson['deviceId'].toString(),
expirationDate: parsedJson['expirationDate'].toString(),
sdkVersion: parsedJson['sdkVersion'].toString(),
);
developmentMode: parsedJson['developmentMode'],
authEnabled: parsedJson['authEnabled'],
chatEnabled: parsedJson['chatEnabled'],
httpProxyAddr: parsedJson['httpProxyAddr'],
socksProxyAddr: parsedJson['socksProxyAddr'],
splitTunneling: parsedJson['splitTunneling'],
hasSucceedingProxy: parsedJson['hasSucceedingProxy'],
fetchedGlobalConfig: parsedJson['fetchedGlobalConfig'],
fetchedProxiesConfig: parsedJson['fetchedProxiesConfig'],
plans: plans,
chat: _ChatOptions.fromJson(parsedJson['chat']),
paymentMethods: paymentMethods,
devices: _parseDevices(parsedJson),
replicaAddr: parsedJson['replicaAddr'].toString(),
deviceId: parsedJson['deviceId'].toString(),
expirationDate: parsedJson['expirationDate'].toString(),
sdkVersion: parsedJson['sdkVersion'].toString(),
country: parsedJson['country'] ?? "");
}

static Devices _parseDevices(Map json) {
Expand Down
2 changes: 1 addition & 1 deletion lib/features/account/report_issue.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:lantern/core/utils/common.dart';
class ReportIssue extends StatefulWidget {
final String? description;

const ReportIssue({Key? key, this.description}) : super(key: key);
const ReportIssue({super.key, this.description});

@override
State<ReportIssue> createState() => _ReportIssueState();
Expand Down
Loading

0 comments on commit 6b2b9a6

Please sign in to comment.