@omnizapi/connect 1.0.0 → 1.0.1
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.
- package/README.md +10 -8
- package/dist/cjs/index.js +16 -4
- package/dist/esm/index.mjs +16 -4
- package/dist/omnizapi.min.js +1 -1
- package/dist/types/src/core/runtime.d.ts +2 -0
- package/examples/angular.ts +56 -0
- package/examples/basic.ts +50 -0
- package/examples/error-handling.ts +74 -0
- package/examples/react.tsx +62 -0
- package/examples/vanilla.html +60 -0
- package/examples/vue.vue +59 -0
- package/examples/with-options.ts +64 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -44,13 +44,15 @@ Dispare `client.connect({ channelId })` em um clique, nunca durante renderizacao
|
|
|
44
44
|
|
|
45
45
|
| Exemplo | Uso |
|
|
46
46
|
| --- | --- |
|
|
47
|
-
| [TypeScript basico](examples/basic.ts) | Botao e mensagem de status no DOM |
|
|
48
|
-
| [HTML com CDN](examples/vanilla.html) | Pagina sem bundler |
|
|
49
|
-
| [React / Next.js](examples/react.tsx) | `'use client'`, cliente por componente via `useState` lazy |
|
|
50
|
-
| [Angular](examples/angular.ts) | Componente com guarda de navegador para SSR |
|
|
51
|
-
| [Vue](examples/vue.vue) | SFC com guarda de navegador para SSR |
|
|
52
|
-
| [Opcoes](examples/with-options.ts) | SANDBOX, URLs explicitas, logo, fluxo padrao/direto e timeout |
|
|
53
|
-
| [Erros](examples/error-handling.ts) | Mensagens para todos os motivos e codigos tipados |
|
|
47
|
+
| [TypeScript basico](https://unpkg.com/browse/@omnizapi/connect@1.0.1/examples/basic.ts) | Botao e mensagem de status no DOM |
|
|
48
|
+
| [HTML com CDN](https://unpkg.com/browse/@omnizapi/connect@1.0.1/examples/vanilla.html) | Pagina sem bundler |
|
|
49
|
+
| [React / Next.js](https://unpkg.com/browse/@omnizapi/connect@1.0.1/examples/react.tsx) | `'use client'`, cliente por componente via `useState` lazy |
|
|
50
|
+
| [Angular](https://unpkg.com/browse/@omnizapi/connect@1.0.1/examples/angular.ts) | Componente com guarda de navegador para SSR |
|
|
51
|
+
| [Vue](https://unpkg.com/browse/@omnizapi/connect@1.0.1/examples/vue.vue) | SFC com guarda de navegador para SSR |
|
|
52
|
+
| [Opcoes](https://unpkg.com/browse/@omnizapi/connect@1.0.1/examples/with-options.ts) | SANDBOX, URLs explicitas, logo, fluxo padrao/direto e timeout |
|
|
53
|
+
| [Erros](https://unpkg.com/browse/@omnizapi/connect@1.0.1/examples/error-handling.ts) | Mensagens para todos os motivos e codigos tipados |
|
|
54
|
+
|
|
55
|
+
Os links abrem o codigo-fonte dos exemplos da versao 1.0.1, incluido no pacote npm. Nao sao demos hospedadas: adapte as chaves, o canal e o backend na sua aplicacao.
|
|
54
56
|
|
|
55
57
|
Os exemplos usam `/api/connect-channel` como rota **da sua aplicacao**, nao fornecida por este pacote. Ela deve autenticar a sessao, validar o resultado e responder com HTTP 2xx somente quando a finalizacao tiver sido confirmada. Os exemplos aguardam `fetch` e verificam `response.ok` antes de exibir sucesso. Adapte a autenticacao e a protecao CSRF ao seu backend. Nao registre resultados, tokens, codigos OAuth ou erros brutos que possam conter URLs sensiveis.
|
|
56
58
|
|
|
@@ -158,7 +160,7 @@ if (window.opener) {
|
|
|
158
160
|
|
|
159
161
|
O Meta Embedded Signup **nativo** nao fornece `attemptId` nem uma referencia customizada da janela emissora (`event.source`) para correlacao. Ele usa a allowlist exata `https://www.facebook.com` / `https://web.facebook.com` e o callback `FB.login`. Nao se pode garantir o isolamento de eventos nativos tardios entre tentativas; essas verificacoes nao sao garantia absoluta de seguranca.
|
|
160
162
|
|
|
161
|
-
|
|
163
|
+
Um login nativo fica em quarentena ate a Meta responder o `FB.login` (com codigo, cancelamento ou erro) ou ate a tentativa ter sucesso. Cancelou ou fechou o dialogo? A Meta responde e a proxima tentativa nativa e liberada na hora. So um login que a Meta nunca respondeu, como um timeout com o dialogo ainda aberto, mantem novas tentativas nativas em `CONNECTION_IN_PROGRESS` ate **recarregar a pagina**. Essa restricao evita atribuir a uma nova tentativa a resposta tardia de um dialogo anterior; nao se aplica aos popups proxy.
|
|
162
164
|
|
|
163
165
|
Durante uma tentativa, apenas `__omnizapiPublicKey`, `__omnizapiCurrentChannel` e `__omnizapiGetQrCode` sao publicados temporariamente para o frontend externo. Uma funcao QR no `window` da aplicacao nao atravessa origens nem e serializada por `postMessage`: o frontend precisa de uma integracao explicitamente autorizada e compativel com a politica de mesma origem. Este repositorio nao valida por si so o fluxo Z-API real nem confirma essa integracao externa.
|
|
164
166
|
|
package/dist/cjs/index.js
CHANGED
|
@@ -402,6 +402,13 @@ var OmnizapiClientImpl = class {
|
|
|
402
402
|
let popupTimer;
|
|
403
403
|
let messageListener;
|
|
404
404
|
let nativeLoginStarted = false;
|
|
405
|
+
let callbackReceived = false;
|
|
406
|
+
const releaseNativeLogin = () => {
|
|
407
|
+
if (runtime.nativeLoginOwner === owner) {
|
|
408
|
+
runtime.nativeLoginPending = false;
|
|
409
|
+
delete runtime.nativeLoginOwner;
|
|
410
|
+
}
|
|
411
|
+
};
|
|
405
412
|
const globals = [];
|
|
406
413
|
const finish = (outcome) => {
|
|
407
414
|
if (settled) return;
|
|
@@ -434,8 +441,8 @@ var OmnizapiClientImpl = class {
|
|
|
434
441
|
}
|
|
435
442
|
}
|
|
436
443
|
if (runtime.activeConnection === owner) delete runtime.activeConnection;
|
|
437
|
-
if (nativeLoginStarted && !(outcome instanceof OmnizapiError) && outcome.success) {
|
|
438
|
-
|
|
444
|
+
if (nativeLoginStarted && (callbackReceived || !(outcome instanceof OmnizapiError) && outcome.success)) {
|
|
445
|
+
releaseNativeLogin();
|
|
439
446
|
}
|
|
440
447
|
if (outcome instanceof OmnizapiError) reject(outcome);
|
|
441
448
|
else resolve(outcome);
|
|
@@ -529,7 +536,6 @@ var OmnizapiClientImpl = class {
|
|
|
529
536
|
let wabaId = null;
|
|
530
537
|
let phoneId = null;
|
|
531
538
|
let coexistence = false;
|
|
532
|
-
let callbackReceived = false;
|
|
533
539
|
const completeNative = () => {
|
|
534
540
|
if (!isLive() || !code || !wabaId) return;
|
|
535
541
|
succeed({ code, wabaId, phoneId, coexistence });
|
|
@@ -562,11 +568,17 @@ var OmnizapiClientImpl = class {
|
|
|
562
568
|
};
|
|
563
569
|
host.addEventListener("message", messageListener);
|
|
564
570
|
runtime.nativeLoginPending = true;
|
|
571
|
+
runtime.nativeLoginOwner = owner;
|
|
565
572
|
nativeLoginStarted = true;
|
|
566
573
|
try {
|
|
567
574
|
fb.login((response) => {
|
|
568
|
-
if (
|
|
575
|
+
if (callbackReceived) return;
|
|
569
576
|
callbackReceived = true;
|
|
577
|
+
if (settled) {
|
|
578
|
+
releaseNativeLogin();
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (!isLive()) return;
|
|
570
582
|
if (!isRecord(response) || response.error) {
|
|
571
583
|
finish(new OmnizapiError("PROVIDER_ERROR", "Meta returned an invalid login response."));
|
|
572
584
|
return;
|
package/dist/esm/index.mjs
CHANGED
|
@@ -373,6 +373,13 @@ var OmnizapiClientImpl = class {
|
|
|
373
373
|
let popupTimer;
|
|
374
374
|
let messageListener;
|
|
375
375
|
let nativeLoginStarted = false;
|
|
376
|
+
let callbackReceived = false;
|
|
377
|
+
const releaseNativeLogin = () => {
|
|
378
|
+
if (runtime.nativeLoginOwner === owner) {
|
|
379
|
+
runtime.nativeLoginPending = false;
|
|
380
|
+
delete runtime.nativeLoginOwner;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
376
383
|
const globals = [];
|
|
377
384
|
const finish = (outcome) => {
|
|
378
385
|
if (settled) return;
|
|
@@ -405,8 +412,8 @@ var OmnizapiClientImpl = class {
|
|
|
405
412
|
}
|
|
406
413
|
}
|
|
407
414
|
if (runtime.activeConnection === owner) delete runtime.activeConnection;
|
|
408
|
-
if (nativeLoginStarted && !(outcome instanceof OmnizapiError) && outcome.success) {
|
|
409
|
-
|
|
415
|
+
if (nativeLoginStarted && (callbackReceived || !(outcome instanceof OmnizapiError) && outcome.success)) {
|
|
416
|
+
releaseNativeLogin();
|
|
410
417
|
}
|
|
411
418
|
if (outcome instanceof OmnizapiError) reject(outcome);
|
|
412
419
|
else resolve(outcome);
|
|
@@ -500,7 +507,6 @@ var OmnizapiClientImpl = class {
|
|
|
500
507
|
let wabaId = null;
|
|
501
508
|
let phoneId = null;
|
|
502
509
|
let coexistence = false;
|
|
503
|
-
let callbackReceived = false;
|
|
504
510
|
const completeNative = () => {
|
|
505
511
|
if (!isLive() || !code || !wabaId) return;
|
|
506
512
|
succeed({ code, wabaId, phoneId, coexistence });
|
|
@@ -533,11 +539,17 @@ var OmnizapiClientImpl = class {
|
|
|
533
539
|
};
|
|
534
540
|
host.addEventListener("message", messageListener);
|
|
535
541
|
runtime.nativeLoginPending = true;
|
|
542
|
+
runtime.nativeLoginOwner = owner;
|
|
536
543
|
nativeLoginStarted = true;
|
|
537
544
|
try {
|
|
538
545
|
fb.login((response) => {
|
|
539
|
-
if (
|
|
546
|
+
if (callbackReceived) return;
|
|
540
547
|
callbackReceived = true;
|
|
548
|
+
if (settled) {
|
|
549
|
+
releaseNativeLogin();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (!isLive()) return;
|
|
541
553
|
if (!isRecord(response) || response.error) {
|
|
542
554
|
finish(new OmnizapiError("PROVIDER_ERROR", "Meta returned an invalid login response."));
|
|
543
555
|
return;
|
package/dist/omnizapi.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";(()=>{var e=class extends Error{constructor(o,i){super(i);this.code=o;this.name="OmnizapiError"}};var re={PRODUCTION:{api:"https://api.omni.z-api.io",front:"https://app.omni.z-api.io"},STAGING:{api:"https://api.staging.omni.z-api.io",front:"https://app.staging.omni.z-api.io"},SANDBOX:{api:"",front:""}},se=({environment:n="PRODUCTION",apiUrl:t,frontUrl:o}={})=>{if(!Object.prototype.hasOwnProperty.call(re,n))throw new e("INVALID_CONFIGURATION","Unknown environment.");let i=re[n],s={api:t!=null?t:i.api,front:o!=null?o:i.front};for(let c of["api","front"]){let f;try{f=new URL(s[c])}catch(l){throw new e("INVALID_CONFIGURATION",`A valid ${c}Url is required for this environment.`)}if(!["http:","https:"].includes(f.protocol)||f.username||f.password||f.href.includes("?")||f.href.includes("#"))throw new e("INVALID_CONFIGURATION",`${c}Url must be an HTTP(S) base URL without credentials, query or fragment.`);s[c]=f.href.replace(/\/+$/,"")}return s};var ae=Symbol.for("omnizapi.connect.runtime.v1");function W(n){let t=n,o=t[ae];return o||(o={nativeLoginPending:!1,failedFacebookScripts:new WeakSet},Object.defineProperty(t,ae,{configurable:!0,value:o})),o}function ce(n){return!!n&&!("__buffer"in n)&&typeof n.init=="function"&&typeof n.login=="function"}function le(n,t){if(t.aborted)return Promise.reject(new e("SDK_LOAD_FAILED","The Meta SDK load was cancelled."));if(ce(n.FB))return Promise.resolve(n.FB);let o=W(n),i=o.facebookLoading;if(i)return i;let s=Promise.resolve().then(()=>new Promise((c,f)=>{let l=!1,r,E=!1,O,w,_=(I,a)=>{if(!l){l=!0,clearTimeout(K),t.removeEventListener("abort",U),r==null||r.removeEventListener("load",M),r==null||r.removeEventListener("error",G);try{n.fbAsyncInit===L&&(O?Object.defineProperty(n,"fbAsyncInit",O):Reflect.deleteProperty(n,"fbAsyncInit"))}catch(P){}if(a&&r&&(t.aborted||o.failedFacebookScripts.add(r),E))try{r.remove()}catch(P){}o.facebookLoading===s&&delete o.facebookLoading,I?c(I):f(a)}},M=()=>{if(l)return;let I=n.FB;ce(I)&&_(I)},G=()=>_(void 0,new e("SDK_LOAD_FAILED","The Meta SDK could not be loaded.")),U=()=>_(void 0,new e("SDK_LOAD_FAILED","The Meta SDK load was cancelled.")),L=()=>{if(!l){try{w==null||w.call(n)}catch(I){}M()}},K=setTimeout(()=>_(void 0,new e("SDK_LOAD_FAILED","The Meta SDK load timed out.")),30*1e3);try{if(t.addEventListener("abort",U,{once:!0}),t.aborted){U();return}O=Object.getOwnPropertyDescriptor(n,"fbAsyncInit"),w=typeof n.fbAsyncInit=="function"?n.fbAsyncInit:void 0,Object.defineProperty(n,"fbAsyncInit",O&&!O.configurable?{value:L}:{configurable:!0,enumerable:!0,writable:!0,value:L});let I=n.document,a=I.getElementById("facebook-jssdk"),P=(a==null?void 0:a.tagName.toLowerCase())==="script"?a:void 0;r=(P&&!o.failedFacebookScripts.has(P)?P:void 0)||Array.from(I.scripts).find(y=>{if(o.failedFacebookScripts.has(y))return!1;try{let F=new URL(y.src);return F.origin==="https://connect.facebook.net"&&/^\/[^/]+\/sdk\.js$/.test(F.pathname)}catch(F){return!1}}),r||(r=I.createElement("script"),E=!0,a||(r.id="facebook-jssdk"),r.src="https://connect.facebook.net/en_US/sdk.js",r.crossOrigin="anonymous",r.async=!0,r.defer=!0),r.addEventListener("load",M),r.addEventListener("error",G),M(),!l&&E&&I.head.appendChild(r)}catch(I){G()}}));return o.facebookLoading=s,s}var ue=(n,t,o=600,i=700)=>{let s=window,c=s.document,f=s.screenLeft!==void 0?s.screenLeft:s.screenX,l=s.screenTop!==void 0?s.screenTop:s.screenY,r=s.innerWidth?s.innerWidth:c.documentElement.clientWidth?c.documentElement.clientWidth:s.screen.width,E=s.innerHeight?s.innerHeight:c.documentElement.clientHeight?c.documentElement.clientHeight:s.screen.height,O=r/2-o/2+f,w=E/2-i/2+l;return s.open(n,t,`scrollbars=yes, width=${o}, height=${i}, top=${w}, left=${O}`)};var Ie=600*1e3,we=30*1e3;function v(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}function p(n){return typeof n=="string"&&n.trim()?n:null}function de(n){if(n==="."||n==="..")throw new e("INVALID_OPTIONS","The channelId cannot be a URL path navigation segment.");try{return encodeURIComponent(n)}catch(t){throw new e("INVALID_OPTIONS","The channelId cannot be encoded as a URL path.")}}function Re(n){return["META_MESSENGER","META_INSTAGRAM","META_WHATSAPP","BOT_TELEGRAM","Z_API_WHATSAPP"].includes(n)}function pe(n){return typeof n=="string"&&["cancelled","popup_closed","not_completed","invalid_channel","origin_not_allowed","unsupported_channel"].includes(n)}function B(n,t,o){return{success:!1,reason:o,channelId:n,type:t,code:null,wabaId:null,phoneId:null}}function fe(n,t,o){return typeof fetch!="function"||typeof AbortController!="function"?Promise.reject(new e("INVALID_CONFIGURATION","Fetch and AbortController are required.")):new Promise((i,s)=>{let c=new AbortController,f=!1,l=(w,_)=>{f||(f=!0,clearTimeout(E),o==null||o.removeEventListener("abort",r),c.abort(),_?s(_):i(w))},r=()=>l(void 0,new e("NETWORK_ERROR","The API request was cancelled.")),E=setTimeout(()=>l(void 0,new e("TIMEOUT","The API request timed out.")),we);if(o==null||o.addEventListener("abort",r,{once:!0}),o!=null&&o.aborted){r();return}(async()=>{let w;try{w=await fetch(n.href,{...t,signal:c.signal})}catch(_){l(void 0,new e("NETWORK_ERROR","The API request failed."));return}if(!f){if(!w||typeof w.ok!="boolean"){l(void 0,new e("INVALID_RESPONSE","The API response is invalid."));return}if(!w.ok){l(void 0,new e("HTTP_ERROR","The API returned an unsuccessful status."));return}try{let _=await w.json();f||l(_)}catch(_){l(void 0,new e("INVALID_RESPONSE","The API did not return valid JSON."))}}})().catch(()=>l(void 0,new e("NETWORK_ERROR","The API request failed.")))})}var j=class{constructor(t){if(!v(t)||!p(t.publicKey)||Array.from(t.publicKey).some(o=>{let i=o.charCodeAt(0);return i<32||i===127})||t.logo!==void 0&&typeof t.logo!="string"||t.apiUrl!==void 0&&!p(t.apiUrl)||t.frontUrl!==void 0&&!p(t.frontUrl))throw new e("INVALID_CONFIGURATION","Valid client properties and a publicKey are required.");this.publicKey=t.publicKey,this.logo=t.logo,this.constants=se({environment:t.environment,apiUrl:t.apiUrl,frontUrl:t.frontUrl})}async getListMessengerPages(t,o){if(!p(t)||!p(o))throw new e("INVALID_OPTIONS","A channelId and authorization code are required.");return fe(new URL(`${this.constants.api}/v1/channels/${de(t)}/list-messenger-pages`),{method:"POST",body:JSON.stringify({redirectUri:new URL(`${this.constants.front}/oauth/proxy-messenger`).href,code:o}),headers:{"Content-Type":"application/json",Authorization:this.publicKey}})}async connect(t){var o;try{if(typeof window=="undefined"||!window.document||!window.location)throw new e("BROWSER_REQUIRED","Connecting requires a browser window.");let i=window,s=W(i);if(s.activeConnection)throw new e("CONNECTION_IN_PROGRESS","A connection is already in progress in this window.");if(!v(t))throw new e("INVALID_OPTIONS","Connection options are required.");let{channelId:c,direct:f,getQRCode:l,timeoutMs:r=Ie}=t;if(typeof c!="string"||f!==void 0&&typeof f!="boolean"||l!==void 0&&typeof l!="function"||typeof r!="number"||!Number.isFinite(r)||r<=0||r>2147483647)throw new e("INVALID_OPTIONS","Invalid channelId, direct, getQRCode or timeoutMs option.");if(!c.trim())return B(c,null,"invalid_channel");let E=de(c);if(typeof i.open!="function"||typeof i.addEventListener!="function"||typeof i.removeEventListener!="function"||typeof i.location.origin!="string"||typeof i.location.href!="string"||typeof((o=i.crypto)==null?void 0:o.getRandomValues)!="function"||typeof AbortController!="function"||typeof fetch!="function")throw new e("INVALID_CONFIGURATION","Required browser APIs are unavailable.");let O=i.location.origin;if(O==="null")return B(c,null,"origin_not_allowed");let w=i.location.href,_=new URL(this.constants.front).origin;return await new Promise((M,G)=>{let U=Symbol("omnizapi-connection"),L=new AbortController,K=Date.now()+r,I=!1,a=null,P=null,y=null,F,q,z,Q=!1,J=[],S=h=>{if(I)return;if(I=!0,clearTimeout(F),clearInterval(q),z)try{i.removeEventListener("message",z)}catch(D){}try{L.abort()}catch(D){}let k=y;y=null;try{k==null||k.close()}catch(D){}for(let{name:D,previous:m,value:N}of J)try{let T=Object.getOwnPropertyDescriptor(i,D);if(!T||!("value"in T)||T.value!==N)continue;m?Object.defineProperty(i,D,m):Reflect.deleteProperty(i,D)}catch(T){}s.activeConnection===U&&delete s.activeConnection,Q&&!(h instanceof e)&&h.success&&(s.nativeLoginPending=!1),h instanceof e?G(h):M(h)},b=()=>(!I&&Date.now()>=K&&S(new e("TIMEOUT","The connection timed out.")),!I),C=h=>S(B(c,a,h)),X=h=>{!b()||!a||S({success:!0,channelId:c,type:a,code:p(h.code),wabaId:p(h.wabaId),phoneId:p(h.phoneId),coexistence:h.coexistence===!0,redirectUri:P,botToken:p(h.botToken),pageId:p(h.pageId),accessToken:p(h.accessToken)})};try{s.activeConnection=U;let h=i.crypto.getRandomValues(new Uint8Array(16)),k=Array.from(h,m=>m.toString(16).padStart(2,"0")).join("");for(let[m,N]of[["__omnizapiPublicKey",this.publicKey],["__omnizapiCurrentChannel",c],["__omnizapiGetQrCode",l]]){let T=Object.getOwnPropertyDescriptor(i,m);Object.defineProperty(i,m,T&&!T.configurable?{value:N}:{configurable:!0,enumerable:!0,writable:!0,value:N}),J.push({name:m,previous:T,value:N})}F=setTimeout(()=>S(new e("TIMEOUT","The connection timed out.")),r),(async()=>{var Z,Y,ee;let m=await fe(new URL(`${this.constants.api}/instances/${E}/sdk-info`),{headers:{Authorization:this.publicKey,"Content-Type":"application/json"}},L.signal);if(!b())return;if(!v(m)||!p(m.middleware))throw new e("INVALID_RESPONSE","The channel configuration is invalid.");let N=(Z=m.allowOrigins)!=null?Z:[];if(!Array.isArray(N)||!N.every(u=>typeof u=="string"))throw new e("INVALID_RESPONSE","The channel origins are invalid.");let T=m.middleware;if(a=Re(T)?T:null,O!==_&&!N.some(u=>O===u.replace(/\/$/,""))){C("origin_not_allowed");return}if(!a){C("unsupported_channel");return}let V=(Y=p(a==="META_INSTAGRAM"?m.appInstagramId:m.appId))!=null?Y:"",H=(ee=p(m.appConfig))!=null?ee:"";if(a.startsWith("META_")&&!V||a==="META_WHATSAPP"&&!H)throw new e("INVALID_RESPONSE","The channel provider configuration is invalid.");if(a==="META_WHATSAPP"&&(f||m.appCustom||O===_)){if(s.nativeLoginPending)throw new e("CONNECTION_IN_PROGRESS","A previous Meta login is incomplete. Reload this window before retrying a native login.");let u=await le(i,L.signal);if(!b())return;try{u.init({appId:V,version:"v25.0",autoLogAppEvents:!0,xfbml:!0})}catch(A){throw new e("PROVIDER_ERROR","The Meta SDK could not be initialized.")}if(!b())return;let d=null,x=null,ne=null,te=!1,oe=!1,ie=()=>{!b()||!d||!x||X({code:d,wabaId:x,phoneId:ne,coexistence:te})};z=A=>{if(!b()||!["https://www.facebook.com","https://web.facebook.com"].includes(A.origin))return;let R=A.data;if(typeof R=="string")try{R=JSON.parse(R)}catch(_e){return}if(!(!v(R)||R.type!=="WA_EMBEDDED_SIGNUP")){if(R.event==="CANCEL")C("cancelled");else if(R.event==="ERROR")S(new e("PROVIDER_ERROR","Meta could not complete the connection."));else if(R.event==="FINISH"||R.event==="FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING"){if(!v(R.data)||!p(R.data.waba_id)||R.data.phone_number_id!=null&&typeof R.data.phone_number_id!="string")return;x=p(R.data.waba_id),ne=p(R.data.phone_number_id),te=R.event==="FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING",ie()}}},i.addEventListener("message",z),s.nativeLoginPending=!0,Q=!0;try{u.login(A=>{if(!(!b()||oe)){if(oe=!0,!v(A)||A.error){S(new e("PROVIDER_ERROR","Meta returned an invalid login response."));return}if(A.authResponse==null){C("not_completed");return}if(!v(A.authResponse)||!p(A.authResponse.code)){C("not_completed");return}d=p(A.authResponse.code),ie()}},{config_id:H,response_type:"code",override_default_response_type:!0,extras:{setup:{},featureType:"whatsapp_business_app_onboarding",sessionInfoVersion:"3"}})}catch(A){throw new e("PROVIDER_ERROR","The Meta login could not be started.")}return}let g;if(a==="META_MESSENGER"||a==="META_INSTAGRAM"){let u=a==="META_MESSENGER";P=new URL(`${this.constants.front}/oauth/proxy-${u?"messenger":"instagram"}`).href,g=new URL(u?"https://www.facebook.com/dialog/oauth":"https://www.instagram.com/oauth/authorize"),g.searchParams.set("client_id",V),u&&g.searchParams.set("display","popup"),g.searchParams.set("redirect_uri",P),g.searchParams.set("response_type","code"),g.searchParams.set("scope",u?"public_profile,business_management,pages_messaging,pages_show_list,pages_manage_metadata,pages_read_engagement":"instagram_business_basic,instagram_business_manage_messages"),g.searchParams.set("state",JSON.stringify({currentUrl:w,attemptId:k,channelId:c}))}else a==="META_WHATSAPP"?(g=new URL(`${this.constants.front}/oauth/proxy-cloud-api`),g.searchParams.set("configId",H),g.searchParams.set("appId",V),g.searchParams.set("channelId",c),g.searchParams.set("channelType",a),g.searchParams.set("logo",this.logo||new URL(`${this.constants.front}/logos/SVG/Ativo%2016.svg`).href),g.searchParams.set("pk",this.publicKey)):(g=new URL(`${this.constants.front}/connect/${a==="BOT_TELEGRAM"?"telegram":"whatsapp-web"}`),g.searchParams.set("channelId",c));if(a!=="META_MESSENGER"&&a!=="META_INSTAGRAM"&&g.searchParams.set("attemptId",k),z=u=>{if(!b()||!y||u.origin!==_||u.source!==y)return;let d=u.data;!v(d)||d.type!=="after-connect"||d.attemptId!==k||d.channelId!==c||typeof d.success!="boolean"||d.reason!==void 0&&!pe(d.reason)||d.coexistence!==void 0&&typeof d.coexistence!="boolean"||["code","wabaId","phoneId","botToken","pageId","accessToken","redirectUri"].some(x=>d[x]!=null&&typeof d[x]!="string")||(d.success?a!=null&&a.startsWith("META_")&&!p(d.code)||a==="BOT_TELEGRAM"&&!p(d.botToken)?C("not_completed"):X(d):C(pe(d.reason)?d.reason:"not_completed"))},i.addEventListener("message",z),!!b()){try{let u=ue(g.href,"_blank",600,740);if(!u||u===i)throw new e("POPUP_BLOCKED","The connection popup was blocked.");y=u}catch(u){throw new e("POPUP_BLOCKED","The connection popup could not be opened.")}q=setInterval(()=>{if(b())try{y!=null&&y.closed&&C("popup_closed")}catch(u){S(new e("PROVIDER_ERROR","The connection popup could not be monitored."))}},500)}})().catch(m=>{b()&&S(m instanceof e?m:new e("PROVIDER_ERROR","The connection could not be completed."))})}catch(h){S(new e("INVALID_CONFIGURATION","The connection could not be initialized."))}})}catch(i){throw i instanceof e?i:new e("INVALID_OPTIONS","The connection could not be started with these options.")}}},me=j;var $=class{static newClient(t){return new me(t)}};var he=$;Object.assign(window,{Omnizapi:he});})();
|
|
1
|
+
"use strict";(()=>{var e=class extends Error{constructor(o,i){super(i);this.code=o;this.name="OmnizapiError"}};var se={PRODUCTION:{api:"https://api.omni.z-api.io",front:"https://app.omni.z-api.io"},STAGING:{api:"https://api.staging.omni.z-api.io",front:"https://app.staging.omni.z-api.io"},SANDBOX:{api:"",front:""}},ae=({environment:n="PRODUCTION",apiUrl:t,frontUrl:o}={})=>{if(!Object.prototype.hasOwnProperty.call(se,n))throw new e("INVALID_CONFIGURATION","Unknown environment.");let i=se[n],r={api:t!=null?t:i.api,front:o!=null?o:i.front};for(let c of["api","front"]){let f;try{f=new URL(r[c])}catch(l){throw new e("INVALID_CONFIGURATION",`A valid ${c}Url is required for this environment.`)}if(!["http:","https:"].includes(f.protocol)||f.username||f.password||f.href.includes("?")||f.href.includes("#"))throw new e("INVALID_CONFIGURATION",`${c}Url must be an HTTP(S) base URL without credentials, query or fragment.`);r[c]=f.href.replace(/\/+$/,"")}return r};var ce=Symbol.for("omnizapi.connect.runtime.v1");function W(n){let t=n,o=t[ce];return o||(o={nativeLoginPending:!1,failedFacebookScripts:new WeakSet},Object.defineProperty(t,ce,{configurable:!0,value:o})),o}function le(n){return!!n&&!("__buffer"in n)&&typeof n.init=="function"&&typeof n.login=="function"}function ue(n,t){if(t.aborted)return Promise.reject(new e("SDK_LOAD_FAILED","The Meta SDK load was cancelled."));if(le(n.FB))return Promise.resolve(n.FB);let o=W(n),i=o.facebookLoading;if(i)return i;let r=Promise.resolve().then(()=>new Promise((c,f)=>{let l=!1,s,E=!1,_,I,O=(h,a)=>{if(!l){l=!0,clearTimeout(K),t.removeEventListener("abort",N),s==null||s.removeEventListener("load",U),s==null||s.removeEventListener("error",G);try{n.fbAsyncInit===k&&(_?Object.defineProperty(n,"fbAsyncInit",_):Reflect.deleteProperty(n,"fbAsyncInit"))}catch(P){}if(a&&s&&(t.aborted||o.failedFacebookScripts.add(s),E))try{s.remove()}catch(P){}o.facebookLoading===r&&delete o.facebookLoading,h?c(h):f(a)}},U=()=>{if(l)return;let h=n.FB;le(h)&&O(h)},G=()=>O(void 0,new e("SDK_LOAD_FAILED","The Meta SDK could not be loaded.")),N=()=>O(void 0,new e("SDK_LOAD_FAILED","The Meta SDK load was cancelled.")),k=()=>{if(!l){try{I==null||I.call(n)}catch(h){}U()}},K=setTimeout(()=>O(void 0,new e("SDK_LOAD_FAILED","The Meta SDK load timed out.")),30*1e3);try{if(t.addEventListener("abort",N,{once:!0}),t.aborted){N();return}_=Object.getOwnPropertyDescriptor(n,"fbAsyncInit"),I=typeof n.fbAsyncInit=="function"?n.fbAsyncInit:void 0,Object.defineProperty(n,"fbAsyncInit",_&&!_.configurable?{value:k}:{configurable:!0,enumerable:!0,writable:!0,value:k});let h=n.document,a=h.getElementById("facebook-jssdk"),P=(a==null?void 0:a.tagName.toLowerCase())==="script"?a:void 0;s=(P&&!o.failedFacebookScripts.has(P)?P:void 0)||Array.from(h.scripts).find(y=>{if(o.failedFacebookScripts.has(y))return!1;try{let F=new URL(y.src);return F.origin==="https://connect.facebook.net"&&/^\/[^/]+\/sdk\.js$/.test(F.pathname)}catch(F){return!1}}),s||(s=h.createElement("script"),E=!0,a||(s.id="facebook-jssdk"),s.src="https://connect.facebook.net/en_US/sdk.js",s.crossOrigin="anonymous",s.async=!0,s.defer=!0),s.addEventListener("load",U),s.addEventListener("error",G),U(),!l&&E&&h.head.appendChild(s)}catch(h){G()}}));return o.facebookLoading=r,r}var de=(n,t,o=600,i=700)=>{let r=window,c=r.document,f=r.screenLeft!==void 0?r.screenLeft:r.screenX,l=r.screenTop!==void 0?r.screenTop:r.screenY,s=r.innerWidth?r.innerWidth:c.documentElement.clientWidth?c.documentElement.clientWidth:r.screen.width,E=r.innerHeight?r.innerHeight:c.documentElement.clientHeight?c.documentElement.clientHeight:r.screen.height,_=s/2-o/2+f,I=E/2-i/2+l;return r.open(n,t,`scrollbars=yes, width=${o}, height=${i}, top=${I}, left=${_}`)};var Ie=600*1e3,Re=30*1e3;function L(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}function p(n){return typeof n=="string"&&n.trim()?n:null}function pe(n){if(n==="."||n==="..")throw new e("INVALID_OPTIONS","The channelId cannot be a URL path navigation segment.");try{return encodeURIComponent(n)}catch(t){throw new e("INVALID_OPTIONS","The channelId cannot be encoded as a URL path.")}}function Oe(n){return["META_MESSENGER","META_INSTAGRAM","META_WHATSAPP","BOT_TELEGRAM","Z_API_WHATSAPP"].includes(n)}function fe(n){return typeof n=="string"&&["cancelled","popup_closed","not_completed","invalid_channel","origin_not_allowed","unsupported_channel"].includes(n)}function j(n,t,o){return{success:!1,reason:o,channelId:n,type:t,code:null,wabaId:null,phoneId:null}}function me(n,t,o){return typeof fetch!="function"||typeof AbortController!="function"?Promise.reject(new e("INVALID_CONFIGURATION","Fetch and AbortController are required.")):new Promise((i,r)=>{let c=new AbortController,f=!1,l=(I,O)=>{f||(f=!0,clearTimeout(E),o==null||o.removeEventListener("abort",s),c.abort(),O?r(O):i(I))},s=()=>l(void 0,new e("NETWORK_ERROR","The API request was cancelled.")),E=setTimeout(()=>l(void 0,new e("TIMEOUT","The API request timed out.")),Re);if(o==null||o.addEventListener("abort",s,{once:!0}),o!=null&&o.aborted){s();return}(async()=>{let I;try{I=await fetch(n.href,{...t,signal:c.signal})}catch(O){l(void 0,new e("NETWORK_ERROR","The API request failed."));return}if(!f){if(!I||typeof I.ok!="boolean"){l(void 0,new e("INVALID_RESPONSE","The API response is invalid."));return}if(!I.ok){l(void 0,new e("HTTP_ERROR","The API returned an unsuccessful status."));return}try{let O=await I.json();f||l(O)}catch(O){l(void 0,new e("INVALID_RESPONSE","The API did not return valid JSON."))}}})().catch(()=>l(void 0,new e("NETWORK_ERROR","The API request failed.")))})}var $=class{constructor(t){if(!L(t)||!p(t.publicKey)||Array.from(t.publicKey).some(o=>{let i=o.charCodeAt(0);return i<32||i===127})||t.logo!==void 0&&typeof t.logo!="string"||t.apiUrl!==void 0&&!p(t.apiUrl)||t.frontUrl!==void 0&&!p(t.frontUrl))throw new e("INVALID_CONFIGURATION","Valid client properties and a publicKey are required.");this.publicKey=t.publicKey,this.logo=t.logo,this.constants=ae({environment:t.environment,apiUrl:t.apiUrl,frontUrl:t.frontUrl})}async getListMessengerPages(t,o){if(!p(t)||!p(o))throw new e("INVALID_OPTIONS","A channelId and authorization code are required.");return me(new URL(`${this.constants.api}/v1/channels/${pe(t)}/list-messenger-pages`),{method:"POST",body:JSON.stringify({redirectUri:new URL(`${this.constants.front}/oauth/proxy-messenger`).href,code:o}),headers:{"Content-Type":"application/json",Authorization:this.publicKey}})}async connect(t){var o;try{if(typeof window=="undefined"||!window.document||!window.location)throw new e("BROWSER_REQUIRED","Connecting requires a browser window.");let i=window,r=W(i);if(r.activeConnection)throw new e("CONNECTION_IN_PROGRESS","A connection is already in progress in this window.");if(!L(t))throw new e("INVALID_OPTIONS","Connection options are required.");let{channelId:c,direct:f,getQRCode:l,timeoutMs:s=Ie}=t;if(typeof c!="string"||f!==void 0&&typeof f!="boolean"||l!==void 0&&typeof l!="function"||typeof s!="number"||!Number.isFinite(s)||s<=0||s>2147483647)throw new e("INVALID_OPTIONS","Invalid channelId, direct, getQRCode or timeoutMs option.");if(!c.trim())return j(c,null,"invalid_channel");let E=pe(c);if(typeof i.open!="function"||typeof i.addEventListener!="function"||typeof i.removeEventListener!="function"||typeof i.location.origin!="string"||typeof i.location.href!="string"||typeof((o=i.crypto)==null?void 0:o.getRandomValues)!="function"||typeof AbortController!="function"||typeof fetch!="function")throw new e("INVALID_CONFIGURATION","Required browser APIs are unavailable.");let _=i.location.origin;if(_==="null")return j(c,null,"origin_not_allowed");let I=i.location.href,O=new URL(this.constants.front).origin;return await new Promise((U,G)=>{let N=Symbol("omnizapi-connection"),k=new AbortController,K=Date.now()+s,h=!1,a=null,P=null,y=null,F,Q,z,J=!1,H=!1,X=()=>{r.nativeLoginOwner===N&&(r.nativeLoginPending=!1,delete r.nativeLoginOwner)},Z=[],S=g=>{if(h)return;if(h=!0,clearTimeout(F),clearInterval(Q),z)try{i.removeEventListener("message",z)}catch(M){}try{k.abort()}catch(M){}let D=y;y=null;try{D==null||D.close()}catch(M){}for(let{name:M,previous:m,value:v}of Z)try{let T=Object.getOwnPropertyDescriptor(i,M);if(!T||!("value"in T)||T.value!==v)continue;m?Object.defineProperty(i,M,m):Reflect.deleteProperty(i,M)}catch(T){}r.activeConnection===N&&delete r.activeConnection,J&&(H||!(g instanceof e)&&g.success)&&X(),g instanceof e?G(g):U(g)},b=()=>(!h&&Date.now()>=K&&S(new e("TIMEOUT","The connection timed out.")),!h),C=g=>S(j(c,a,g)),Y=g=>{!b()||!a||S({success:!0,channelId:c,type:a,code:p(g.code),wabaId:p(g.wabaId),phoneId:p(g.phoneId),coexistence:g.coexistence===!0,redirectUri:P,botToken:p(g.botToken),pageId:p(g.pageId),accessToken:p(g.accessToken)})};try{r.activeConnection=N;let g=i.crypto.getRandomValues(new Uint8Array(16)),D=Array.from(g,m=>m.toString(16).padStart(2,"0")).join("");for(let[m,v]of[["__omnizapiPublicKey",this.publicKey],["__omnizapiCurrentChannel",c],["__omnizapiGetQrCode",l]]){let T=Object.getOwnPropertyDescriptor(i,m);Object.defineProperty(i,m,T&&!T.configurable?{value:v}:{configurable:!0,enumerable:!0,writable:!0,value:v}),Z.push({name:m,previous:T,value:v})}F=setTimeout(()=>S(new e("TIMEOUT","The connection timed out.")),s),(async()=>{var ee,ne,te;let m=await me(new URL(`${this.constants.api}/instances/${E}/sdk-info`),{headers:{Authorization:this.publicKey,"Content-Type":"application/json"}},k.signal);if(!b())return;if(!L(m)||!p(m.middleware))throw new e("INVALID_RESPONSE","The channel configuration is invalid.");let v=(ee=m.allowOrigins)!=null?ee:[];if(!Array.isArray(v)||!v.every(u=>typeof u=="string"))throw new e("INVALID_RESPONSE","The channel origins are invalid.");let T=m.middleware;if(a=Oe(T)?T:null,_!==O&&!v.some(u=>_===u.replace(/\/$/,""))){C("origin_not_allowed");return}if(!a){C("unsupported_channel");return}let V=(ne=p(a==="META_INSTAGRAM"?m.appInstagramId:m.appId))!=null?ne:"",B=(te=p(m.appConfig))!=null?te:"";if(a.startsWith("META_")&&!V||a==="META_WHATSAPP"&&!B)throw new e("INVALID_RESPONSE","The channel provider configuration is invalid.");if(a==="META_WHATSAPP"&&(f||m.appCustom||_===O)){if(r.nativeLoginPending)throw new e("CONNECTION_IN_PROGRESS","A previous Meta login is incomplete. Reload this window before retrying a native login.");let u=await ue(i,k.signal);if(!b())return;try{u.init({appId:V,version:"v25.0",autoLogAppEvents:!0,xfbml:!0})}catch(A){throw new e("PROVIDER_ERROR","The Meta SDK could not be initialized.")}if(!b())return;let d=null,x=null,oe=null,ie=!1,re=()=>{!b()||!d||!x||Y({code:d,wabaId:x,phoneId:oe,coexistence:ie})};z=A=>{if(!b()||!["https://www.facebook.com","https://web.facebook.com"].includes(A.origin))return;let R=A.data;if(typeof R=="string")try{R=JSON.parse(R)}catch(_e){return}if(!(!L(R)||R.type!=="WA_EMBEDDED_SIGNUP")){if(R.event==="CANCEL")C("cancelled");else if(R.event==="ERROR")S(new e("PROVIDER_ERROR","Meta could not complete the connection."));else if(R.event==="FINISH"||R.event==="FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING"){if(!L(R.data)||!p(R.data.waba_id)||R.data.phone_number_id!=null&&typeof R.data.phone_number_id!="string")return;x=p(R.data.waba_id),oe=p(R.data.phone_number_id),ie=R.event==="FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING",re()}}},i.addEventListener("message",z),r.nativeLoginPending=!0,r.nativeLoginOwner=N,J=!0;try{u.login(A=>{if(!H){if(H=!0,h){X();return}if(b()){if(!L(A)||A.error){S(new e("PROVIDER_ERROR","Meta returned an invalid login response."));return}if(A.authResponse==null){C("not_completed");return}if(!L(A.authResponse)||!p(A.authResponse.code)){C("not_completed");return}d=p(A.authResponse.code),re()}}},{config_id:B,response_type:"code",override_default_response_type:!0,extras:{setup:{},featureType:"whatsapp_business_app_onboarding",sessionInfoVersion:"3"}})}catch(A){throw new e("PROVIDER_ERROR","The Meta login could not be started.")}return}let w;if(a==="META_MESSENGER"||a==="META_INSTAGRAM"){let u=a==="META_MESSENGER";P=new URL(`${this.constants.front}/oauth/proxy-${u?"messenger":"instagram"}`).href,w=new URL(u?"https://www.facebook.com/dialog/oauth":"https://www.instagram.com/oauth/authorize"),w.searchParams.set("client_id",V),u&&w.searchParams.set("display","popup"),w.searchParams.set("redirect_uri",P),w.searchParams.set("response_type","code"),w.searchParams.set("scope",u?"public_profile,business_management,pages_messaging,pages_show_list,pages_manage_metadata,pages_read_engagement":"instagram_business_basic,instagram_business_manage_messages"),w.searchParams.set("state",JSON.stringify({currentUrl:I,attemptId:D,channelId:c}))}else a==="META_WHATSAPP"?(w=new URL(`${this.constants.front}/oauth/proxy-cloud-api`),w.searchParams.set("configId",B),w.searchParams.set("appId",V),w.searchParams.set("channelId",c),w.searchParams.set("channelType",a),w.searchParams.set("logo",this.logo||new URL(`${this.constants.front}/logos/SVG/Ativo%2016.svg`).href),w.searchParams.set("pk",this.publicKey)):(w=new URL(`${this.constants.front}/connect/${a==="BOT_TELEGRAM"?"telegram":"whatsapp-web"}`),w.searchParams.set("channelId",c));if(a!=="META_MESSENGER"&&a!=="META_INSTAGRAM"&&w.searchParams.set("attemptId",D),z=u=>{if(!b()||!y||u.origin!==O||u.source!==y)return;let d=u.data;!L(d)||d.type!=="after-connect"||d.attemptId!==D||d.channelId!==c||typeof d.success!="boolean"||d.reason!==void 0&&!fe(d.reason)||d.coexistence!==void 0&&typeof d.coexistence!="boolean"||["code","wabaId","phoneId","botToken","pageId","accessToken","redirectUri"].some(x=>d[x]!=null&&typeof d[x]!="string")||(d.success?a!=null&&a.startsWith("META_")&&!p(d.code)||a==="BOT_TELEGRAM"&&!p(d.botToken)?C("not_completed"):Y(d):C(fe(d.reason)?d.reason:"not_completed"))},i.addEventListener("message",z),!!b()){try{let u=de(w.href,"_blank",600,740);if(!u||u===i)throw new e("POPUP_BLOCKED","The connection popup was blocked.");y=u}catch(u){throw new e("POPUP_BLOCKED","The connection popup could not be opened.")}Q=setInterval(()=>{if(b())try{y!=null&&y.closed&&C("popup_closed")}catch(u){S(new e("PROVIDER_ERROR","The connection popup could not be monitored."))}},500)}})().catch(m=>{b()&&S(m instanceof e?m:new e("PROVIDER_ERROR","The connection could not be completed."))})}catch(g){S(new e("INVALID_CONFIGURATION","The connection could not be initialized."))}})}catch(i){throw i instanceof e?i:new e("INVALID_OPTIONS","The connection could not be started with these options.")}}},he=$;var q=class{static newClient(t){return new he(t)}};var ge=q;Object.assign(window,{Omnizapi:ge});})();
|
|
@@ -2,6 +2,8 @@ import type { FacebookSdk } from "./facebook-sdk";
|
|
|
2
2
|
interface SharedRuntime {
|
|
3
3
|
activeConnection?: symbol;
|
|
4
4
|
nativeLoginPending: boolean;
|
|
5
|
+
/** Attempt that set nativeLoginPending; only it may release the lock. */
|
|
6
|
+
nativeLoginOwner?: symbol;
|
|
5
7
|
facebookLoading?: Promise<FacebookSdk>;
|
|
6
8
|
failedFacebookScripts: WeakSet<HTMLScriptElement>;
|
|
7
9
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { isPlatformBrowser } from "@angular/common";
|
|
2
|
+
import { Component, inject, Input, PLATFORM_ID } from "@angular/core";
|
|
3
|
+
import Omnizapi, { OmnizapiError } from "@omnizapi/connect";
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: "app-connect",
|
|
7
|
+
standalone: true,
|
|
8
|
+
template: `
|
|
9
|
+
<button type="button" (click)="handleConnect()" [disabled]="loading">
|
|
10
|
+
{{ loading ? "Connecting..." : "Connect channel" }}
|
|
11
|
+
</button>
|
|
12
|
+
<p role="status">{{ message }}</p>
|
|
13
|
+
`,
|
|
14
|
+
})
|
|
15
|
+
export class ConnectComponent {
|
|
16
|
+
@Input() channelId = "";
|
|
17
|
+
|
|
18
|
+
loading = false;
|
|
19
|
+
message = "";
|
|
20
|
+
|
|
21
|
+
private readonly platformId = inject(PLATFORM_ID);
|
|
22
|
+
private readonly client = Omnizapi.newClient({ publicKey: "your-public-key" });
|
|
23
|
+
|
|
24
|
+
async handleConnect() {
|
|
25
|
+
if (!isPlatformBrowser(this.platformId) || this.loading) return;
|
|
26
|
+
this.loading = true;
|
|
27
|
+
this.message = "Connecting...";
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = await this.client.connect({ channelId: this.channelId });
|
|
31
|
+
|
|
32
|
+
if (!result.success) {
|
|
33
|
+
this.message = `Connection not completed (${result.reason}).`;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const response = await fetch("/api/connect-channel", {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "Content-Type": "application/json" },
|
|
40
|
+
body: JSON.stringify(result),
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
this.message = "The backend could not finalize the connection.";
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.message = "Channel connected!";
|
|
48
|
+
} catch (error) {
|
|
49
|
+
this.message = error instanceof OmnizapiError
|
|
50
|
+
? `Connection error (${error.code}). Please try again.`
|
|
51
|
+
: "Could not complete the request. Please try again.";
|
|
52
|
+
} finally {
|
|
53
|
+
this.loading = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load as a browser module after these elements:
|
|
3
|
+
* <button id="connect-btn" type="button">Connect channel</button>
|
|
4
|
+
* <p id="status" role="status"></p>
|
|
5
|
+
* /api/connect-channel is your authenticated backend, not an SDK route.
|
|
6
|
+
*/
|
|
7
|
+
import Omnizapi, { OmnizapiError } from "@omnizapi/connect";
|
|
8
|
+
|
|
9
|
+
const client = Omnizapi.newClient({ publicKey: "your-public-key" });
|
|
10
|
+
|
|
11
|
+
if (typeof document !== "undefined") {
|
|
12
|
+
const button = document.querySelector<HTMLButtonElement>("#connect-btn");
|
|
13
|
+
const statusElement = document.querySelector<HTMLElement>("#status");
|
|
14
|
+
|
|
15
|
+
if (button && statusElement) {
|
|
16
|
+
button.addEventListener("click", async () => {
|
|
17
|
+
if (button.disabled) return;
|
|
18
|
+
button.disabled = true;
|
|
19
|
+
statusElement.textContent = "Connecting...";
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const result = await client.connect({ channelId: "your-channel-id" });
|
|
23
|
+
|
|
24
|
+
if (!result.success) {
|
|
25
|
+
statusElement.textContent = `Connection not completed (${result.reason}).`;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Includes channelId. Never log credentials; validate them on the server.
|
|
30
|
+
const response = await fetch("/api/connect-channel", {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { "Content-Type": "application/json" },
|
|
33
|
+
body: JSON.stringify(result),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
statusElement.textContent = "The backend could not finalize the connection.";
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
statusElement.textContent = "Channel connected!";
|
|
41
|
+
} catch (error) {
|
|
42
|
+
statusElement.textContent = error instanceof OmnizapiError
|
|
43
|
+
? `Connection error (${error.code}). Please try again.`
|
|
44
|
+
: "Could not complete the request. Please try again.";
|
|
45
|
+
} finally {
|
|
46
|
+
button.disabled = false;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load after <button id="connect-btn" type="button">Connect channel</button>
|
|
3
|
+
* and <p id="status" role="status"></p>.
|
|
4
|
+
* Negative results resolve; technical SDK errors reject. Neither is logged.
|
|
5
|
+
*/
|
|
6
|
+
import Omnizapi, { OmnizapiError } from "@omnizapi/connect";
|
|
7
|
+
import type { ConnectionFailureReason, OmnizapiErrorCode } from "@omnizapi/connect";
|
|
8
|
+
|
|
9
|
+
const failureMessages: Record<ConnectionFailureReason, string> = {
|
|
10
|
+
cancelled: "Authorization was cancelled.",
|
|
11
|
+
popup_closed: "The popup was closed before authorization finished.",
|
|
12
|
+
not_completed: "Authorization was not completed. Please try again.",
|
|
13
|
+
invalid_channel: "This channel is invalid or unavailable.",
|
|
14
|
+
origin_not_allowed: "This application origin is not authorized for the channel.",
|
|
15
|
+
unsupported_channel: "This channel type is not supported.",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const errorMessages: Record<OmnizapiErrorCode, string> = {
|
|
19
|
+
BROWSER_REQUIRED: "Open this page in a browser to connect.",
|
|
20
|
+
CONNECTION_IN_PROGRESS: "Finish the active attempt first. If a previous native Meta login did not complete, reload this page.",
|
|
21
|
+
INVALID_CONFIGURATION: "Check the public key, environment and base URLs.",
|
|
22
|
+
INVALID_OPTIONS: "Check the channel ID and timeout options.",
|
|
23
|
+
NETWORK_ERROR: "The connection service could not be reached. Please try again.",
|
|
24
|
+
HTTP_ERROR: "The connection service rejected the request.",
|
|
25
|
+
INVALID_RESPONSE: "The connection service returned an invalid response.",
|
|
26
|
+
POPUP_BLOCKED: "Allow popups for this application, then click Connect again.",
|
|
27
|
+
TIMEOUT: "The operation timed out. For an incomplete native Meta login, reload before retrying.",
|
|
28
|
+
SDK_LOAD_FAILED: "The provider SDK could not load. Check browser restrictions.",
|
|
29
|
+
PROVIDER_ERROR: "The provider reported an error. Please try again.",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const client = Omnizapi.newClient({ publicKey: "your-public-key" });
|
|
33
|
+
|
|
34
|
+
if (typeof document !== "undefined") {
|
|
35
|
+
const button = document.querySelector<HTMLButtonElement>("#connect-btn");
|
|
36
|
+
const statusElement = document.querySelector<HTMLElement>("#status");
|
|
37
|
+
|
|
38
|
+
if (button && statusElement) {
|
|
39
|
+
button.addEventListener("click", async () => {
|
|
40
|
+
if (button.disabled) return;
|
|
41
|
+
button.disabled = true;
|
|
42
|
+
statusElement.textContent = "Connecting...";
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = await client.connect({ channelId: "your-channel-id" });
|
|
46
|
+
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
statusElement.textContent = failureMessages[result.reason];
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Your backend must authorize result.channelId for the session/tenant.
|
|
53
|
+
const response = await fetch("/api/connect-channel", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
body: JSON.stringify(result),
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
statusElement.textContent = "The backend could not finalize the connection.";
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
statusElement.textContent = "Channel connected!";
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Do not display/log raw errors: messages may contain sensitive URLs.
|
|
66
|
+
statusElement.textContent = error instanceof OmnizapiError
|
|
67
|
+
? errorMessages[error.code]
|
|
68
|
+
: "Could not complete the backend request. Please try again.";
|
|
69
|
+
} finally {
|
|
70
|
+
button.disabled = false;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import Omnizapi, { OmnizapiError } from "@omnizapi/connect";
|
|
5
|
+
|
|
6
|
+
interface ConnectButtonProps {
|
|
7
|
+
channelId: string;
|
|
8
|
+
onSuccess?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ConnectButton({ channelId, onSuccess }: ConnectButtonProps) {
|
|
12
|
+
const [client] = useState(() => Omnizapi.newClient({
|
|
13
|
+
publicKey: "your-public-key",
|
|
14
|
+
logo: "https://app.example.com/logo.png",
|
|
15
|
+
}));
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
const [message, setMessage] = useState("");
|
|
18
|
+
|
|
19
|
+
async function handleConnect() {
|
|
20
|
+
if (loading) return;
|
|
21
|
+
setLoading(true);
|
|
22
|
+
setMessage("Connecting...");
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const result = await client.connect({ channelId });
|
|
26
|
+
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
setMessage(`Connection not completed (${result.reason}).`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Includes channelId; validate the session/tenant and credentials server-side.
|
|
33
|
+
const response = await fetch("/api/connect-channel", {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
body: JSON.stringify(result),
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
setMessage("The backend could not finalize the connection.");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setMessage("Channel connected!");
|
|
44
|
+
onSuccess?.();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
setMessage(error instanceof OmnizapiError
|
|
47
|
+
? `Connection error (${error.code}). Please try again.`
|
|
48
|
+
: "Could not complete the request. Please try again.");
|
|
49
|
+
} finally {
|
|
50
|
+
setLoading(false);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div>
|
|
56
|
+
<button type="button" onClick={handleConnect} disabled={loading}>
|
|
57
|
+
{loading ? "Connecting..." : "Connect channel"}
|
|
58
|
+
</button>
|
|
59
|
+
<p role="status">{message}</p>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Omnizapi SDK - Vanilla HTML Example</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<h1>Connect your channel</h1>
|
|
10
|
+
<button id="connect-btn" type="button">Connect channel</button>
|
|
11
|
+
<p id="status" role="status"></p>
|
|
12
|
+
|
|
13
|
+
<!-- Pin a published, tested version in production. Serve this page over HTTP(S). -->
|
|
14
|
+
<script src="https://unpkg.com/@omnizapi/connect/dist/omnizapi.min.js"></script>
|
|
15
|
+
<script>
|
|
16
|
+
const button = document.getElementById("connect-btn");
|
|
17
|
+
const statusElement = document.getElementById("status");
|
|
18
|
+
let client;
|
|
19
|
+
|
|
20
|
+
button.addEventListener("click", async () => {
|
|
21
|
+
if (button.disabled) return;
|
|
22
|
+
button.disabled = true;
|
|
23
|
+
button.textContent = "Connecting...";
|
|
24
|
+
statusElement.textContent = "Connecting...";
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
client ??= Omnizapi.newClient({ publicKey: "your-public-key" });
|
|
28
|
+
const result = await client.connect({ channelId: "your-channel-id" });
|
|
29
|
+
|
|
30
|
+
if (!result.success) {
|
|
31
|
+
statusElement.textContent = `Connection not completed (${result.reason}).`;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Your authenticated backend validates credentials and result.channelId.
|
|
36
|
+
statusElement.textContent = "Finalizing on the backend...";
|
|
37
|
+
const response = await fetch("/api/connect-channel", {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "Content-Type": "application/json" },
|
|
40
|
+
body: JSON.stringify(result),
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
statusElement.textContent = "The backend could not finalize the connection.";
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
statusElement.textContent = "Channel connected!";
|
|
48
|
+
} catch {
|
|
49
|
+
// The browser global exposes the factory, not the named error export.
|
|
50
|
+
// Never log raw errors or credentials, including URLs in error messages.
|
|
51
|
+
statusElement.textContent =
|
|
52
|
+
"Could not complete the request. Check popup permissions and try again.";
|
|
53
|
+
} finally {
|
|
54
|
+
button.disabled = false;
|
|
55
|
+
button.textContent = "Connect channel";
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
</script>
|
|
59
|
+
</body>
|
|
60
|
+
</html>
|
package/examples/vue.vue
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<button type="button" @click="handleConnect" :disabled="loading">
|
|
4
|
+
{{ loading ? "Connecting..." : "Connect channel" }}
|
|
5
|
+
</button>
|
|
6
|
+
<p role="status">{{ message }}</p>
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import { ref } from "vue";
|
|
12
|
+
import Omnizapi, { OmnizapiError } from "@omnizapi/connect";
|
|
13
|
+
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
channelId: string;
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
success: [];
|
|
20
|
+
}>();
|
|
21
|
+
|
|
22
|
+
const client = Omnizapi.newClient({ publicKey: "your-public-key" });
|
|
23
|
+
const loading = ref(false);
|
|
24
|
+
const message = ref("");
|
|
25
|
+
|
|
26
|
+
async function handleConnect() {
|
|
27
|
+
if (typeof window === "undefined" || loading.value) return;
|
|
28
|
+
loading.value = true;
|
|
29
|
+
message.value = "Connecting...";
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const result = await client.connect({ channelId: props.channelId });
|
|
33
|
+
|
|
34
|
+
if (!result.success) {
|
|
35
|
+
message.value = `Connection not completed (${result.reason}).`;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const response = await fetch("/api/connect-channel", {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "Content-Type": "application/json" },
|
|
42
|
+
body: JSON.stringify(result),
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
message.value = "The backend could not finalize the connection.";
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
message.value = "Channel connected!";
|
|
50
|
+
emit("success");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
message.value = error instanceof OmnizapiError
|
|
53
|
+
? `Connection error (${error.code}). Please try again.`
|
|
54
|
+
: "Could not complete the request. Please try again.";
|
|
55
|
+
} finally {
|
|
56
|
+
loading.value = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox URLs must point to your deployed API and connection frontend.
|
|
3
|
+
* Load after these elements; both buttons use the same Meta WhatsApp channel:
|
|
4
|
+
* <button type="button" data-omnizapi-connect>Connect with default flow</button>
|
|
5
|
+
* <button type="button" data-omnizapi-connect data-direct="true">Connect directly</button>
|
|
6
|
+
* <p id="status" role="status"></p>
|
|
7
|
+
* Channel config or the connection frontend origin may select native signup
|
|
8
|
+
* even with direct: false. An incomplete native login requires a page reload.
|
|
9
|
+
*/
|
|
10
|
+
import Omnizapi, { Environment, OmnizapiError } from "@omnizapi/connect";
|
|
11
|
+
|
|
12
|
+
const client = Omnizapi.newClient({
|
|
13
|
+
publicKey: "your-public-key",
|
|
14
|
+
logo: "https://app.example.com/logo.png",
|
|
15
|
+
environment: Environment.SANDBOX,
|
|
16
|
+
apiUrl: "https://api.example.com",
|
|
17
|
+
frontUrl: "https://connect.example.com",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (typeof document !== "undefined") {
|
|
21
|
+
const buttons = document.querySelectorAll<HTMLButtonElement>("[data-omnizapi-connect]");
|
|
22
|
+
const statusElement = document.querySelector<HTMLElement>("#status");
|
|
23
|
+
|
|
24
|
+
if (statusElement) {
|
|
25
|
+
buttons.forEach((button) => {
|
|
26
|
+
button.addEventListener("click", async () => {
|
|
27
|
+
if (button.disabled) return;
|
|
28
|
+
buttons.forEach((item) => { item.disabled = true; });
|
|
29
|
+
statusElement.textContent = "Connecting...";
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const result = await client.connect({
|
|
33
|
+
channelId: "your-meta-whatsapp-channel-id",
|
|
34
|
+
direct: button.dataset.direct === "true",
|
|
35
|
+
timeoutMs: 5 * 60 * 1000,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!result.success) {
|
|
39
|
+
statusElement.textContent = `Connection not completed (${result.reason}).`;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const response = await fetch("/api/connect-channel", {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: { "Content-Type": "application/json" },
|
|
46
|
+
body: JSON.stringify(result),
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
statusElement.textContent = "The backend could not finalize the connection.";
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
statusElement.textContent = "Channel connected!";
|
|
54
|
+
} catch (error) {
|
|
55
|
+
statusElement.textContent = error instanceof OmnizapiError
|
|
56
|
+
? `Connection error (${error.code}). Check popup permissions before retrying.`
|
|
57
|
+
: "Could not complete the request. Please try again.";
|
|
58
|
+
} finally {
|
|
59
|
+
buttons.forEach((item) => { item.disabled = false; });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnizapi/connect",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Omnizapi SDK for browser applications",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.mjs",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
|
-
"dist"
|
|
24
|
+
"dist",
|
|
25
|
+
"examples"
|
|
25
26
|
],
|
|
26
27
|
"scripts": {
|
|
27
28
|
"dev": "rimraf dist && npm run build:types && dotenv -e .env.local -- node esbuild.config.mjs --watch",
|