sms_insights_flutter

Flutter plugin that wraps the PayU SMS Insights Android SDKs. Reads transactional SMS for credit-assessment with a consent-based permission flow, and exposes optional logging / analytics hooks.

Android only (minSdk 21). Calling the plugin on iOS raises MissingPluginException.

What it wraps

Native artifact Maven coordinate Required?
Core SDK in.payuinnovations:payuinnovations-smsinsights-core-sdk:1.0.1-20260715.071507-5 Yes (declared by the plugin)
Network SDK in.payuinnovations:payuinnovations-smsinsights-network-sdk:1.0.1-20260714.114535-8 No — transitive from core-sdk (see note below)
Logger SDK in.payuinnovations:payuinnovations-smsinsights-logger-sdk:1.0.1-20260714.104324-5 No (you add it in your app's build.gradle if you want logging)

Network SDK is transitive. The core SDK POM declares payuinnovations-smsinsights-network-sdk as a runtime dependency, and Gradle resolves it automatically — consumers do not need to add it separately. The plugin pins an aligned network-sdk build explicitly (see android/sdk_versions.gradle) so Gradle does not pick up a stale SNAPSHOT cache entry with the old com.smsinsights.network_library.* package.

The plugin exposes two customer-facing Dart classes:

  • SmsSdkWrapper — full lifecycle for the SMS Insights core SDK (init, startSync, setDeviceMatch, forgetUser) plus a Stream<SdkEvent> for callbacks.
  • LoggerWrappersetLog, setIdentity, logEvent. Throws LoggerNotInstalledException if the optional logger artifact is missing from the consumer app.

The plugin automatically tags every init call with platform: flutter and the plugin semver (wrapperVersion) for backend telemetry — you do not need to configure these.


Getting Started

Step 1: Add the plugin to your app

In your Flutter app's pubspec.yaml:

dependencies:
  sms_insights_flutter: ^0.1.0

Then:

flutter pub get

If you depend on a -SNAPSHOT core SDK version, ensure your project-level build.gradle includes the Sonatype snapshot repository:

repositories {
    mavenCentral()
    google()
    maven { url 'https://central.sonatype.com/repository/maven-snapshots/' }
}

Step 2: Permissions

All seven required permissions are declared in the plugin's own AndroidManifest.xml and are automatically merged into your app via Android's manifest merger — you do not need to add anything to your app's manifest.

Permission Type
INTERNET normal
READ_SMS dangerous
ACCESS_COARSE_LOCATION dangerous
ACCESS_FINE_LOCATION dangerous
READ_PHONE_STATE dangerous
READ_PHONE_NUMBERS dangerous
QUERY_ALL_PACKAGES dangerous

Use a runtime-permission package (e.g. permission_handler) to prompt the user for dangerous permissions — the SDK asks for them via the RequestPermissionsEvent flow described in Step 5.

Step 3 (optional): Enable logging

If you want OpenTelemetry traces and CleverTap analytics, add the optional logger artifact to your app's android/app/build.gradle:

dependencies {
  implementation "in.payuinnovations:payuinnovations-smsinsights-logger-sdk:1.0.1-20260714.104324-5"
}

Skip this step if you don't need logging — LoggerWrapper will simply throw LoggerNotInstalledException and SmsSdkWrapper will continue to work normally.

Step 4: Initialise the SDK

Subscribe to the events stream before calling init:

import 'package:sms_insights_flutter/sms_insights_flutter.dart';

SmsSdkWrapper.instance.events.listen((event) {
  switch (event) {
    case InitSuccessEvent():
      print('SDK ready');
    case InitFailureEvent(:final code, :final message):
      print('Init failed: $code $message');
    default:
      break;
  }
});

await SmsSdkWrapper.instance.init(
  const SdkClientConfig(
    clientEmail: 'you@yourcompany.com',
    accessId:    'your-access-key',
    customerId:  'end-user-id',
    // optional fields:
    upperLimit:  180,
    incomePrediction: IncomeEstimation(salary: 50000, company: 'Acme Corp'),
    environment: SdkEnvironmentKind.staging, // default is staging
  ),
);

For production:

environment: SdkEnvironmentKind.production,

For a custom backend:

environment: SdkEnvironmentKind.custom,
customEnvironment: CustomEnvironmentConfig(
  testEmail: 'test@example.com',
  accessKey: 'key',
  envUrl: 'https://your-base-url/',
  signozUrl: 'https://your-signoz-url/',
),

init returns once the native call returns. Auth result arrives asynchronously on the events stream subscribed above.

Step 5: Start sync (with permission handshake)

Subscribe to events before calling startSync so RequestPermissionsEvent is not missed:

import 'package:permission_handler/permission_handler.dart';

SmsSdkWrapper.instance.events.listen((event) async {
  if (event is RequestPermissionsEvent) {
    final granted = await _requestPermissions(event.permissions);
    await SmsSdkWrapper.instance.grantPermissions(
      requestId: event.requestId,
      grantedPermissions: granted,
    );
  }
});

await SmsSdkWrapper.instance.startSync();

// Maps the SDK's Android permission strings to permission_handler objects,
// then requests them all in a single system dialog (same as Kotlin's
// requestPermissionsLauncher.launch(permissions.toTypedArray())).
Future<List<String>> _requestPermissions(List<String> requested) async {
  // Build Permission → [android name, ...] map.
  // ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION both map to
  // Permission.location; when location is granted both strings are returned.
  final mapping = <Permission, List<String>>{};
  for (final p in requested) {
    final handler = switch (p) {
      'android.permission.READ_SMS'               => Permission.sms,
      'android.permission.ACCESS_FINE_LOCATION'   => Permission.location,
      'android.permission.ACCESS_COARSE_LOCATION' => Permission.location,
      'android.permission.READ_PHONE_STATE'       => Permission.phone,
      'android.permission.READ_PHONE_NUMBERS'     => Permission.phone,
      _ => null,
    };
    if (handler != null) (mapping[handler] ??= []).add(p);
  }
  if (mapping.isEmpty) return [];
  final statuses = await mapping.keys.toList().request();
  return [
    for (final entry in statuses.entries)
      if (entry.value.isGranted) ...?mapping[entry.key],
  ];
}

Sync results (SmsSyncSuccessEvent, NoMessagesToSyncEvent, etc.) also arrive on the same events stream.

Running the bundled example

cd example
cp lib/client_credentials.example.dart lib/client_credentials.dart
# Edit lib/client_credentials.dart with your staging credentials
flutter pub get
flutter run

The example pre-fills credentials from client_credentials.dart (gitignored). client_credentials.example.dart ships placeholder values — copy it before your first run.

Subscribe to SmsSdkWrapper.instance.events before calling init so InitSuccessEvent / InitFailureEvent are not dropped.

Step 6: Optional logging

final available = await LoggerWrapper.instance.isAvailable;
if (available) {
  await LoggerWrapper.instance.setIdentity(
    customerId: 'end-user-id',
    clientEmail: 'you@yourcompany.com',
  );
  await LoggerWrapper.instance.setLog('SDK initialised');
  await LoggerWrapper.instance.logEvent(
    'SDK_INIT_SUCCESS',
    data: {'source': 'flutter'},
  );
}

Step 7: Forget user / cleanup

await SmsSdkWrapper.instance.forgetUser();

Cancels all background sync workers and purges persisted state. The native wrapper releases its SDK instance so you can call init again. There is no success callback — failures arrive as ForgetUserFailureEvent on the events stream.


API Reference

SmsSdkWrapper

  • Future<void> init(SdkClientConfig config)
  • Future<void> startSync()
  • Future<bool> grantPermissions({required String requestId, required List<String> grantedPermissions})
  • Future<void> setDeviceMatch(DeviceMatch match)
  • Future<void> forgetUser()
  • Stream<SdkEvent> get events

LoggerWrapper

  • Future<bool> get isAvailable
  • Future<void> setLog(String message, {List<Object?> args = const []})
  • Future<void> setIdentity({String? customerId, String? androidId, String? clientEmail})
  • Future<void> logEvent(String eventName, {Map<String, Object> data = const {}})

Models

  • SdkClientConfig({required clientEmail, required accessId, required customerId, upperLimit?, incomePrediction?, environment?, customEnvironment?})
  • IncomeEstimation({required salary, required company})
  • CustomEnvironmentConfig({required testEmail, required accessKey, required envUrl, required signozUrl})
  • SdkEnvironmentKindstaging, production, custom
  • DeviceMatch({email?, name?, phoneNo?})

Events (sealed SdkEvent)

InitSuccessEvent, InitFailureEvent, RequestPermissionsEvent, SmsSyncSuccessEvent, NoMessagesToSyncEvent, UploadFailureEvent, ForgetUserFailureEvent, MatchDeviceFailureEvent, SmsPermissionNotAvailableEvent, InitNotCalledEvent, UnknownSdkEvent.


Error handling

All synchronous failures surface as SmsInsightsException:

  • INVALID_ARGUMENT — a required field was empty or the wrong type.
  • NATIVE_ERROR — the native SDK threw; check details for the Kotlin stack trace.
  • loggerNotInstalled — the consumer app did not add the optional logger Maven artifact (raised as LoggerNotInstalledException, which is a subclass of SmsInsightsException).

Asynchronous failures from the native SDK arrive on the events stream — handle InitFailureEvent, UploadFailureEvent, ForgetUserFailureEvent, etc.


ProGuard / R8

The core SDK ships consumer ProGuard rules. If minification still strips SDK classes, add to your proguard-rules.pro:

-keep class in.payuinnovations.insights.** { *; }
-keep class in.payuinnovations.network_library.** { *; }
-keep class com.smsinsights.network_library.** { *; }
-keep class com.google.gson.** { *; }
-keepattributes Signature
-keepattributes *Annotation*

Keep both in.payuinnovations.network_library.** and com.smsinsights.network_library.** until PayU fully migrates all published network-sdk builds to the renamed package.


Permissions reference

  • android.permission.INTERNET
  • android.permission.READ_SMS
  • android.permission.ACCESS_COARSE_LOCATION
  • android.permission.ACCESS_FINE_LOCATION
  • android.permission.READ_PHONE_STATE
  • android.permission.READ_PHONE_NUMBERS
  • android.permission.QUERY_ALL_PACKAGES

Troubleshooting

  • NATIVE_ERROR / ClassNotFoundException: RegexInfo on init — the core-sdk and network-sdk SNAPSHOT builds on your machine are out of sync. Core SDK 1.0.1-SNAPSHOT (Jul 2026+) expects in.payuinnovations.network_library.models.RegexInfo, but older network-sdk SNAPSHOT caches still ship com.smsinsights.network_library.models.RegexInfo. Fix options:
    1. Use the aligned snapshot builds pinned in android/sdk_versions.gradle (recommended — the plugin already does this).
    2. Refresh dependencies: ./gradlew --refresh-dependencies then flutter clean && flutter run.
    3. Inspect the resolved AAR: jar tf classes.jar | grep RegexInfo — should show in/payuinnovations/network_library/models/RegexInfo.class.
    4. If no aligned build is available, contact PayU Mobile — this is a native SDK publishing mismatch, not fixable in Dart/Kotlin wrapper code.
  • LoggerNotInstalledException — add implementation "in.payuinnovations:payuinnovations-smsinsights-logger-sdk:1.0.1-20260714.104324-5" to your app's android/app/build.gradle and rebuild. Or call LoggerWrapper.instance.isAvailable first to skip logger calls gracefully.
  • InitNotCalledEvent after startSync — call init and wait for InitSuccessEvent before invoking startSync.
  • SmsPermissionNotAvailableEvent — the user denied SMS permission; guide the user to grant READ_SMS in app settings.
  • Maven Central snapshot 403 — add the Sonatype snapshot repository (see Step 1).

Device QA checklist (before production release)

Run on a physical Android device with the bundled example app or your integration:

  1. Subscribe to events, then call init — confirm InitSuccessEvent.
  2. Call startSync — grant all requested permissions when prompted.
  3. Confirm sync result (SmsSyncSuccessEvent or NoMessagesToSyncEvent).
  4. Call forgetUser, then init again — confirm a second session works.
  5. (Optional) Call setDeviceMatch before startSync and verify no MatchDeviceFailureEvent.
  6. (Optional) With logger SDK added, confirm LoggerWrapper calls succeed.

Native SDK dependencies are pinned to aligned SNAPSHOT builds in android/sdk_versions.gradle. Prefer stable 1.0.1+ artifacts from PayU when available before a production release.


Releasing

pubspec.yaml is the single source of truth for the plugin version. It drives pub.flutter-io.cn publishing, the wrapperVersion sent to the native SDK on every init, and the Android library artifact version (read from pubspec in android/build.gradle).

Release checklist:

# 1. Bump version in pubspec.yaml (e.g. 0.1.0 → 0.1.1)
# 2. Sync the generated Dart constant used for wrapperVersion telemetry
dart run tool/sync_version.dart
flutter test
# 3. Update CHANGELOG.md
# 4. Publish
flutter pub publish

lib/src/version.dart is generated — do not edit it by hand. The drift test in test/version_sync_test.dart fails if it falls out of sync with pubspec.


License

Apache-2.0. See LICENSE.

Libraries

sms_insights_flutter
Public entry-point for the sms_insights_flutter plugin.