Reputation: 11
I have a developed Flutter app that uses the sfmc_plugin for push notifications and the app_links package for deep linking.
The Problem: The app was previously working correctly, with push notifications opening the app and navigating to the intended screens via deep links. However, recently, tapping on a push notification no longer opens the app.
After a few notification sending and tapping, it directs me to the app browser.
App Manifest (Changed the package name and android:host to my.app but the I can assure you that it is set to the proper url):
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.app.my">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:label="@string/app_name"
android:icon="@mipmap/launcher_icon"
android:allowBackup="false"
android:fullBackupContent="false">
<!-- <meta-data-->
<!-- android:name="com.onesignal.messaging.default_notification_icon"-->
<!-- android:resource="@mipmap/ic_notification" />-->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification" />
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleInstance"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- <meta-data-->
<!-- android:name="com.onesignal.messaging.default_notification_icon"-->
<!-- android:resource="@mipmap/ic_notification" />-->
<!-- <meta-data-->
<!-- android:name="com.google.firebase.messaging.default_notification_icon"-->
<!-- android:resource="@drawable/ic_notification" />-->
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<!-- <meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/> -->
<!-- <meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/splash"
/> -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- If a user clicks on a shared link that uses the "http" scheme, your
app should be able to delegate that traffic to "https". -->
<data android:scheme="https" android:host="www.myapp.com" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="tel" />
</intent>
</queries>
</manifest>
Main Tab Manager (A wrapper for main page navigation):
class MainTabManager extends StatefulWidget {
const MainTabManager({super.key});
static const routeName = '/home-tab-management';
@override
State<MainTabManager> createState() => _MainTabManagerState();
}
ValueNotifier<String> selectedMainPage = ValueNotifier<String>('');
class _MainTabManagerState extends State<MainTabManager>
with WidgetsBindingObserver {
int activeIndex = 0;
late final UserSettingBloc userSetting;
@override
void initState() {
userSetting = BlocProvider.of<UserSettingBloc>(context);
// Call to get Currencies from API.
userSetting.add(const GetCurrenciesEvent());
final storeList = userSetting.state.storeLocation.allStores;
if (storeList.isEmpty) {
userSetting.add(SetStoreLocationList(ProductDetailRepositoryImpl()));
}
userSetting.add(const GetShippingCountriesFromAPI());
selectedMainPage.value = HomeScreen.routeName;
selectedMainPage.addListener(() {
if (mounted) {
switch (selectedMainPage.value) {
case HomeScreen.routeName:
activeIndex = 0;
break;
case SearchScreen.routeName:
activeIndex = 1;
break;
case CategoriesScreen.routeName:
activeIndex = 2;
break;
case InstoreMainScreen.routeName:
activeIndex = 3;
break;
case ProfileScreen.routeName:
activeIndex = 4;
break;
default:
}
setState(() {});
}
});
if (Platform.isAndroid) {
initDeepLinks();
}
super.initState();
WidgetsBinding.instance.addObserver(this);
}
void openAppLink(Uri uri) {
toUrlFromNotif(uri.toString());
//code if wants to push to routing
// _navigatorKey.currentState?.pushNamed(uri.fragment);
}
Future<void> toUrlFromNotif(String url) async {
String webUrl = config['WEB_URL'] ?? '';
final notifUrl = url.replaceAll(webUrl, '');
final splitUrl = notifUrl.split('?');
if (splitUrl.isNotEmpty) {
urlFromNotif.value = splitUrl.first;
if (notifUrl.contains('utm_') && splitUrl.length > 1) {
logNotifCampaignDetails(notifUrl);
}
}
}
late AppLinks _appLinks;
Future<void> initDeepLinks() async {
_appLinks = AppLinks();
final appLink = await _appLinks.getInitialLink();
if (appLink != null) {
openAppLink(appLink);
}
_appLinks.uriLinkStream.listen((uri) {
openAppLink(uri);
});
}
Future handler(MethodCall methodCall) async {
switch (methodCall.method) {
case 'handle_url':
String? url = methodCall.arguments['url'];
if (url != null) {
toUrlFromNotif(url);
}
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
var secure = SecureApplicationProvider.of(context);
switch (state) {
case AppLifecycleState.resumed:
log('app cycle resume');
secure!.unlock();
userSetting.add(const FetchAppUpdateVersion(onResume: true));
userSetting.add(const CheckAuthentication());
break;
case AppLifecycleState.inactive:
log('app cycle inactive');
secure!.lock();
break;
case AppLifecycleState.paused:
log('app cycle paused');
break;
case AppLifecycleState.detached:
log('app cycle detached');
break;
case AppLifecycleState.hidden:
log('app cycle hidden');
break;
}
}
@override
Widget build(BuildContext context) {
return PopScope(
onPopInvoked: (didPop) async {
if (subNavigator(context).canPop()) {
subNavigator(context).pop();
} else {
if (navigator(context).canPop()) {
navigator(context).pop();
}
}
},
child: CustomSafeArea(
color: Colors.white,
child: Scaffold(
bottomNavigationBar: Container(
decoration: BoxDecoration(
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(
0.0,
-10,
),
),
],
),
child: BottomAppBar(
color: Colors.white,
elevation: 0,
child: SizedBox(
height: kToolbarHeight + 10,
child: Stack(
children: [
Container(
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.only(
top: 10,
),
alignment: Alignment.topCenter,
child: AnimatedSmoothIndicator(
activeIndex: activeIndex,
count: 5,
duration: const Duration(milliseconds: 600),
effect: SlideEffect(
spacing:
(MediaQuery.of(context).size.width / 5) - 46.5,
radius: 0,
dotWidth: 40,
dotHeight: 40,
dotColor: Colors.transparent,
activeDotColor: const Color(0xFFEEEEEE),
),
),
),
Container(
margin: const EdgeInsets.only(
left: 16,
right: 16,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
NavButton(
page: HomeScreen.routeName,
icon: SvgPicture.asset(
Assets.images.icons.hNIcon,
height: 16,
width: 16,
colorFilter: ColorFilter.mode(
colorTheme(context).primary,
BlendMode.srcIn,
),
),
text: 'Home',
),
NavButton(
page: SearchScreen.routeName,
icon: SvgPicture.asset(
Assets.images.icons.searchHnSvg,
height: 16,
width: 16,
colorFilter: ColorFilter.mode(
colorTheme(context).primary,
BlendMode.srcIn,
),
),
text: 'Search',
),
NavButton(
page: CategoriesScreen.routeName,
icon: SvgPicture.asset(
Assets.images.icons.burgerHn,
height: 16,
width: 16,
colorFilter: ColorFilter.mode(
colorTheme(context).primary,
BlendMode.srcIn,
),
),
text: 'Categories',
),
NavButton(
page: InstoreMainScreen.routeName,
icon: SvgPicture.asset(
Assets.images.icons.inStoreMode,
height: 16,
width: 16,
colorFilter: ColorFilter.mode(
colorTheme(context).primary,
BlendMode.srcIn,
),
),
text: 'In-Store',
),
NavButton(
page: ProfileScreen.routeName,
icon: SvgPicture.asset(
Assets.images.icons.userHn,
height: 16,
width: 16,
colorFilter: ColorFilter.mode(
colorTheme(context).primary,
BlendMode.srcIn,
),
),
text: 'Account',
),
],
),
),
],
),
),
),
),
// Sub Navigator for showing product details page
body: BlocListener<UserSettingBloc, UserSettingState>(
bloc: userSetting,
listenWhen: (previous, current) =>
(previous.hasUpdate != current.hasUpdate &&
current.hasUpdate) ||
current.isErrorCustomerInfo != null,
listener: (context, state) {
if (state.hasUpdate) {
forceUpdateAlert(context);
}
if (state.isErrorCustomerInfo != null &&
!state.isLoadingCustomerInfo) {
final message = state.isErrorCustomerInfo;
if (message != null) {
showSnackbar(context: context, text: message.message);
if ((message.message ==
'Session has expired. Please sign-in again.' ||
message.message ==
'User has initiated account deletion and won\'t be allowed to sign-in during this process. For any concern, contact customer service +44 (0)20 7201 8088')) {
Navigator.of(context).pushNamedAndRemoveUntil(
SignInScreen.routeName,
(Route<dynamic> route) => false,
);
subNavigator(context).popUntil((route) => route.isFirst);
navigator(context).popUntil((route) => route.isFirst);
}
}
}
},
child: Navigator(
key: AppRouterScreens.instance().subNavigatorKey,
initialRoute: 'sub-tab-management',
onGenerateRoute: (RouteSettings settings) => buildPageRoute(
context,
settings,
PageTransitionType.bottomToTop,
),
),
),
),
),
);
}
}
Additional Information:
Flutter Doctor
Flutter Doctor pt.1
Flutter Doctor pt. 2
app_links: ^6.0.2
Test Devices: Android 13, Android 14
I've verified that the deep link is correctly formatted and included in the notification payload.
I've ensured that before sending push notif, I set the behavior to Open in App Page, Also double checked the link if it was correct.
I've checked the app_links package setup and ensured it's initialized correctly.
I've tested on multiple devices and the issue persists.
What I want to happen is, When the user taps on push notification it automatically opens the App.
Update: I tried sending a push notification while the app is paused in the background it seems that nothing happens when I tap the push notification. But, if I tap on the notification and open the app. It is working as expected. It directs me to the page I wanted it to go.
Upvotes: 1
Views: 42