Abdullah Khudher
Abdullah Khudher

Reputation: 309

How to use Private channel of Pusher in Flutter

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

Answers (4)

Zia
Zia

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

Sjerdo
Sjerdo

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.

  1. Uncomment the onauthorizer function in the pusher.init() in example/main.dart. (from this example project)

  2. 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

  3. You must decode the key from step 2, and return it to the onAuthorizer method you commented out in the first step.

  4. 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

Okafor Zuruoke
Okafor Zuruoke

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

doydoy
doydoy

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

Related Questions