Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .github/actions/create-demo-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ inputs:
onesignal-api-key:
description: "OneSignal REST API key for the demo app"
required: true
e2e-mode:
description: "Whether to enable E2E_MODE in the demo app"
required: false
default: "true"
runs:
using: "composite"
steps:
Expand All @@ -21,5 +17,4 @@ runs:
{
echo "ONESIGNAL_APP_ID=${{ inputs.onesignal-app-id }}"
echo "ONESIGNAL_API_KEY=${{ inputs.onesignal-api-key }}"
echo "E2E_MODE=${{ inputs.e2e-mode }}"
} > .env
12 changes: 11 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,17 @@ jobs:
flutter build ipa --release \
--export-options-plist=ios/ExportOptions.plist \
--split-debug-info=build/symbols
cp build/ios/ipa/*.ipa build/ios/ipa/Runner.ipa
shopt -s nullglob
ipas=(build/ios/ipa/*.ipa)
if [ ${#ipas[@]} -eq 0 ]; then
echo "::error::No IPA was produced in build/ios/ipa"
exit 1
fi
src="${ipas[0]}"
dst="build/ios/ipa/Runner.ipa"
if [ "$src" != "$dst" ]; then
cp "$src" "$dst"
fi

- name: Verify aps-environment in IPA
working-directory: examples/demo
Expand Down
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
group 'com.onesignal.flutter'
version '5.5.4'
version '5.5.5'

buildscript {
repositories {
Expand Down Expand Up @@ -38,5 +38,5 @@ android {
}

dependencies {
implementation 'com.onesignal:OneSignal:5.8.1'
implementation 'com.onesignal:OneSignal:5.9.2'
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ private void init(Context context, BinaryMessenger messenger) {
this.messenger = messenger;
OneSignalWrapper.setSdkType("flutter");
// Keep in sync with pubspec.yaml version
OneSignalWrapper.setSdkVersion("050504");
OneSignalWrapper.setSdkVersion("050505");

channel = new MethodChannel(messenger, "OneSignal");
channel.setMethodCallHandler(this);
Expand Down
138 changes: 83 additions & 55 deletions examples/demo/lib/services/onesignal_api_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,71 +35,99 @@ class OneSignalApiService {
NotificationType type,
String subscriptionId,
) async {
try {
final body = <String, dynamic>{
'app_id': _appId,
'include_subscription_ids': [subscriptionId],
'headings': {'en': type.title},
'contents': {'en': type.body},
};
if (type.bigPicture != null) {
body['big_picture'] = type.bigPicture;
}
if (type.iosAttachments != null) {
body['ios_attachments'] = type.iosAttachments;
}
if (type.iosSound != null) {
body['ios_sound'] = type.iosSound;
}
if (type.useAndroidChannel) {
body['android_channel_id'] = _resolveAndroidChannelId();
}

final response = await http.post(
Uri.parse('https://onesignal.com/api/v1/notifications'),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/vnd.onesignal.v1+json',
},
body: jsonEncode(body),
);

debugPrint('Send notification response: ${response.statusCode}');
return response.statusCode == 200;
} catch (e) {
debugPrint('Send notification error: $e');
return false;
final body = <String, dynamic>{
'app_id': _appId,
'include_subscription_ids': [subscriptionId],
'headings': {'en': type.title},
'contents': {'en': type.body},
};
if (type.bigPicture != null) {
body['big_picture'] = type.bigPicture;
}
if (type.iosAttachments != null) {
body['ios_attachments'] = type.iosAttachments;
}
if (type.iosSound != null) {
body['ios_sound'] = type.iosSound;
}
if (type.useAndroidChannel) {
body['android_channel_id'] = _resolveAndroidChannelId();
}

return _postNotification(body);
}

Future<bool> sendCustomNotification(
String title,
String body,
String subscriptionId,
) async {
try {
final payload = <String, dynamic>{
'app_id': _appId,
'include_subscription_ids': [subscriptionId],
'headings': {'en': title},
'contents': {'en': body},
};

final response = await http.post(
Uri.parse('https://onesignal.com/api/v1/notifications'),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/vnd.onesignal.v1+json',
},
body: jsonEncode(payload),
);
final payload = <String, dynamic>{
'app_id': _appId,
'include_subscription_ids': [subscriptionId],
'headings': {'en': title},
'contents': {'en': body},
};

return _postNotification(payload);
}

debugPrint('Send custom notification response: ${response.statusCode}');
return response.statusCode == 200;
} catch (e) {
debugPrint('Send custom notification error: $e');
return false;
Future<bool> _postNotification(Map<String, dynamic> payload) async {
const maxAttempts = 5;
int backoffMs(int n) => 2000 * (1 << (n - 1));

// Retry while the OneSignal backend hasn't yet indexed the freshly
// created subscription. The /notifications endpoint reports this race in a
// few different shapes, all of which return HTTP 200:
// - {"errors":{"invalid_player_ids":[...]}}
// - {"id":"","errors":["All included players are not subscribed"]}
// - {"id":"","errors":[...]}
// Treat any 200 response without a real notification id as transient.
for (var attempt = 1; attempt <= maxAttempts; attempt++) {
try {
final response = await http.post(
Uri.parse('https://onesignal.com/api/v1/notifications'),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/vnd.onesignal.v1+json',
},
body: jsonEncode(payload),
);

if (response.statusCode < 200 || response.statusCode >= 300) {
debugPrint('Send notification failed: ${response.body}');
return false;
}

final decoded = jsonDecode(response.body);
if (_isTransientSendFailure(decoded)) {
if (attempt < maxAttempts) {
await Future<void>.delayed(Duration(milliseconds: backoffMs(attempt)));
continue;
}
debugPrint('Send notification failed: ${response.body}');
return false;
}

return true;
} catch (e) {
debugPrint('Send notification error: $e');
return false;
}
}

return false;
}

bool _isTransientSendFailure(dynamic decoded) {
if (decoded is! Map<String, dynamic>) return false;
final id = decoded['id'];
final errors = decoded['errors'];
final hasErrors =
(errors is List && errors.isNotEmpty) ||
(errors is Map && errors.isNotEmpty);
final missingId = id is! String || id.isEmpty;
return hasErrors || missingId;
}

Future<bool> updateLiveActivity(
Expand Down
14 changes: 0 additions & 14 deletions examples/demo/lib/utils/mask_value.dart

This file was deleted.

16 changes: 14 additions & 2 deletions examples/demo/lib/viewmodels/app_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,16 @@ class AppViewModel extends ChangeNotifier {

bool get isLoggedIn => _externalUserId != null;

String? _oneSignalId;
String? get oneSignalId => _oneSignalId;

// Push state
String? _pushSubscriptionId;
String? get pushSubscriptionId => _pushSubscriptionId;
// The native bridge can hand back an empty string before the subscription
// id is provisioned. Treat that as "no id yet" so the UI's `?? '—'`
// fallback renders the placeholder instead of an empty cell.
String? get pushSubscriptionId =>
(_pushSubscriptionId?.isEmpty ?? true) ? null : _pushSubscriptionId;

bool _pushEnabled = false;
bool get pushEnabled => _pushEnabled;
Expand Down Expand Up @@ -139,9 +146,11 @@ class AppViewModel extends ChangeNotifier {
_pushEnabled = OneSignal.User.pushSubscription.optedIn ?? false;
_hasNotificationPermission = OneSignal.Notifications.permission;

final onesignalId = await OneSignal.User.getOnesignalId();
_oneSignalId = onesignalId;

notifyListeners();

final onesignalId = await OneSignal.User.getOnesignalId();
if (onesignalId == null) return;

// fetchUserDataFromApi owns _isLoading + notifyListeners.
Expand Down Expand Up @@ -171,6 +180,9 @@ class AppViewModel extends ChangeNotifier {
'User changed: onesignalId=${nextOnesignalId ?? 'null'}, externalId=${state.current.externalId ?? 'null'}',
);

_oneSignalId = nextOnesignalId;
notifyListeners();

// Drive the post-login fetch from the observer so it runs only once the
// SDK has actually assigned a new onesignalId. Logout clears it to null;
// skip the fetch in that case (logoutUser already clears local lists).
Expand Down
3 changes: 1 addition & 2 deletions examples/demo/lib/widgets/sections/app_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';

import '../../theme.dart';
import '../../utils/mask_value.dart';
import '../../viewmodels/app_viewmodel.dart';
import '../section_card.dart';
import '../toggle_row.dart';
Expand Down Expand Up @@ -38,7 +37,7 @@ class AppSection extends StatelessWidget {
identifier: 'app_id_value',
container: true,
child: SelectableText(
maskValue(vm.appId),
vm.appId,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
),
Expand Down
3 changes: 1 addition & 2 deletions examples/demo/lib/widgets/sections/push_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../../theme.dart';
import '../../utils/mask_value.dart';
import '../../viewmodels/app_viewmodel.dart';
import '../action_button.dart';
import '../section_card.dart';
Expand Down Expand Up @@ -41,7 +40,7 @@ class PushSection extends StatelessWidget {
identifier: 'push_id_value',
container: true,
child: SelectableText(
maskValue(vm.pushSubscriptionId ?? 'N/A'),
vm.pushSubscriptionId ?? '—',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
),
Expand Down
2 changes: 1 addition & 1 deletion examples/demo/lib/widgets/sections/user_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class UserSection extends StatelessWidget {
identifier: 'user_external_id_value',
container: true,
child: SelectableText(
vm.isLoggedIn ? (vm.externalUserId ?? '') : '',
vm.isLoggedIn ? (vm.externalUserId ?? '') : '',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
),
Expand Down
Loading
Loading