Reputation: 309
I am using pusher_channels_flutter package to get real-time notifications.
I want to use the Private channel of Pusher in Flutter.
await pusher.subscribe(channelName: "private-chat.5");
but I got this error:
LOG: ERROR: PlatformException(error, Cannot subscribe to a private or presence channel because no Authorizer has been set. Call PusherOptions.setAuthorizer() before connecting to Pusher, null, java.lang.IllegalStateException: Cannot subscribe to a private or presence channel because no Authorizer has been set. Call PusherOptions.setAuthorizer() before connecting to Pusher
When I added onAuthorizer
function, I added it like this:
dynamic onAuthorizer(String channelName, String socketId, dynamic options) async {
return {
"auth": "foo:bar",
"channel_data": '{"user_id": 1}',
"shared_secret": "foobar"
};
}
but I got this error:
LOG: onError: Invalid key in subscription auth data: 'token' code: null exception: null
what I should put in values of onAuthorizer map
for auth
and channel_data
and shared_secret
keys?
my full code:
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:pusher_channels_flutter/pusher_channels_flutter.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
PusherChannelsFlutter pusher = PusherChannelsFlutter.getInstance();
String _log = 'output:\n';
final _apiKey = TextEditingController();
final _cluster = TextEditingController();
final _channelName = TextEditingController();
final _eventName = TextEditingController();
final _channelFormKey = GlobalKey<FormState>();
final _eventFormKey = GlobalKey<FormState>();
final _listViewController = ScrollController();
final _data = TextEditingController();
void log(String text) {
print("LOG: $text");
setState(() {
_log += text + "\n";
Timer(
const Duration(milliseconds: 100),
() => _listViewController
.jumpTo(_listViewController.position.maxScrollExtent));
});
}
@override
void initState() {
super.initState();
initPlatformState();
}
void onConnectPressed() async {
if (!_channelFormKey.currentState!.validate()) {
return;
}
// Remove keyboard
FocusScope.of(context).requestFocus(FocusNode());
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("apiKey", "mykey"); //_apiKey.text);
prefs.setString("cluster", "eu"); // _cluster.text);
prefs.setString("channelName", "private-chat.5"); //_channelName.text);
try {
await pusher.init(
apiKey: "mykey", //_apiKey.text,
cluster: "eu", //_cluster.text,
onConnectionStateChange: onConnectionStateChange,
onError: onError,
onSubscriptionSucceeded: onSubscriptionSucceeded,
onEvent: onEvent,
onSubscriptionError: onSubscriptionError,
onDecryptionFailure: onDecryptionFailure,
onMemberAdded: onMemberAdded,
onMemberRemoved: onMemberRemoved,
// authEndpoint: "<Your Authendpoint Url>",
onAuthorizer: onAuthorizer,
// authParams: {
// 'params': { 'foo': 'bar' },
// 'headers': { 'X-CSRF-Token': 'SOME_CSRF_TOKEN' }
// }
);
await pusher.subscribe(channelName: "private-chat.5"); // _channelName.text,);
await pusher.connect();
} catch (e) {
log("ERROR: $e");
}
}
dynamic onAuthorizer(String channelName, String socketId, dynamic options) async {
return {
"auth": "foo:bar",
"channel_data": '{"user_id": 1}',
"shared_secret": "foobar"
};
}
Future<void> pusherDiconnect() async {
await pusher.unsubscribe(channelName: "private-chat.5"); //_channelName.text,);
await pusher.disconnect();
print("pusherDiconnect");
}
void onConnectionStateChange(dynamic currentState, dynamic previousState) {
log("Connection: $currentState");
}
void onError(String message, int? code, dynamic e) {
log("onError: $message code: $code exception: $e");
}
void onEvent(PusherEvent event) {
log("onEvent: $event");
}
void onSubscriptionSucceeded(String channelName, dynamic data) {
log("onSubscriptionSucceeded: $channelName data: $data");
final me = pusher.getChannel(channelName)?.me;
log("Me: $me");
}
void onSubscriptionError(String message, dynamic e) {
log("onSubscriptionError: $message Exception: $e");
}
void onDecryptionFailure(String event, String reason) {
log("onDecryptionFailure: $event reason: $reason");
}
void onMemberAdded(String channelName, PusherMember member) {
log("onMemberAdded: $channelName user: $member");
}
void onMemberRemoved(String channelName, PusherMember member) {
log("onMemberRemoved: $channelName user: $member");
}
void onTriggerEventPressed() async {
var eventFormValidated = _eventFormKey.currentState!.validate();
if (!eventFormValidated) {
return;
}
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("eventName", _eventName.text);
prefs.setString("data", _data.text);
pusher.trigger(PusherEvent(
channelName: _channelName.text,
eventName: _eventName.text,
data: _data.text));
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
_apiKey.text = prefs.getString("apiKey") ?? '';
_cluster.text = prefs.getString("cluster") ?? 'eu';
_channelName.text = prefs.getString("channelName") ?? 'my-channel';
_eventName.text = prefs.getString("eventName") ?? 'client-event';
_data.text = prefs.getString("data") ?? 'test';
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text(pusher.connectionState == 'DISCONNECTED'
? 'Pusher Channels Example'
: _channelName.text),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
controller: _listViewController,
scrollDirection: Axis.vertical,
shrinkWrap: true,
children: <Widget>[
if (pusher.connectionState != 'CONNECTED')
Form(
key: _channelFormKey,
child: Column(children: <Widget>[
TextFormField(
controller: _apiKey,
validator: (String? value) {
return (value != null && value.isEmpty)
? 'Please enter your API key.'
: null;
},
decoration:
const InputDecoration(labelText: 'API Key'),
),
TextFormField(
controller: _cluster,
validator: (String? value) {
return (value != null && value.isEmpty)
? 'Please enter your cluster.'
: null;
},
decoration: const InputDecoration(
labelText: 'Cluster',
),
),
TextFormField(
controller: _channelName,
validator: (String? value) {
return (value != null && value.isEmpty)
? 'Please enter your channel name.'
: null;
},
decoration: const InputDecoration(
labelText: 'Channel',
),
),
ElevatedButton(
onPressed: onConnectPressed,
child: const Text('Connect'),
)
]))
else
Form(
key: _eventFormKey,
child: Column(children: <Widget>[
ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: pusher
.channels[_channelName.text]?.members.length,
itemBuilder: (context, index) {
final member = pusher
.channels[_channelName.text]!.members.values
.elementAt(index);
return ListTile(
title: Text(member.userInfo.toString()),
subtitle: Text(member.userId));
}),
TextFormField(
controller: _eventName,
validator: (String? value) {
return (value != null && value.isEmpty)
? 'Please enter your event name.'
: null;
},
decoration: const InputDecoration(
labelText: 'Event',
),
),
TextFormField(
controller: _data,
decoration: const InputDecoration(
labelText: 'Data',
),
),
ElevatedButton(
onPressed: onTriggerEventPressed,
child: const Text('Trigger Event'),
),
ElevatedButton(
onPressed: (){
pusherDiconnect();
},
child: const Text('pusher Diconnect'),
),
]),
),
SingleChildScrollView(
scrollDirection: Axis.vertical, child: Text(_log)),
]),
),
),
);
}
}
Upvotes: 0
Views: 5786
Reputation: 683
I had problem with subscribing to private channels using pusher_channels_flutter
I'm pasting my code here if anyone else face to same problem.
These codes work perfectly for me, please check your user token
used in onAuthorizer
Initialize pusher at initState
@override
void initState() {
super.initState();
_initPusher();
}
and at dispose
@override
void dispose() {
_pusher.disconnect();
_pusher.unsubscribe(channelName: "private-notification.${_user!.id}");
super.dispose();
}
this is the _initPusher()
function
Future<void> _initPusher() async {
_user = Provider.of<ProfileProvider>(context, listen: false).getUser;
_pusher = PusherChannelsFlutter.getInstance();
try {
await _pusher.init(
apiKey: pusherKey,
cluster: "eu",
onConnectionStateChange: onConnectionStateChange,
onError: onError,
onSubscriptionSucceeded: onSubscriptionSucceeded,
onEvent: onEvent,
onSubscriptionError: onSubscriptionError,
onDecryptionFailure: onDecryptionFailure,
onMemberAdded: onMemberAdded,
onMemberRemoved: onMemberRemoved,
//authEndpoint: "https://my-website.com/broadcasting/auth",
onAuthorizer: onAuthorizer,
);
await _pusher.subscribe(channelName: "private-notification.${_user!.id}");
await _pusher.connect();
} catch (e) {
print("error in initialization: $e");
}
}
in the onAuthorizer()
I don't know how the parameters get managed, but it works perfectly.
this is the onAuthorizer()
function
dynamic onAuthorizer(
String channelName, String socketId, dynamic options) async {
String token = await Store.read("token");
var authUrl = "$basePath/broadcasting/auth";
var result = await http.post(
Uri.parse(authUrl),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer ${token}',
},
body: 'socket_id=' + socketId + '&channel_name=' + channelName,
);
var json = jsonDecode(result.body);
return json;
}
for authUrl
my backend is Laravel
if anyone using laravel in the backend, you must have a route named broadcasting/auth
when you type php artisan route:list
. otherwise fix the problem in the back-end.
and these are the rest of the functions
void onError(String message, int? code, dynamic e) {
print("onError: $message code: $code exception: $e");
}
void onConnectionStateChange(dynamic currentState, dynamic previousState) {
print("Connection: $currentState");
}
void onMemberRemoved(String channelName, PusherMember member) {
print("onMemberRemoved: $channelName member: $member");
}
void onMemberAdded(String channelName, PusherMember member) {
print("onMemberAdded: $channelName member: $member");
}
void onSubscriptionSucceeded(String channelName, dynamic data) {
print("onSubscriptionSucceeded: $channelName data: $data");
}
void onSubscriptionError(String message, dynamic e) {
print("onSubscriptionError: $message Exception: $e");
}
void onEvent(PusherEvent event) {
print("onEvent: $event");
Provider.of<NotificationProvider>(context, listen: false)
.setNotificationCount(1);
}
void onDecryptionFailure(String event, String reason) {
print("onDecryptionFailure: $event reason: $reason");
}
I hop it solve someone's problem.
Upvotes: 8
Reputation: 11
I got stuck for a long time too, but I managed to subscribe to private-channels and trigger events to them
Here's how I did it.
Uncomment the onauthorizer function in the pusher.init() in example/main.dart. (from this example project)
Make an api call to the endpoint that returns an authorizer token. The response must be this, in json format: { "auth" : "key:key" } more info about the auth endpoint and response
You must decode the key from step 2, and return it to the onAuthorizer method you commented out in the first step.
You can now subscribe and trigger events to pusher private channels! BUT KEEP IN MIND THAT YOU HAVE TO ADD "PRIVATE-", BEFORE A CHANNELNAME, AND KEEP IN MIND THAT YOUR HAVE TO ADD "client-", BEFORE AN EVENTNAME. So the channelname "company", you want to subscribe on must be "private-company" and the event "location", will become "client-location", if you want to push the flutters apps user current location, for an example.
The method will look something like this:
dynamic onAuthorizer(String channelName, String socketId, dynamic options) {
var pusherAuthKey = http.post(
Uri.parse('YOUR-AUTHENTICATION-ENDPOINT'),
headers: {
'Authorization': '<YOUR-BEARER-TOKEN>'
},
body: 'socket_id=$socketId&channel_name=$channelName',
);
var data = null;
var authorizer = pusherAuthKey.then((pusherAuthKey) {
data = pusherAuthKey.body;
_authResponse = data;
return jsonDecode(data);
});
return authorizer;
}
Upvotes: 1
Reputation: 137
Replace the onAuthorizer callback with the following code:
getSignature(String value) {
var key = utf8.encode('<your-pusher-app-secret>');
var bytes = utf8.encode(value);
var hmacSha256 = Hmac(sha256, key); // HMAC-SHA256
var digest = hmacSha256.convert(bytes);
print("HMAC signature in string is: $digest");
return digest;
}
dynamic onAuthorizer(String channelName, String socketId, dynamic options) {
return {
"auth": "<your-pusher-key>:${getSignature("$socketId:$channelName")}",
};
}
P.S: use https://pub.dev/packages/crypto to get access to Hmac methods
See reference: https://pusher.com/docs/channels/library_auth_reference/auth-signatures/
Upvotes: 5
Reputation: 4091
The onAuthorizer function you have used is an example, you would need to replace with something that generates the necessary auth token (as described at https://pusher.com/docs/channels/library_auth_reference/auth-signatures/),
For example:
dynamic onAuthorizer(String channelName, String socketId, dynamic options) {
var response = Api().post('auth', {
"socket_id": socketId,
"channel_name": channelName,
});
var data = null;
response.then((response){
data = response.data;
return jsonDecode(data); // {"auth":"<redacted>:<redacted>"}
}); }
Upvotes: 2