whatsapp_notifier 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58d5e7ea12d1eb8560b911c6c84595f4ee1590cb64d5351ecc4d3d5e5af8245d
4
- data.tar.gz: c743fca10de3d4cd9b21789822dc8b6dff4dbfe9ffc9a5a6b119d257386c08a7
3
+ metadata.gz: '0202159fbe717a98a4bb69187abf75876258ce5f1d0bb1840deb46ed83096002'
4
+ data.tar.gz: df8857673fc57f8be6ba9681eb959d9aa842a051a522ceed530a19e57868d1b3
5
5
  SHA512:
6
- metadata.gz: 07b15329406ab0f4f64013f60108b66d40906277acb5ae9c980688e9dfb11317fb2988a2e7ca73c9945c0853bdf46071f94af8f1ba0e5cdb81bb7595dd6750e7
7
- data.tar.gz: 932f108caf6a2ee6af828a4e23d41b1bacdedc308f1d59d41ac6cdd03f9cfde1e3892f6065eaf888c80af238a73a641bc42549b5a5a4df4972511b1582063c76
6
+ metadata.gz: a9618573ca58faba4ba1b98c69626caf7627288a1b490ea102b39053166d15552d83a30e7a2583db5702111a312cd33b6bdb7dc02c011d2f355161259532cc9a
7
+ data.tar.gz: 973ff970420fc87db0280f82758f2ba51cf6cc54189d0b1ea0a2ba84f43aaa993ec4938b6a606ebae0cb14bc38e314c832b6c56fb1622712433a64fb0eaa7c33
data/README.md CHANGED
@@ -54,6 +54,20 @@ status = WhatsAppNotifier.connection_status(metadata: { user_id: current_user.id
54
54
  # => { state: "...", authenticated: true/false, has_qr: true/false }
55
55
  ```
56
56
 
57
+ ## Log out (disconnect + clear session)
58
+
59
+ Explicitly disconnects the user and wipes their saved WhatsApp session from the
60
+ service, so the next connect starts fresh with a new QR. Call this from a
61
+ user-initiated "Log out WhatsApp" action — NOT from your app's sign-out, which
62
+ should leave the WhatsApp session intact for the next login.
63
+
64
+ ```ruby
65
+ WhatsAppNotifier.logout(metadata: { user_id: current_user.id })
66
+ # => { success: true }
67
+ ```
68
+
69
+ The mounted engine also exposes `DELETE /whatsapp/logout` for the same effect.
70
+
57
71
  ## Send a message
58
72
 
59
73
  ```ruby
@@ -120,6 +134,13 @@ This copies the service to `whatsapp_service/` and updates `.gitignore`.
120
134
 
121
135
  - This gem uses WhatsApp Web automation. Use responsibly and follow WhatsApp policies.
122
136
  - Keep Chromium available in your runtime (or set `PUPPETEER_EXECUTABLE_PATH`).
137
+ - Session profiles persist under `WHATSAPP_SESSION_DIR` (default `/whatsapp_data`).
138
+ Mount it on durable storage so logins survive restarts; the service clears stale
139
+ Chromium `SingletonLock` files on each launch so an unclean exit can't wedge it.
140
+ - Resilience knobs: `WHATSAPP_INIT_TIMEOUT_MS` (default 90000) recycles a client
141
+ that never reaches QR/READY; set `WWEBJS_WEB_VERSION` (e.g. `2.3000.1023204887`,
142
+ optionally `WWEBJS_WEB_VERSION_CACHE_URL`) to pin the WhatsApp Web build so a
143
+ live web.whatsapp.com change can't silently break the client.
123
144
 
124
145
  ## License
125
146
 
@@ -22,6 +22,16 @@ module WhatsAppNotifier
22
22
  status: :internal_server_error
23
23
  end
24
24
 
25
+ # DELETE /logout — disconnect WhatsApp and clear the saved session for the
26
+ # current user. Explicit, user-initiated; app sign-out must NOT call this.
27
+ def destroy
28
+ WhatsAppNotifier.logout(metadata: metadata)
29
+ render json: { success: true }
30
+ rescue StandardError => e
31
+ render json: { error: "Failed to log out: #{e.message}" },
32
+ status: :internal_server_error
33
+ end
34
+
25
35
  private
26
36
 
27
37
  def metadata
data/config/routes.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  WhatsAppNotifier::Engine.routes.draw do
2
- get "status", to: "sessions#show", as: :status
3
- get "qr", to: "sessions#qr", as: :qr
4
- post "send", to: "messages#create", as: :send_message
2
+ get "status", to: "sessions#show", as: :status
3
+ get "qr", to: "sessions#qr", as: :qr
4
+ delete "logout", to: "sessions#destroy", as: :logout
5
+ post "send", to: "messages#create", as: :send_message
5
6
  end
@@ -27,6 +27,10 @@ module WhatsAppNotifier
27
27
  provider_for(provider || @configuration.provider).connection_status(metadata: metadata)
28
28
  end
29
29
 
30
+ def logout(metadata: {}, provider: nil)
31
+ provider_for(provider || @configuration.provider).logout(metadata: metadata)
32
+ end
33
+
30
34
  private
31
35
 
32
36
  def provider_for(key)
@@ -1,6 +1,10 @@
1
1
  module WhatsAppNotifier
2
2
  module Jobs
3
3
  if defined?(::ActiveJob::Base)
4
+ # Real async path, only live in a host app that has ActiveJob loaded (any
5
+ # Rails app). Not exercisable in the gem's unit suite — the class is chosen
6
+ # at load time and ActiveJob isn't a gem dependency.
7
+ # :nocov:
4
8
  class SendMessageJob < ::ActiveJob::Base
5
9
  queue_as :default
6
10
 
@@ -9,6 +13,7 @@ module WhatsAppNotifier
9
13
  klass.with(params).deliver_now
10
14
  end
11
15
  end
16
+ # :nocov:
12
17
  else
13
18
  class SendMessageJob
14
19
  def self.perform_later(notification_class_name, params)
@@ -48,6 +48,15 @@ module WhatsAppNotifier
48
48
  adapter.connection_status(metadata: metadata)
49
49
  end
50
50
 
51
+ def logout(metadata: {})
52
+ raise ConfigurationError, "web automation provider is disabled" unless configuration.web_automation_enabled
53
+
54
+ adapter = configuration.web_adapter
55
+ raise ConfigurationError, "web_adapter must be configured for web_automation provider" unless adapter.respond_to?(:logout)
56
+
57
+ adapter.logout(metadata: metadata)
58
+ end
59
+
51
60
 
52
61
  private
53
62
 
@@ -9,6 +9,17 @@ const port = Number(process.env.PORT || 3001);
9
9
  const SESSION_BASE_DIR = process.env.WHATSAPP_SESSION_DIR || '/whatsapp_data';
10
10
  const BROWSER_EXECUTABLE_PATH = process.env.PUPPETEER_EXECUTABLE_PATH;
11
11
 
12
+ // Recycle a client that boots Chromium but never reaches QR/READY (e.g. after a
13
+ // WhatsApp Web update breaks the injected store), instead of wedging in
14
+ // INITIALIZING forever with no QR.
15
+ const INIT_TIMEOUT_MS = Number(process.env.WHATSAPP_INIT_TIMEOUT_MS || 90000);
16
+ // Optionally pin the WhatsApp Web build so a live web.whatsapp.com change can't
17
+ // silently break the client. Set WWEBJS_WEB_VERSION to a known-good version
18
+ // (e.g. "2.3000.1023204887"); leave unset to use the library default.
19
+ const WEB_VERSION = process.env.WWEBJS_WEB_VERSION || null;
20
+ const WEB_VERSION_CACHE_URL = process.env.WWEBJS_WEB_VERSION_CACHE_URL ||
21
+ 'https://raw.githubusercontent.com/wppconnect-team/wa-version/main/html/{version}.html';
22
+
12
23
  // Multi-user client management
13
24
  interface ClientData {
14
25
  client: Client;
@@ -17,11 +28,19 @@ interface ClientData {
17
28
  lastUsed: number;
18
29
  isDestroying?: boolean;
19
30
  ready?: boolean;
31
+ initTimer?: ReturnType<typeof setTimeout>;
20
32
  }
21
33
 
22
34
  const clients = new Map<string, ClientData>();
23
35
  const initializingClients = new Set<string>();
24
36
 
37
+ function clearInitTimer(clientData: ClientData) {
38
+ if (clientData.initTimer) {
39
+ clearTimeout(clientData.initTimer);
40
+ clientData.initTimer = undefined;
41
+ }
42
+ }
43
+
25
44
  function sessionDirForUser(userId: string) {
26
45
  return join(SESSION_BASE_DIR, `session-user-${userId}`);
27
46
  }
@@ -135,7 +154,11 @@ async function getOrCreateClient(userId: string): Promise<ClientData> {
135
154
  '--disable-gpu'
136
155
  ],
137
156
  ...(BROWSER_EXECUTABLE_PATH ? { executablePath: BROWSER_EXECUTABLE_PATH } : {})
138
- }
157
+ },
158
+ ...(WEB_VERSION ? {
159
+ webVersion: WEB_VERSION,
160
+ webVersionCache: { type: 'remote' as const, remotePath: WEB_VERSION_CACHE_URL }
161
+ } : {})
139
162
  });
140
163
 
141
164
  const clientData: ClientData = {
@@ -148,8 +171,18 @@ async function getOrCreateClient(userId: string): Promise<ClientData> {
148
171
 
149
172
  clients.set(userId, clientData);
150
173
 
174
+ // Watchdog: if the client never reaches QR/READY, recycle it so the next
175
+ // poll spins a fresh attempt instead of wedging in INITIALIZING forever.
176
+ clientData.initTimer = setTimeout(() => {
177
+ if (clientData.state === 'INITIALIZING' && !clientData.qr && !clientData.ready) {
178
+ console.error(`User ${userId} stuck INITIALIZING > ${INIT_TIMEOUT_MS}ms — recycling`);
179
+ destroyClient(userId).catch(console.error);
180
+ }
181
+ }, INIT_TIMEOUT_MS);
182
+
151
183
  client.on('qr', async (qr) => {
152
184
  clientData.state = 'QR_REQUIRED';
185
+ clearInitTimer(clientData); // progress made — QR is showable
153
186
  try {
154
187
  clientData.qr = await toDataURL(qr);
155
188
  console.log(`QR RECEIVED and converted for User ${userId}`);
@@ -162,18 +195,21 @@ async function getOrCreateClient(userId: string): Promise<ClientData> {
162
195
  clientData.state = 'AUTHENTICATED';
163
196
  clientData.qr = null;
164
197
  clientData.ready = true;
198
+ clearInitTimer(clientData);
165
199
  console.log(`Client is READY for User ${userId}`);
166
200
  });
167
201
 
168
202
  client.on('authenticated', () => {
169
203
  clientData.state = 'AUTHENTICATED';
170
204
  clientData.ready = false;
205
+ clearInitTimer(clientData);
171
206
  console.log(`AUTHENTICATED for User ${userId}`);
172
207
  });
173
208
 
174
209
  client.on('auth_failure', (msg) => {
175
210
  clientData.state = 'DISCONNECTED';
176
211
  clientData.ready = false;
212
+ clearInitTimer(clientData);
177
213
  console.error(`AUTHENTICATION FAILURE for User ${userId}`, msg);
178
214
  });
179
215
 
@@ -186,6 +222,7 @@ async function getOrCreateClient(userId: string): Promise<ClientData> {
186
222
 
187
223
  client.initialize().catch(async (err) => {
188
224
  console.error(`Initialization failed for user ${userId}:`, err.message || err);
225
+ clearInitTimer(clientData);
189
226
  // Clean up the failed client
190
227
  try { client.removeAllListeners(); } catch (_) {}
191
228
  try { await client.destroy(); } catch (_) {}
@@ -203,11 +240,12 @@ async function getOrCreateClient(userId: string): Promise<ClientData> {
203
240
  return clientData;
204
241
  }
205
242
 
206
- async function destroyClient(userId: string) {
243
+ async function destroyClient(userId: string, clearSession: boolean = false) {
207
244
  const data = clients.get(userId);
208
245
  if (data && !data.isDestroying) {
209
246
  data.isDestroying = true;
210
- console.log(`Destroying WhatsApp client for User: ${userId}`);
247
+ clearInitTimer(data);
248
+ console.log(`Destroying WhatsApp client for User: ${userId} (clearSession: ${clearSession})`);
211
249
  try {
212
250
  // Unregister listeners to prevent loops or double-destroys
213
251
  data.client.removeAllListeners();
@@ -216,7 +254,18 @@ async function destroyClient(userId: string) {
216
254
  console.error(`Error destroying client for ${userId}`, e);
217
255
  }
218
256
  clients.delete(userId);
219
- // Session data is preserved on disk for auto-reconnect
257
+ // Session data is preserved on disk for auto-reconnect (unless clearing below).
258
+ }
259
+ // On explicit logout, wipe the persisted Chromium profile / WhatsApp session
260
+ // from disk — even if there was no live client in memory.
261
+ if (clearSession) {
262
+ const dir = sessionDirForUser(userId);
263
+ try {
264
+ rmSync(dir, { recursive: true, force: true });
265
+ console.log(`Cleared session dir for User: ${userId}`);
266
+ } catch (e) {
267
+ console.error(`Failed to clear session dir for ${userId}`, e);
268
+ }
220
269
  }
221
270
  }
222
271
 
@@ -250,6 +299,17 @@ app.get('/qr/:userId', async (c) => {
250
299
  return c.json({ qr: data.qr });
251
300
  });
252
301
 
302
+ // Explicit per-user logout: disconnect the client and wipe the saved WhatsApp
303
+ // session from disk so the next connect starts fresh with a new QR. Triggered by
304
+ // the user-settings "Log out WhatsApp" button — NOT by app sign-out.
305
+ app.post('/logout/:userId', async (c) => {
306
+ const userId = c.req.param('userId');
307
+ console.log(`Logout requested for User: ${userId}`);
308
+ await destroyClient(userId, true);
309
+ initializingClients.delete(userId);
310
+ return c.json({ success: true });
311
+ });
312
+
253
313
  app.post('/send/:userId', async (c) => {
254
314
  const userId = c.req.param('userId');
255
315
  const { to, message, mediaUrl } = await c.req.json();
@@ -20,28 +20,6 @@ module WhatsAppNotifier
20
20
  session = store.load
21
21
  store.save(session.merge(active: true, token: token, qr_code: nil))
22
22
  end
23
-
24
- private
25
-
26
- def cached_qr(session, user_id)
27
- return session[:qr_code] unless user_id
28
-
29
- session.fetch(:users, {}).fetch(user_key(user_id), {})[:qr_code]
30
- end
31
-
32
- def with_cached_qr(session, user_id, qr_code)
33
- return session.merge(qr_code: qr_code) unless user_id
34
-
35
- users = session.fetch(:users, {})
36
- key = user_key(user_id)
37
- user_session = users.fetch(key, {})
38
- users[key] = user_session.merge(qr_code: qr_code)
39
- session.merge(users: users)
40
- end
41
-
42
- def user_key(user_id)
43
- user_id.to_s.to_sym
44
- end
45
23
  end
46
24
  end
47
25
  end
@@ -1,4 +1,4 @@
1
1
  module WhatsAppNotifier
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
 
4
4
  end
@@ -52,6 +52,13 @@ module WhatsAppNotifier
52
52
  }
53
53
  end
54
54
 
55
+ # Logs the user out of WhatsApp and clears their saved session on the service.
56
+ def logout(metadata: {})
57
+ user_id = user_id_from(metadata)
58
+ response = request(:post, "/logout/#{user_id}")
59
+ { success: response.fetch("success", false) }
60
+ end
61
+
55
62
  private
56
63
 
57
64
  def user_id_from(metadata)
@@ -62,6 +62,10 @@ module WhatsAppNotifier
62
62
  client.connection_status(provider: provider, metadata: metadata)
63
63
  end
64
64
 
65
+ def logout(provider: nil, metadata: {})
66
+ client.logout(provider: provider, metadata: metadata)
67
+ end
68
+
65
69
  end
66
70
  end
67
71
 
data/spec/client_spec.rb CHANGED
@@ -49,4 +49,21 @@ RSpec.describe WhatsAppNotifier::Client do
49
49
  expect(status).to include(state: "QR_REQUIRED")
50
50
  end
51
51
  end
52
+
53
+ it "delegates logout to the provider" do
54
+ Dir.mktmpdir do |dir|
55
+ config.provider = :web_automation
56
+ config.web_automation_enabled = true
57
+ config.web_session_path = File.join(dir, "session.json")
58
+ config.web_adapter = double(
59
+ send_message: { success: true, session: {} },
60
+ fetch_qr_code: "qr",
61
+ connection_status: { state: "AUTHENTICATED", authenticated: true },
62
+ logout: { success: true }
63
+ )
64
+ client = described_class.new(configuration: config)
65
+
66
+ expect(client.logout(provider: :web_automation, metadata: { user_id: 1 })).to eq(success: true)
67
+ end
68
+ end
52
69
  end
@@ -24,13 +24,14 @@ RSpec.describe WhatsAppNotifier::Jobs::SendMessageJob do
24
24
  expect(result).to be_success
25
25
  end
26
26
 
27
- it "raises when perform_later has no active job base" do
28
- hide_const("ActiveJob")
29
- expect { described_class.perform_later("JobNotification", to: "+1") }.to raise_error(LoadError)
27
+ it "falls back to synchronous delivery when ActiveJob is unavailable" do
28
+ # ActiveJob is not a gem dependency, so the sync-fallback class is the one
29
+ # that loads; perform_later runs perform_now rather than raising.
30
+ result = described_class.perform_later("JobNotification", to: "+1")
31
+ expect(result).to be_success
30
32
  end
31
33
 
32
- it "allows perform_later when active job base is present" do
33
- stub_const("ActiveJob::Base", Class.new)
34
+ it "allows perform_later without raising" do
34
35
  expect { described_class.perform_later("JobNotification", to: "+1") }.not_to raise_error
35
36
  end
36
37
  end
@@ -30,14 +30,19 @@ RSpec.describe WhatsAppNotifier::Notification do
30
30
  expect(result).to be_success
31
31
  end
32
32
 
33
- it "supports deliver_later when active job exists" do
34
- stub_const("ActiveJob::Base", Class.new)
33
+ it "supports deliver_later" do
35
34
  expect { TestNotification.deliver_later(params: { name: "Neha" }) }.not_to raise_error
36
35
  end
37
36
 
38
- it "raises for deliver_later without active job" do
39
- hide_const("ActiveJob")
40
- expect { TestNotification.deliver_later(params: { name: "Neha" }) }.to raise_error(LoadError)
37
+ it "falls back to synchronous delivery for deliver_later without active job" do
38
+ # No ActiveJob dependency → deliver_later runs the job synchronously.
39
+ result = TestNotification.deliver_later(params: { name: "Neha" })
40
+ expect(result).to be_success
41
+ end
42
+
43
+ it "supports instance-level deliver_later" do
44
+ result = TestNotification.with(params: { name: "Neha" }).deliver_later
45
+ expect(result).to be_success
41
46
  end
42
47
 
43
48
  it "raises when recipient is missing" do
@@ -106,4 +106,34 @@ RSpec.describe WhatsAppNotifier::Providers::WebAutomation do
106
106
  expect(adapter).to have_received(:send_message).with(payload: hash_including(metadata: { user_id: "user-b" }), session: {}).once
107
107
  end
108
108
  end
109
+
110
+ it "logs out via adapter when enabled" do
111
+ Dir.mktmpdir do |dir|
112
+ adapter = double(fetch_qr_code: "qr", connection_status: {}, logout: { success: true })
113
+ config = build_config(path: File.join(dir, "session.json"), adapter: adapter)
114
+ provider = described_class.new(configuration: config)
115
+
116
+ expect(provider.logout(metadata: { user_id: 1 })).to eq(success: true)
117
+ end
118
+ end
119
+
120
+ it "raises on logout when provider disabled" do
121
+ Dir.mktmpdir do |dir|
122
+ adapter = double(fetch_qr_code: "qr", connection_status: {}, logout: { success: true })
123
+ config = build_config(path: File.join(dir, "session.json"), adapter: adapter, enabled: false)
124
+ provider = described_class.new(configuration: config)
125
+
126
+ expect { provider.logout }.to raise_error(WhatsAppNotifier::ConfigurationError, /disabled/)
127
+ end
128
+ end
129
+
130
+ it "raises for missing adapter logout method" do
131
+ Dir.mktmpdir do |dir|
132
+ adapter = Object.new
133
+ config = build_config(path: File.join(dir, "session.json"), adapter: adapter)
134
+ provider = described_class.new(configuration: config)
135
+
136
+ expect { provider.logout }.to raise_error(WhatsAppNotifier::ConfigurationError, /web_adapter/)
137
+ end
138
+ end
109
139
  end
@@ -12,7 +12,7 @@ RSpec.describe WhatsAppNotifier::Session::QrService do
12
12
  end
13
13
  end
14
14
 
15
- it "caches qr code per user_id when metadata is provided" do
15
+ it "fetches a fresh qr on every call (codes expire, so no caching) and passes metadata" do
16
16
  Dir.mktmpdir do |dir|
17
17
  store = WhatsAppNotifier::Session::Store.new(path: File.join(dir, "s.json"))
18
18
  adapter = double
@@ -20,10 +20,9 @@ RSpec.describe WhatsAppNotifier::Session::QrService do
20
20
  service = described_class.new(store: store, adapter: adapter)
21
21
 
22
22
  expect(service.qr_code(metadata: { user_id: 1 })).to eq("qr-user-1")
23
- expect(service.qr_code(metadata: { user_id: 1 })).to eq("qr-user-1")
24
- expect(service.qr_code(metadata: { user_id: 2 })).to eq("qr-user-2")
23
+ expect(service.qr_code(metadata: { user_id: 1 })).to eq("qr-user-2")
25
24
 
26
- expect(adapter).to have_received(:fetch_qr_code).twice
25
+ expect(adapter).to have_received(:fetch_qr_code).with(metadata: { user_id: 1 }).twice
27
26
  end
28
27
  end
29
28
 
@@ -52,4 +52,26 @@ RSpec.describe WhatsAppNotifier::WebAdapter do
52
52
  expect(adapter.fetch_qr_code(metadata: {})).to be_nil
53
53
  expect { adapter.connection_status(metadata: {}) }.to raise_error(/raw-error/)
54
54
  end
55
+
56
+ it "logs out via the service" do
57
+ allow(Net::HTTP).to receive(:start).and_return(http_success(body: { "success" => true }))
58
+
59
+ expect(adapter.logout(metadata: { user_id: "u-1" })).to eq(success: true)
60
+ end
61
+
62
+ it "defaults logout success to false when the service omits it" do
63
+ allow(Net::HTTP).to receive(:start).and_return(http_success(body: {}))
64
+
65
+ expect(adapter.logout(metadata: {})).to eq(success: false)
66
+ end
67
+
68
+ it "executes the request inside the Net::HTTP block" do
69
+ fake_http = instance_double(Net::HTTP)
70
+ allow(fake_http).to receive(:request).and_return(http_success(body: { "qr" => "data:image/png;base64,x" }))
71
+ # Invoke the block so the real request path runs (other specs stub it away).
72
+ allow(Net::HTTP).to receive(:start) { |*_args, &blk| blk.call(fake_http) }
73
+
74
+ expect(adapter.fetch_qr_code(metadata: { user_id: 1 })).to eq("data:image/png;base64,x")
75
+ expect(fake_http).to have_received(:request)
76
+ end
55
77
  end
@@ -93,11 +93,13 @@ RSpec.describe WhatsAppNotifier do
93
93
  allow(fake_client).to receive(:deliver_bulk).and_return(total: 0, success: 0, failed: 0, results: [])
94
94
  allow(fake_client).to receive(:scan_qr).and_return("qr-code")
95
95
  allow(fake_client).to receive(:connection_status).and_return(state: "QR_REQUIRED")
96
+ allow(fake_client).to receive(:logout).and_return(success: true)
96
97
  described_class.instance_variable_set(:@client, fake_client)
97
98
 
98
99
  expect(described_class.deliver_bulk([], provider: :web_automation)[:total]).to eq(0)
99
100
  expect(described_class.scan_qr(provider: :web_automation, metadata: { user_id: 1 })).to eq("qr-code")
100
101
  expect(described_class.connection_status(provider: :web_automation, metadata: { user_id: 1 })).to include(state: "QR_REQUIRED")
102
+ expect(described_class.logout(provider: :web_automation, metadata: { user_id: 1 })).to eq(success: true)
101
103
  end
102
104
 
103
105
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whatsapp_notifier
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kshitiz Sinha