Migrating from 2.x to 3.0
This guide covers every change you need to make to upgrade from StorifyMe SDK 2.x to 3.0 across Android, iOS, Flutter, and React Native.
Before you start
Migration details differ by platform. On iOS, most renamed v2 APIs remain available with deprecation warnings in 3.x; the StorifyMeExperienceType.customAd case was removed, and the minimum deployment target is now iOS 15. See the platform section before changing your integration.
What changed:
- Event callback names and types change by platform — see the relevant section
- Widget class name —
StoriesView→StorifyMeWidget(Android / Flutter) - iOS content model —
StorifyMeStoryModel→StorifyMeExperience - iOS content types —
StorifyMeExperienceTypecases include.story,.shorts,.snap,.ad, and.unknown
Android
1. Widget class name (XML layouts)
<com.storify.android_sdk.ui.view.StoriesView
android:id="@+id/storiesView"
android:layout_width="match_parent"
android:layout_height="150dp" />
<com.storify.android_sdk.StorifyMeWidget
android:id="@+id/storiesView"
android:layout_width="match_parent"
android:layout_height="150dp" />
Both class names work in 3.x (StoriesView is now a deprecated subclass of StorifyMeWidget).
android:id="@+id/storiesView" is a developer-defined label — it's not linked to the class name. You only need to change the class in the XML. Renaming the ID is optional and would require updating every R.id.storiesView reference in your code.
2. Widget class name (Kotlin)
val widget = findViewById<StoriesView>(R.id.storiesView)
// R.id.storiesView is your existing resource ID — no need to change it
val widget = findViewById<StorifyMeWidget>(R.id.storiesView)
3. Event listener callbacks
StorifyMe.instance.eventListener = object : StorifyMeEventListener() {
override fun onLoad(stories: List<StoryWithSeen>) {
// show story count badge
}
override fun onStoryOpened(story: StoryWithSeen, index: Int) {
analytics.track("story_opened", mapOf("id" to story.story.id))
}
}
widget.widgetListener = object : StorifyMeWidgetListener {
override fun onLoaded(widgetId: Long, experiences: List<StorifyMeExperience>) {
// show experience count badge
}
}
widget.experienceListener = object : StorifyMeExperienceListener {
override fun onOpened(experience: StorifyMeExperience, index: Int) {
analytics.track("experience_opened", mapOf("id" to experience.id))
}
}
In 3.x the legacy listeners remain available with deprecation warnings. New listeners are attached to a widget and split by responsibility.
4. Handle experience navigation
override fun onStoryDeeplinkTriggered(
story: StoryWithSeen,
completion: (StorifyMeStoryDeeplinkTriggerCompletion) -> Unit
) {
if (needsPaywall(story)) {
showPaywall()
completion(StorifyMeStoryDeeplinkTriggerCompletion.IGNORE_PRESENTING_STORY)
} else {
completion(StorifyMeStoryDeeplinkTriggerCompletion.OPEN_STORY_BY_DEFAULT)
}
}
widget.experienceListener = object : StorifyMeExperienceListener {
override fun onNavigationRequested(
experience: StorifyMeExperience,
completion: (StorifyMeExperienceNavigationDecision) -> Unit,
) {
if (needsPaywall(experience)) {
showPaywall()
completion(StorifyMeExperienceNavigationDecision.HANDLE_NAVIGATION)
} else {
completion(StorifyMeExperienceNavigationDecision.OPEN_EXPERIENCE)
}
}
}
The v3 navigation callback runs when an experience has a navigation URL. Read it from experience.navigationUrl; it replaces the deprecated deeplink property.
5. Using StorifyMeExperienceType
override fun onOpened(experience: StorifyMeExperience, index: Int) {
when (experience.type) {
StorifyMeExperienceType.SHORTS, StorifyMeExperienceType.SNAP ->
analytics.track("short_opened")
StorifyMeExperienceType.STORY ->
analytics.track("story_opened")
StorifyMeExperienceType.AD ->
analytics.track("ad_viewed")
StorifyMeExperienceType.UNKNOWN -> Unit
else -> { }
}
}
Full rename reference
| 2.x | 3.x |
|---|---|
StoriesView | StorifyMeWidget |
StoryWithSeen | StorifyMeExperience |
StorifyMe | StorifyMeSDK |
StorifyMeEnv | StorifyMeEnvironment |
StorifyMeEventListener | focused widget, experience, ad, interaction, and commerce listeners |
onLoad(stories) | onLoaded(widgetId, experiences) |
onStoryOpened(story, index) | onOpened(experience, index) |
onStoryFinished(story, index) | onFinished(experience, index) |
onStoryClosed(story) | onClosed(experience) |
onStoryShared(story) | onShared(experience) |
onStoryDeeplinkTriggered(story, completion) | onNavigationRequested(experience, completion) |
StorifyMeStoryDeeplinkTriggerCompletion.OPEN_STORY_BY_DEFAULT | StorifyMeExperienceNavigationDecision.OPEN_EXPERIENCE |
StorifyMeStoryDeeplinkTriggerCompletion.IGNORE_PRESENTING_STORY | StorifyMeExperienceNavigationDecision.HANDLE_NAVIGATION |
StorifyMeWidgetConfig.Builder.setCustomerId(...) | StorifyMeSDK.instance.configureCustomerId(...) |
iOS
SDK entry point
3.0 adds StorifyMeSDK, a static facade for initialization, state, lifecycle, and direct experience presentation. It forwards to StorifyMeInstance.shared; existing code using the instance API remains supported.
StorifyMeSDK.initialize(
accountId: "ACCOUNT_ID",
apiKey: "API_KEY",
environment: .EU
)
StorifyMeSDK.openExperienceByHandle(handle: "experience-handle")
1. Event delegate callbacks
extension MyViewController: StorifyMeStoryEventProtocol {
func onLoad(widgetId: Int, stories: [StorifyMeStoryModel]) {
updateBadge(count: stories.count)
}
func onStoryOpened(story: StorifyMeStoryModel?, index: Int) {
analytics.track("story_opened")
}
func onStoryFinished(story: StorifyMeStoryModel?, index: Int) { }
func onStoryClose(story: StorifyMeStoryModel?) { } // note: no 'd' in 2.x
func onStoryShared(story: StorifyMeStoryModel?) { }
}
// Assign focused delegates to the widget before calling load().
widget.widgetDelegate = self
widget.experienceDelegate = self
extension MyViewController: StorifyMeWidgetDelegate, StorifyMeExperienceDelegate {
func onLoaded(widgetID: StorifyMeWidgetID, experiences: [StorifyMeExperience]) {
updateBadge(count: experiences.count)
}
func onOpened(_ experience: StorifyMeExperience, at index: Int) {
analytics.track("experience_opened")
}
func onFinished(_ experience: StorifyMeExperience, at index: Int) { }
func onClosed(_ experience: StorifyMeExperience?) { }
func onShared(_ experience: StorifyMeExperience) { }
}
2. Deeplink intercept
func onStoryDeeplinkTriggered(
story: StorifyMeStoryModel,
completion: @escaping (StorifyMeStoryDeeplinkTriggerCompletion) -> Void
) {
if needsPaywall(story) {
showPaywall()
completion(.ignorePresentingStory)
} else {
completion(.openStoryByDefault)
}
}
func onNavigationRequested(
_ experience: StorifyMeExperience,
completion: @escaping (StorifyMeExperienceNavigationDecision) -> Void
) {
if needsPaywall(experience) {
showPaywall()
completion(.handleNavigation)
} else {
completion(.openExperience)
}
}
3. Content type branching
func onOpened(experience: StorifyMeExperience?, index: Int) {
switch experience.type {
case .shorts, .snap:
analytics.track("short_opened")
case .story:
analytics.track("story_opened")
case .ad:
analytics.track("ad_viewed")
default:
break
}
}
StorifyMeStoryModel is a deprecated typealias to StorifyMeExperience. Existing code remains source-compatible in 3.x; use StorifyMeExperience in new code.
Full rename reference
| 2.x | 3.x |
|---|---|
StorifyMeStoryModel | StorifyMeExperience |
StorifyMeStoryEventProtocol | StorifyMeWidgetDelegate, StorifyMeExperienceDelegate, StorifyMeAdDelegate, StorifyMeInteractionDelegate, StorifyMeCommerceDelegate |
onLoad(widgetId:stories:) | onLoaded(widgetID:experiences:) |
onStoryOpened(story:index:) | onOpened(_:at:) |
onStoryFinished(story:index:) | onFinished(_:at:) |
onStoryClose(story:) | onClosed(_:) |
onStoryShared(story:) | onShared(_:) |
onStoryDeeplinkTriggered(story:completion:) | onNavigationRequested(_:completion:) |
onWidgetScrollViewDidScroll(widgetId:scrollView:) | onScrolled(widgetID:offsetX:) |
onAction(type:data:) / onEvent(type:data:) | onAction(_:) / onEvent(_:) with typed payloads |
onShopping(type:data:) | StorifyMeCommerceDelegate cart callbacks |
StorifyMeStoryDeeplinkTriggerCompletion.openStoryByDefault | StorifyMeExperienceNavigationDecision.openExperience |
StorifyMeStoryDeeplinkTriggerCompletion.ignorePresentingStory | StorifyMeExperienceNavigationDecision.handleNavigation |
The removed .customAd case is replaced by .ad; inspect experience.adInfo to distinguish ad sources.
iOS installation compatibility
iOS 3.x requires iOS 15.0 or later. CocoaPods publishing has ended for the iOS SDK; use Swift Package Manager or Carthage for current releases.
Flutter
Requirements and iOS installation
Flutter SDK 3.0.0 requires Flutter 3.47.0 or later, Dart 3.13.0 or later, and iOS 15 or later. Android builds require AGP 9.3.1, Gradle 9.5.0, and NDK 28.2.13676358 or later; migrate the app to AGP 9 built-in Kotlin as described in Flutter's migration guide.
The Flutter 3.x plugin uses Swift Package Manager and does not include CocoaPods support. Apps that must keep CocoaPods can pin storifyme_flutter to 2.6.4, the final CocoaPods-compatible release.
1. Widget name
StoriesViewWidget(
widgetId: WIDGET_ID,
)
StorifyMeWidget( // preferred v3 name; StoriesViewWidget still works
widgetId: WIDGET_ID,
)
2. Event listener mixin
class _MyState extends State<MyWidget>
with NativeEventListener, StorifyMeAdEventListener {
void onLoad(int widgetId, List<StorifyMeStory> stories) {
setState(() => _count = stories.length);
}
void onStoryOpened(StorifyMeStory? story, int index) { }
void onStoryFinished(StorifyMeStory? story, int index) { }
void onStoryClosed(StorifyMeStory? story) { }
void onStoryShared(StorifyMeStory? story) { }
void onAdDisplayed(StorifyMeStory? story, int index) { }
}
class _MyState extends State<MyWidget>
with NativeEventListener, StorifyMeAdEventListener {
void onLoad(int widgetId, List<StorifyMeExperience> experiences) {
setState(() => _count = experiences.length);
}
void onOpened(StorifyMeExperience? experience, int index) { }
void onFinished(StorifyMeExperience? experience, int index) { }
void onClosed(StorifyMeExperience? experience) { }
void onShared(StorifyMeExperience? experience) { }
void onAdDisplayed(StorifyMeExperience? experience, int index) { }
}
3. Deeplink intercept
Future<StorifyMeStoryDeeplinkTriggerCompletion> onStoryDeeplinkTriggered(
StorifyMeStory? story) async {
if (needsPaywall(story)) {
showPaywall();
return StorifyMeStoryDeeplinkTriggerCompletion.ignorePresentingStory;
}
return StorifyMeStoryDeeplinkTriggerCompletion.openStoryByDefault;
}
Future<StorifyMeOpenAction> onExperienceWillOpen(
StorifyMeExperience? experience) async {
if (needsPaywall(experience)) {
showPaywall();
return StorifyMeOpenAction.intercept;
}
return StorifyMeOpenAction.open;
}
4. Content type branching
void onOpened(StorifyMeExperience? experience, int index) {
switch (experience?.type) {
case StorifyMeExperienceType.shorts:
case StorifyMeExperienceType.snap:
analytics.track('short_opened');
break;
case StorifyMeExperienceType.story:
analytics.track('story_opened');
break;
default:
break;
}
}
Full rename reference
| 2.x | 3.x |
|---|---|
StoriesViewWidget | StorifyMeWidget |
StorifyMeStory | StorifyMeExperience |
onLoad(widgetId, stories) | onLoad(widgetId, experiences) |
onStoryOpened(story, index) | onOpened(experience, index) |
onStoryFinished(story, index) | onFinished(experience, index) |
onStoryClosed(story) | onClosed(experience) |
onStoryShared(story) | onShared(experience) |
onStoryDeeplinkTriggered(story) | onExperienceWillOpen(experience) |
onAdDisplayed(story, index) | onAdDisplayed(experience, index) |
StorifyMeStoryDeeplinkTriggerCompletion.openStoryByDefault | StorifyMeOpenAction.open |
StorifyMeStoryDeeplinkTriggerCompletion.ignorePresentingStory | StorifyMeOpenAction.intercept |
Flutter 3.0 also adds typed StorifyMeInteractionListener and StorifyMeCommerceListener callbacks, StorifyMeError, StorifyMeAdInfo, experience ID filtering, widget themes, and the experience-named widget navigator methods. See the Flutter events guide and experience model guide for examples.
Pass content signing keys through Params.INTEGRITY_PUBLIC_KEYS during initialization to enable verification. The synchronous native request interceptor cannot be exposed as a Dart callback across Flutter's asynchronous method channel.
The Android SDK also has setExperienceViewerOrientation; Flutter exposes this as an Android-only plugin method. Calling it on iOS has no effect.
React Native
1. Event props
<StorifyMeWidget
widgetId={WIDGET_ID}
onLoad={({ nativeEvent: { stories } }) => {
setCount(stories.length);
}}
onStoryOpened={({ nativeEvent: { story, index } }) => {
analytics.track('story_opened', { id: story?.id });
}}
onStoryFinished={({ nativeEvent: { story, index } }) => { }}
onStoryClosed={({ nativeEvent: { story } }) => { }}
onStoryShared={({ nativeEvent: { story } }) => { }}
/>
<StorifyMeWidget
widgetId={WIDGET_ID}
onLoad={({ nativeEvent: { experiences } }) => {
setCount(experiences.length);
}}
onOpened={({ nativeEvent: { experience, index } }) => {
analytics.track('experience_opened', { id: experience?.id });
}}
onFinished={({ nativeEvent: { experience, index } }) => { }}
onClosed={({ nativeEvent: { experience } }) => { }}
onShared={({ nativeEvent: { experience } }) => { }}
/>
2. Deeplink intercept
<StorifyMeWidget
widgetId={WIDGET_ID}
onStoryDeeplinkTriggered={async (event) => {
if (needsPaywall(event.nativeEvent.story)) {
showPaywall();
return StorifyMeStoryDeeplinkTriggerCompletion.IGNORE_PRESENTING_STORY;
}
return StorifyMeStoryDeeplinkTriggerCompletion.OPEN_STORY_BY_DEFAULT;
}}
/>
<StorifyMeWidget
widgetId={WIDGET_ID}
onExperienceWillOpen={async (event) => {
if (needsPaywall(event.nativeEvent.experience)) {
showPaywall();
return StorifyMeOpenAction.intercept;
}
return StorifyMeOpenAction.open;
}}
/>
3. Content type branching
onOpened={({ nativeEvent: { experience } }) => {
switch (experience.type) {
case StorifyMeExperienceType.short:
case StorifyMeExperienceType.snap:
analytics.track('short_opened');
break;
case StorifyMeExperienceType.story:
analytics.track('story_opened');
break;
}
}}
Full rename reference
| 2.x prop / method | 3.x |
|---|---|
onLoad → { stories: any } | onLoad → { experiences: StorifyMeExperience[] } |
onStoryOpened | onOpened |
onStoryFinished | onFinished |
onStoryClosed | onClosed |
onStoryShared | onShared |
onStoryDeeplinkTriggered | onExperienceWillOpen |
StorifyMe.respondToStoryDeeplink() | StorifyMe.respondToExperienceWillOpen() |
StorifyMeStoryDeeplinkTriggerCompletion.OPEN_STORY_BY_DEFAULT | StorifyMeOpenAction.open |
StorifyMeStoryDeeplinkTriggerCompletion.IGNORE_PRESENTING_STORY | StorifyMeOpenAction.intercept |
Additional 3.0.0 improvements
React Native — stronger types
StorifyMeError is now fully typed. type and objectType are enums instead of strings:
// 3.0.0
import { StorifyMeErrorType, StorifyMeObjectType } from 'react-native-storifyme';
onFail={({ nativeEvent: { error } }) => {
if (error.type === StorifyMeErrorType.networkError) {
showRetryDialog();
}
}}
StorifyMeExperience.id and widgetId changed from number to string — they are 64-bit integers on Android that exceed JS safe integer range. Update any code that calls parseInt() or compares them as numbers.
StorifyMeWidgetConfig now accepts direction and contentType:
config={{ contentType: StorifyMeWidgetContentType.liked }}
Flutter — StorifyMeStory deprecated
StorifyMeStory is deprecated in 3.0.0 and removed in 4.0.0. Replace with StorifyMeExperience everywhere.
Kotlin Multiplatform (KMP)
Upgrading from 0.0.x to 0.1.0
Storyrenamed toStorifyMeExperience.Storyis kept as a deprecated typealias — existing code compiles with a deprecation warning.StorifyMeEventListenercallbacks renamed (same pattern as all platforms):
| 0.0.x | 0.1.0 |
|---|---|
onLoad(widgetId, stories) | onLoad(widgetId, experiences) |
onStoryOpened(story, index) | onOpened(experience, index) |
onStoryFinished(story, index) | onFinished(experience, index) |
onStoryClosed(story) | onClosed(experience) |
onStoryShared(story) | onShared(experience) |
onStoryDeeplinkTriggered(story, completion) | onExperienceWillOpen(experience, completion) |
StorifyMeAdEventListener.onAdDisplayed— parameter renamed fromstorytoexperience.StoryDeeplinkTriggerCompletiondeprecated — useStorifyMeOpenAction(OPEN / INTERCEPT).StorifyMeWidgetConfiggainssetDirection(),setContentType(),addDynamicData().
Deprecation timeline
| Phase | When | What happens |
|---|---|---|
| 3.x | Current | Deprecated APIs remain available on some platforms. Their compatibility behavior differs by platform; follow the migration steps for your SDK. |
| 4.0.0 | Future | iOS APIs marked for removal in 4.0.0 are scheduled to be removed. Check the release notes for the platform-specific migration details. |
If you have questions or run into issues migrating, open an issue on the relevant SDK repository or contact support.