100+ Social Media Skills for AI Agents
AI 에이전트를 소셜 미디어 전문가로 전환, Claude·ChatGPT·Gemini·등 100개 이상의 무료 오픈소스 스킬 제공
Setgreet는 앱 성장에 필요한 온보딩, 유료벽, 설문을 설계·제공·측정합니다
Setgreet designs, ships, and measures the onboarding, paywalls, and surveys that grow your app. Native on iOS, Android, React Native & Flutter. Live in minutes. No code, no release, no revenue cut.
마케팅Dart · SwiftGitHub @setgreet온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
Setgreet Flutter SDK - Mobile app user onboarding
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Setgreet Flutter SDK allows you to show Setgreet flows in your Flutter app.
Add setgreet as a dependency in your pubspec.yaml file:
dependencies:
setgreet: ^1.0.0
Then run:
flutter pub get
The plugin bundles the Setgreet iOS SDK, so there is nothing to add on either dependency manager.
Swift Package Manager (the default from Flutter 3.44): the plugin ships a Swift package (ios/setgreet/Package.swift) that depends on setgreet-ios-sdk at the exact version this release was built with. Flutter resolves it when you build or run the app.
CocoaPods (projects that have not moved to Swift Package Manager yet, and Flutter versions before 3.44): the same SDK is included as prebuilt frameworks inside the plugin, so no extra pod source is needed. Run:
cd ios && pod install
No additional setup required for Android.
[!WARNING] A missing or incomplete Podfile setup for Lottie still builds without errors, then crashes at launch, for example with
dyld: Library not loaded: @rpath/Lottie.framework/Lottie(referenced fromSetgreetSDKLottie.framework). Apply every Podfile line below. A Podfile allows only onepre_installand onepost_installblock, so merge these lines into the blocks your Podfile already has instead of adding second ones.
Lottie support is opt-in on iOS, so apps that don't use it stay smaller. Without it, a Lottie component in a flow renders nothing on iOS; the rest of the flow is unaffected. On Android the native SDK already includes Lottie.
iOS with Swift Package Manager: the plugin cannot add an optional product for you, so add it to the app in Xcode (ios/Runner.xcworkspace, File > Add Package Dependencies):
https://github.com/setgreet/setgreet-ios-sdk.git, Exact Version set to the iOS SDK version this plugin release bundles (see the CHANGELOG entry for your version, or the exact: line in the plugin's ios/setgreet/Package.swift). Add the SetgreetSDKLottie product to the Runner target. The version must match the plugin's, since both resolve the same package.https://github.com/airbnb/lottie-spm.git, from 4.6.0. Add the Lottie product to the Runner target. SetgreetSDKLottie is a prebuilt framework and cannot declare this dependency itself.Then register at startup (below). No Podfile changes are needed; if your Podfile still has a pod 'setgreet/Lottie' line from a CocoaPods setup, remove it so the plugin is not linked twice.
iOS with CocoaPods: add the setgreet/Lottie pod to your ios/Podfile. It brings in the SetgreetSDKLottie module and lottie-ios. SetgreetSDKLottie is a prebuilt dynamic framework that loads @rpath/Lottie.framework, built with library evolution, so lottie-ios must be built as a dynamic framework with library evolution too.
With dynamic frameworks (use_frameworks!, Flutter's default), add the pod and one setting to the post_install block Flutter generated:
use_frameworks!
target 'Runner' do
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
pod 'setgreet/Lottie', :path => '.symlinks/plugins/setgreet/ios'
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) # already in your Podfile
# Added for Setgreet Lottie
if target.name == 'lottie-ios'
target.build_configurations.each do |config|
config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
end
end
end
end
Static frameworks (use_frameworks! :linkage => :static): pods are then built as static frameworks, and SetgreetSDKLottie cannot load a static Lottie, so the app crashes at launch with the error above. Keep the pod line and the post_install setting, and also build just lottie-ios as a dynamic framework in pre_install (merge it into yours if you already have one):
use_frameworks! :linkage => :static
pre_install do |installer|
installer.pod_targets.each do |pod|
if pod.name == 'lottie-ios'
def pod.build_type
Pod::BuildType.dynamic_framework
end
end
end
end
Then run cd ios && pod install.
Register at startup, once, before the first flow shows:
await Setgreet.registerLottie(); // true when Lottie can render
registerLottie() returns false on iOS when neither the setgreet/Lottie pod nor the SetgreetSDKLottie Swift package product is in the app. On Android it always returns true. The example app does both steps with CocoaPods.
Initialize the SDK in your Flutter app:
import 'package:setgreet/setgreet.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
await Setgreet.initialize(
'your_app_key_here',
config: SetgreetConfig(
debugMode: true, // Enable for development
),
);
} catch (e) {
print('Failed to initialize Setgreet: $e');
}
runApp(MyApp());
}
Sync your app's theme (colors + typography) to Setgreet so generated flows
match your brand. A Flutter app draws its UI from its own ThemeData, which
the native SDKs can't see — their auto-sync reads the host platform theme
(iOS system colors / the Android Activity theme), which for a Flutter app is
just framework defaults. So Flutter syncs its ThemeData explicitly:
import 'package:flutter/material.dart';
import 'package:setgreet/setgreet.dart';
// Call from a widget below MaterialApp (so Theme.of resolves your app theme),
// e.g. once after the first frame.
WidgetsBinding.instance.addPostFrameCallback((_) {
Setgreet.syncTheme(context); // extracts ColorScheme + TextTheme, posts to Setgreet
});
syncTheme reads the current Material 3 ColorScheme and TextTheme from the
BuildContext and posts them to POST /sdk/sync-theme. It throws
SetgreetThemeException on failure. Point it at a non-production backend by
passing apiUrl in SetgreetConfig at initialize time.
Identifies a user for Setgreet analytics and flow management.
Parameters:
userId (String): The unique identifier for the userattributes (Optional): Additional user attributesoperation (Optional): Operation type ('create' or 'update', defaults to 'create')locale (Optional): User's locale (e.g., "en-US"). Saved for the user only when you pass one; the device language is never stored. See Language for the order Setgreet usesuserHash (Optional): The hex HMAC-SHA256 of userId, computed on your server with your app's identity verification secret. Lets Setgreet verify that the user id belongs to the signed-in user. See Verify Usersawait Setgreet.identifyUser(
'user123',
attributes: {
'plan': 'premium',
'signup_date': '2025-08-27',
},
operation: 'create',
locale: 'en-US',
);
To let Setgreet verify that a user id belongs to the signed-in user, compute a hash of the user id on your server and pass it as userHash. The hash is the hex HMAC-SHA256 of the exact userId, keyed with the identity verification secret from your app's settings. Keep the secret on your server: never ship it in the app.
// userHash comes from your server, for this exact user id
await Setgreet.identifyUser(
'user_123',
attributes: {'plan': 'pro'},
userHash: userHash,
);
The SDK keeps the hash with the user id and sends it on every request for that user. It forgets the hash when you call resetUser or identify a different user id, so always pass the hash that belongs to the id in the same call. An empty hash is treated as no hash.
Call setLocale when the user picks a language in your app. The choice is saved on the device: it survives relaunches and resetUser, and it works before initialize, so anonymous users get localized flows too.
await Setgreet.setLocale('tr-TR'); // a BCP-47 tag
await Setgreet.setLocale(null); // back to the device language
Flows are shown in, in order:
setLocale override.locale passed to identifyUser for the current user.null or an empty string removes the override. An invalid tag is ignored with a warning, and the previous locale stays.
Clears user identification data and resets user session state for logout scenarios.
Setgreet.resetUser();
The SDK automatically generates an anonymous ID on initialization, which persists across app launches. When identifyUser is called, the anonymous identity is merged with the identified user. A new anonymous ID is generated when resetUser() is called.
final anonId = await Setgreet.anonymousId;
https://app.setgreet.com/flows/1234, the flow ID is 1234.To show the Setgreet flow, call the following method:
// Show a specific flow
await Setgreet.showFlow('your_flow_id');
Tracks a screen view for analytics and potential flow triggers.
Parameters:
screenName (String): The name of the screen being viewedproperties (Optional): Additional properties associated with the screen view// Track screen view
await Setgreet.trackScreen('home_screen');
// With properties
await Setgreet.trackScreen(
'product_details',
properties: {
'product_id': '12345',
'category': 'electronics',
},
);
Tracks custom events for analytics and flow triggers.
Parameters:
eventName (String): The name of the custom eventproperties (Optional): Additional properties associated with the event// Simple event
await Setgreet.trackEvent('button_clicked');
// Event with properties
await Setgreet.trackEvent(
'purchase_completed',
properties: {
'product_id': '12345',
'amount': 99.99,
'currency': 'USD',
},
);
Listen to flow lifecycle events to track user interactions and flow completion.
Available Callbacks:
onFlowStarted: Called when a flow begins displayingonFlowCompleted: Called when user completes all screens in the flowonFlowDismissed: Called when user dismisses the flow before completiononScreenChanged: Called when user navigates between screensonActionTriggered: Called when a button's action is triggered, just before it runs, with the action's type (see Action types). It also fires when required inputs on the screen then stop the actiononCustomAction: Called when the user taps a button or card whose action is Custom Action, with the custom data configured on itonTrackEvent: Called when the user taps a button or card whose action is Track Event, with the event name and properties, so you can forward them to your own analyticsonPermissionRequested: Called when a permission request completesonError: Called when an error occurs during flow operations, with an error type (network, parse, display or unknown) and a messageonPurchaseCompleted, onPurchaseCancelled, onPurchaseFailed, onPurchasePending, onRestoreCompleted, onRestoreFailed: paywall outcomes (see Purchases and paywalls)Using Callbacks Builder:
Setgreet.setFlowCallbacks(
SetgreetFlowCallbacks()
..onFlowStarted((event) {
print('Flow started: ${event.flowId}');
print('Total screens: ${event.screenCount}');
})
..onFlowCompleted((event) {
print('Flow completed: ${event.flowId}');
print('Duration: ${event.durationMs}ms');
})
..onFlowDismissed((event) {
print('Flow dismissed: ${event.flowId}');
print('Reason: ${event.reason}');
print('Screen: ${event.screenIndex + 1}/${event.screenCount}');
})
..onScreenChanged((event) {
print('Screen changed: ${event.fromIndex + 1} -> ${event.toIndex + 1}');
})
..onActionTriggered((event) {
print('Action: ${event.actionType} on screen ${event.screenIndex + 1}');
})
..onCustomAction((event) {
print('Custom action: ${event.data}');
})
..onTrackEvent((event) {
// Forward to your own analytics
print('Track event: ${event.eventName} ${event.properties}');
})
..onPermissionRequested((event) {
print('Permission: ${event.permissionType} -> ${event.result}');
})
..onError((event) {
print('Error: ${event.errorType} - ${event.message}');
}),
);
Using Stream API:
// Listen to all events as a stream
Setgreet.flowEvents.listen((event) {
switch (event) {
case FlowStartedEvent():
print('Flow started: ${event.flowId}');
case FlowCompletedEvent():
print('Flow completed: ${event.flowId}');
case FlowDismissedEvent():
print('Flow dismissed: ${event.reason}');
case ScreenChangedEvent():
print('Screen changed: ${event.fromIndex} -> ${event.toIndex}');
case ActionTriggeredEvent():
print('Action: ${event.actionType}');
case CustomActionEvent():
print('Custom action: ${event.data}');
case TrackEventEvent():
print('Track event: ${event.eventName}');
case PermissionRequestedEvent():
print('Permission: ${event.permissionType} -> ${event.result}');
case FlowErrorEvent():
print('Error: ${event.errorType}');
case PurchaseCompletedEvent():
print('Purchased: ${event.productRef}');
case PurchaseCancelledEvent() ||
PurchaseFailedEvent() ||
PurchasePendingEvent() ||
RestoreCompletedEvent() ||
RestoreFailedEvent():
print('Paywall outcome: $event');
}
});
SetgreetFlowEvent is sealed: a switch over it must cover every event type, or end with a default case.
The callbacks and Setgreet.flowEvents work together: they share one subscription to the native events, so you can use both, listen to flowEvents more than once, and cancel a flowEvents listener without stopping the callbacks.
Dismiss Reasons:
| Reason | Description |
|---|---|
userClose | User tapped the close button |
userSkip | User tapped the skip button |
backPress | User pressed the back button (hardware) |
replaced | Flow was replaced by a higher priority flow |
programmatic | Flow was dismissed programmatically |
swipeDown | User swiped down to dismiss a bottom sheet |
completed | Flow reached its end node |
Permission Types:
| Type | Description |
|---|---|
notification | Push notification permission |
location | Location access permission |
camera | Camera access permission |
tracking | App Tracking Transparency (iOS only; notRequired on Android) |
microphone | Microphone access permission |
photoLibrary | Photo library read access |
Permission Results:
| Result | Description |
|---|---|
granted | Permission was granted by the user |
denied | Permission was denied by the user |
permanentlyDenied | Permission was permanently denied |
alreadyGranted | Permission was already granted |
notRequired | Permission request was not required |
ActionTriggeredEvent.actionType is one of these snake_case strings, the same on iOS and Android:
| Action type | Action |
|---|---|
next, previous | Go to the next or previous screen |
skip | Skip the flow |
dismiss | Close the flow |
url | Open a URL |
request_notification_permission, request_location_permission, request_camera_permission, request_tracking_permission, request_microphone_permission, request_photo_library_permission | Request a permission (the result fires onPermissionRequested) |
request_review | Ask for an App Store or Google Play review |
share | Open the share sheet |
open_settings | Open the app's system settings |
track_event | Track an event (also fires onTrackEvent); actionName is the event name |
custom | Custom action (also fires onCustomAction) |
purchase, restore_purchases | Buy a product or restore purchases (the outcome fires the purchase events) |
actionName is the Track Event's event name for track_event and null for every other action.
| Event | Fields |
|---|---|
CustomActionEvent | flowId, data (Map<String, Object?>?, the custom data configured in the dashboard), screenIndex, timestamp |
TrackEventEvent | flowId, eventName, properties (Map<String, Object?>?), screenIndex, timestamp |
On iOS, data and properties keep their JSON types (numbers, booleans, nested maps and lists). On Android, every value arrives as a string, and a nested object or array as null, so read values defensively, for example int.tryParse('${event.data?['count']}').
On Android, the plugin receives onCustomAction and onTrackEvent through the native SDK's Setgreet.setFlowListener, which holds a single listener. Setgreet.initialize replaces a listener your Android code set earlier, and a Setgreet.setFlowListener call from your Android code after initialize stops these two events in Dart. Use the Dart callbacks or Setgreet.flowEvents instead of a native listener.
Setgreet renders paywall screens natively and branches the flow on the outcome. Your app implements one Dart interface, SetgreetPurchaseProvider, on top of the purchase tool it already uses (RevenueCat, Adapty, StoreKit, Google Play Billing). Setgreet wraps no vendor SDK, stores no vendor key and keeps no entitlement state; the SDK owns the loading state, the plan picker, live prices in copy, the purchase lock, branching, analytics and syncing your product catalog to the dashboard.
Configure your purchase tool yourself, then register the provider after Setgreet.initialize:
await Setgreet.initialize('your_app_key_here');
// Your own RevenueCat setup (Setgreet never configures it).
await Purchases.configure(PurchasesConfiguration('<your RevenueCat public SDK key>'));
await Setgreet.setPurchaseProvider(SetgreetRevenueCatProvider());
defaultSource (the current offering unless you pass another) is loaded in the background right away, so the catalog reaches the dashboard before any paywall is shown. It is also the source a paywall screen without a plan picker reads, and the only source whose catalog is synced:
await Setgreet.setPurchaseProvider(
SetgreetRevenueCatProvider(),
defaultSource: const SetgreetPurchaseSource.offering('onboarding'),
);
A registration made before initialize throws SetgreetInitializationException; a providerId that does not match ^[a-z][a-z0-9_]{0,39}$ throws SetgreetException. A later call replaces the provider. Register from the root isolate only: the native SDK calls the provider through the root isolate's platform channel.
abstract class SetgreetPurchaseProvider {
String get providerId; // "revenuecat", "adapty", ... (^[a-z][a-z0-9_]{0,39}$)
Future<List<SetgreetPurchaseProduct>> loadProducts(SetgreetPurchaseSource source);
Future<SetgreetPurchaseResult> purchase(String productRef, SetgreetPurchaseSource source);
Future<SetgreetRestoreResult> restorePurchases();
Future<SetgreetPurchaseAccessState?> accessState(); // null = cannot say
void trackPaywallImpression(SetgreetPaywallImpressionContext context) {} // optional
void setAttributes(Map<String, String> attributes) {} // optional
Future<void> identify(String userId) async {} // optional
Future<void> reset() async {} // optional
}
Extend the class; the optional methods default to no-ops.
$rc_annual) is unique only within its source, since every offering can have a $rc_annual. purchase receives the source the product was loaded from: buy the product you returned for that ref from that source.loadProducts throws a SetgreetPurchaseError (providerNotInitialized, productNotFound, networkError, ...) when products cannot be loaded; any other error is reported as unknown. purchase and restorePurchases report every outcome as a status; a thrown SetgreetPurchaseError becomes failed with its code, any other error failed with unknown. accessState returns null when it cannot say, and a thrown error counts as null.priceAmount is a double (the type purchases_flutter reports). The native SDKs keep prices as exact decimals: the plugin converts the value through its shortest decimal form, so 49.99 stays 49.99.identify and reset run when you call Setgreet.identifyUser / Setgreet.resetUser. Implement them only if Setgreet should drive your purchase tool's identity; the RevenueCat reference leaves them empty because your app already calls Purchases.logIn / logOut.trackPaywallImpression and setAttributes return void, so nothing awaits them: if you override them with an async body, catch your own errors inside it.example/lib/setgreet_revenuecat_provider.dart is a complete provider backed by RevenueCat. Copy it into your app; it compiles against the purchases_flutter dependency your app already has (10.3.0 or later) and adds nothing to Setgreet's own dependencies. The example app shows where to paste your RevenueCat keys (example/lib/config.dart). On Google Play it drops the app name Google Play appends to product titles, so "Premium Annual (Your App)" reaches your paywall as "Premium Annual".
loadProducts (and for accessState when the screen skips users who already have access) before the screen follows its "unavailable" policy. purchase and restorePurchases have no time limit, since a store sheet can stay open for minutes.loadProducts or accessState, the plugin stops waiting after 30 s. identify and reset have no time limit: the native SDK runs them one after another and writes the user's access attributes once they are done, so cutting a slow one short could attribute the previous user's access to the new one.failed with not_configured), so a paywall button never stays locked.FlutterActivity, and purchases_flutter launches Google Play Billing from that FlutterActivity. If that activity is destroyed underneath the paywall, loads and purchases fail as engine-detached.Setgreet.setFlowCallbacks(
SetgreetFlowCallbacks()
..onPurchaseCompleted((event) => print('${event.productRef}: ${event.accessRefs}'))
..onPurchaseCancelled((event) => print('Cancelled ${event.productRef}'))
..onPurchaseFailed((event) => print('Failed: ${event.error.code.rawValue} ${event.error.providerCode}'))
..onPurchasePending((event) => print('Pending ${event.productRef}'))
..onRestoreCompleted((event) => print('Restore: ${event.status.rawValue}'))
..onRestoreFailed((event) => print('Restore failed: ${event.error.code.rawValue}')),
);
Each of these events carries flowId, screenId, componentId (the button), provider and timestamp. The four purchase events add productRef; PurchaseCompletedEvent and RestoreCompletedEvent add accessRefs (the access the user now holds); RestoreCompletedEvent adds status (restored or nothingToRestore); the two failure events add error (code, providerCode, message). The same events arrive on Setgreet.flowEvents.
Flows can render data fetched from your own API — a product list filtered by answers the user gave earlier in the same flow. You register the endpoint once in the Setgreet dashboard (Data Sources), bind it into a screen, and it renders.
Nothing to do on the Flutter side. The feature needs no code, no new permission and no new dependency in your app.
What is worth knowing:
goal in the editor arrives as goal. Attributes you set via identifyUser are available to it too.See External Data for the endpoint contract and the signature-verification recipe.
If your flows use permission buttons, add the required keys to your Info.plist (iOS):
<!-- For location permission -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your description for location usage</string>
<!-- For camera permission -->
<key>NSCameraUsageDescription</key>
<string>Your description for camera usage</string>
<!-- For tracking permission (App Tracking Transparency) -->
<key>NSUserTrackingUsageDescription</key>
<string>Your description for tracking usage</string>
<!-- For microphone permission -->
<key>NSMicrophoneUsageDescription</key>
<string>Your description for microphone usage</string>
<!-- For photo library permission -->
<key>NSPhotoLibraryUsageDescription</key>
<string>Your description for photo library usage</string>
Note: Notification permission doesn't require an Info.plist key. A missing key is logged, reported as notRequired, and the flow advances.
On Android the SDK declares only INTERNET; declare the permissions your flows request in your app's AndroidManifest.xml: POST_NOTIFICATIONS (API 33+), ACCESS_FINE_LOCATION, CAMERA, RECORD_AUDIO, and READ_MEDIA_IMAGES (API 33+) or READ_EXTERNAL_STORAGE below. Tracking has no Android equivalent and advances immediately.
A screen can also carry a permission gate ("Skip when granted" in the editor): the SDK skips the primer when the permission is already granted, with nothing to do on the Flutter side.
If you continue to have issues, please open an issue with:
Dart tests and the analyzer, from the repository root and from example/:
flutter test && flutter analyze
cd example && flutter test && flutter analyze
The Android side of the purchase bridge has JVM unit tests in android/src/test. Run them through the example app's Gradle build:
cd example/android && ./gradlew :setgreet:testDebugUnitTest
The iOS side has XCTest cases in example/ios/RunnerTests. Build the example once first (the build runs pod install), then run them on a simulator:
cd example
flutter build ios --simulator --no-codesign
cd ios
xcodebuild test -workspace Runner.xcworkspace -scheme Runner -only-testing:RunnerTests \
-destination 'platform=iOS Simulator,name=iPhone 16'
The example targets iOS 15.1 and builds the simulator app for arm64 only (EXCLUDED_ARCHS on the Runner target), so both commands work with Xcode 27 as they are. On an Intel Mac, remove that exclusion to run the example in a simulator.