@momentco-ai/moment-sdk 0.1.0-dev.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/LICENSE +21 -0
- package/README.md +188 -0
- package/dist/moment-sdk.js +1 -0
- package/dist/moment-sdk.mjs +342 -0
- package/dist/types/sdk/button.d.ts +15 -0
- package/dist/types/sdk/index.d.ts +4 -0
- package/dist/types/sdk/moment-sdk.d.ts +50 -0
- package/dist/types/sdk/popup-bridge.d.ts +36 -0
- package/dist/types/sdk/types.d.ts +58 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moment Co.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Moment SDK
|
|
2
|
+
|
|
3
|
+
Embeddable calendar sync widget for external team websites. Zero runtime dependencies.
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add the SDK via a script tag:
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<script src="https://unpkg.com/@momentco-ai/moment-sdk"></script>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or via npm:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @momentco-ai/moment-sdk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<!-- 1. Add trigger buttons with data attributes -->
|
|
29
|
+
<button
|
|
30
|
+
class="moment-sync-trigger"
|
|
31
|
+
data-moment-team-slug="your-team-slug"
|
|
32
|
+
data-external-event-id="event-001"
|
|
33
|
+
data-moment-trigger-type="moment"
|
|
34
|
+
>
|
|
35
|
+
Add to Calendar
|
|
36
|
+
</button>
|
|
37
|
+
|
|
38
|
+
<!-- 2. Load the SDK and initialize -->
|
|
39
|
+
<script src="https://unpkg.com/@momentco-ai/moment-sdk"></script>
|
|
40
|
+
<script>
|
|
41
|
+
const sdk = new MomentSdk({
|
|
42
|
+
onSuccess: (result) => console.log('Synced!', result),
|
|
43
|
+
onError: (result) => console.error('Failed', result),
|
|
44
|
+
onClose: (result) => console.log('Closed', result),
|
|
45
|
+
});
|
|
46
|
+
</script>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That's it — clicking the button opens a modal where users can sync the event to Google Calendar or Outlook.
|
|
50
|
+
|
|
51
|
+
See [`examples/basic/`](./examples/basic/) for a complete working example.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Init Options
|
|
56
|
+
|
|
57
|
+
| Option | Type | Required | Description |
|
|
58
|
+
| ----------------- | ------------------- | -------- | ------------------------------------------------------------------ |
|
|
59
|
+
| `triggerSelector` | `string` | No | CSS selector for trigger elements. Default: `.moment-sync-trigger` |
|
|
60
|
+
| `onSuccess` | `(payload) => void` | No | Called on successful calendar sync |
|
|
61
|
+
| `onError` | `(payload) => void` | No | Called on sync failure |
|
|
62
|
+
| `onClose` | `(payload) => void` | No | Called when modal closes (success, error, or cancel) |
|
|
63
|
+
| `onAnalytics` | `(event) => void` | No | Called for every analytics event |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Data Attributes
|
|
68
|
+
|
|
69
|
+
Place these on any element matching the trigger selector (default: `.moment-sync-trigger`):
|
|
70
|
+
|
|
71
|
+
| Attribute | Required | Description |
|
|
72
|
+
| -------------------------- | -------- | ------------------------------------------------------------ |
|
|
73
|
+
| `data-moment-team-slug` | ✅ | Team/brand slug |
|
|
74
|
+
| `data-external-event-id` | No | Specific event identifier (resolved to real ID by Moment) |
|
|
75
|
+
| `data-moment-list-slug` | No | List slug (resolved to moment IDs by Moment) |
|
|
76
|
+
| `data-moment-trigger-type` | No | `moment` (default), `list`, or `schedule` |
|
|
77
|
+
| `data-moment-ids` | No | Comma-separated moment IDs (fallback if slugs don't resolve) |
|
|
78
|
+
| `data-moment-game-id` | No | Alias for `data-moment-ids` |
|
|
79
|
+
| `data-moment-calendar` | No | Pre-select provider: `google` or `outlook` |
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Programmatic API
|
|
84
|
+
|
|
85
|
+
You can also open the modal programmatically instead of relying on data attributes:
|
|
86
|
+
|
|
87
|
+
```html
|
|
88
|
+
<script>
|
|
89
|
+
const sdk = new MomentSdk({
|
|
90
|
+
/* callbacks */
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Open modal
|
|
94
|
+
sdk.open({
|
|
95
|
+
teamSlug: 'your-team-slug',
|
|
96
|
+
externalEventId: 'event-123',
|
|
97
|
+
triggerType: 'moment',
|
|
98
|
+
ids: ['moment-id-1'],
|
|
99
|
+
calendar: 'google', // optional: skip provider selection
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Close modal
|
|
103
|
+
sdk.close();
|
|
104
|
+
|
|
105
|
+
// Re-bind triggers after dynamic DOM updates
|
|
106
|
+
sdk.rebind();
|
|
107
|
+
|
|
108
|
+
// Destroy instance entirely
|
|
109
|
+
sdk.destroy();
|
|
110
|
+
</script>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## `createMomentButton()`
|
|
116
|
+
|
|
117
|
+
Programmatically create a styled trigger button:
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<script>
|
|
121
|
+
const btn = createMomentButton({
|
|
122
|
+
teamSlug: 'your-team-slug',
|
|
123
|
+
externalEventId: 'event-001',
|
|
124
|
+
triggerType: 'moment',
|
|
125
|
+
label: 'Add to Calendar',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
document.getElementById('container').appendChild(btn);
|
|
129
|
+
</script>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The button renders with Moment's default styling (stone-900 background, subtle border, rounded corners). Override with CSS or pass a `className` option.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Analytics Events
|
|
137
|
+
|
|
138
|
+
The `onAnalytics` callback receives events with these names:
|
|
139
|
+
|
|
140
|
+
| Event | When |
|
|
141
|
+
| --------------------- | ----------------------------------- |
|
|
142
|
+
| `embed.init` | SDK initialized |
|
|
143
|
+
| `embed.trigger.click` | User clicked a trigger button |
|
|
144
|
+
| `embed.open` | Modal iframe opened |
|
|
145
|
+
| `oauth.start` | OAuth popup requested |
|
|
146
|
+
| `oauth.popup.blocked` | Browser blocked the popup |
|
|
147
|
+
| `oauth.popup.closed` | User closed popup before completing |
|
|
148
|
+
| `subscribe.success` | Calendar sync succeeded |
|
|
149
|
+
| `subscribe.error` | Calendar sync failed |
|
|
150
|
+
| `embed.close` | Modal closed |
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Result Payload
|
|
155
|
+
|
|
156
|
+
All callbacks (`onSuccess`, `onError`, `onClose`) receive a result payload:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"source": "moment-live-embed",
|
|
161
|
+
"type": "moment.embed.result",
|
|
162
|
+
"result": "success | error | cancelled",
|
|
163
|
+
"provider": "google | outlook",
|
|
164
|
+
"triggerType": "moment | list | schedule",
|
|
165
|
+
"momentIds": ["..."],
|
|
166
|
+
"message": "optional details"
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Examples
|
|
173
|
+
|
|
174
|
+
See the [`examples/basic/`](./examples/basic/) directory for a complete standalone integration example.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Contributing
|
|
179
|
+
|
|
180
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and guidelines.
|
|
181
|
+
|
|
182
|
+
## Security
|
|
183
|
+
|
|
184
|
+
See [SECURITY.md](./SECURITY.md) for reporting vulnerabilities.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
[MIT](./LICENSE) — Moment Co.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(u){"use strict";class g{popup=null;pollTimer=null;messageHandler=null;resultReceived=!1;opts;constructor(e){this.opts=e}open(e){this.cleanup(),this.resultReceived=!1;const t=this.buildPopupUrl(e),s=Math.round(window.screenX+(window.outerWidth-500)/2),r=Math.round(window.screenY+(window.outerHeight-700)/2);if(this.popup=window.open(t,"moment-oauth-popup",`width=500,height=700,left=${s},top=${r},menubar=no,toolbar=no,location=yes,status=no`),!this.popup||this.popup.closed){this.sendStatusToIframe("popup-blocked"),this.opts.onPopupBlocked();return}this.sendStatusToIframe("popup-opened"),this.startPolling(),this.messageHandler=i=>{if(i.origin!==this.opts.liveOrigin||i.source!==this.popup)return;const o=i.data;!o||typeof o!="object"||o.source!=="moment-live-embed"||o.type!=="moment.embed.result"||(this.resultReceived=!0,this.sendStatusToIframe("completed"),this.opts.iframeWindow&&this.opts.iframeWindow.postMessage(o,this.opts.liveOrigin),this.opts.onResult(o),this.cleanup())},window.addEventListener("message",this.messageHandler)}cleanup(){if(this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.messageHandler&&(window.removeEventListener("message",this.messageHandler),this.messageHandler=null),this.popup&&!this.popup.closed)try{this.popup.close()}catch{}this.popup=null}buildPopupUrl(e){const t=window.location.origin,s=new URLSearchParams({provider:e.provider,teamSlug:e.teamSlug,ids:e.ids.join(","),subscriptionType:e.triggerType,returnOrigin:t});return`${this.opts.liveBaseUrl}/en/embed/oauth-popup?${s.toString()}`}startPolling(){this.pollTimer=setInterval(()=>{(!this.popup||this.popup.closed)&&(this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.resultReceived||(this.sendStatusToIframe("popup-closed"),this.opts.onPopupClosed()),this.cleanup())},500)}sendStatusToIframe(e){if(!this.opts.iframeWindow)return;const t={source:"moment-sdk",type:"moment.embed.oauth.status",status:e};this.opts.iframeWindow.postMessage(t,this.opts.liveOrigin)}}const f=".moment-sync-trigger",v="https://live.dev.momentco.ai";class m{opts;liveBaseUrl;liveOrigin;overlay=null;iframe=null;popupBridge=null;messageHandler=null;boundClickHandlers=new Map;activePayload=null;previousFocusedElement=null;previousBodyOverflow="";didManageBodyOverflow=!1;closeTimer=null;constructor(e){this.opts={triggerSelector:f,...e},this.liveBaseUrl=v;try{this.liveOrigin=new URL(this.liveBaseUrl).origin}catch{throw new Error(`[MomentSdk] Invalid VITE_LIVE_BASE_URL: "${this.liveBaseUrl}"`)}this.bindTriggers(),this.emitAnalytics("embed.init")}open(e){this.emitAnalytics("embed.open",{teamSlug:e.teamSlug,triggerType:e.triggerType}),this.createModal(e)}close(){this.handleCancel()}rebind(){this.unbindTriggers(),this.bindTriggers()}destroy(){this.cleanup(),this.unbindTriggers()}bindTriggers(){document.querySelectorAll(this.opts.triggerSelector).forEach(t=>{const s=i=>{i.preventDefault(),this.handleTriggerClick(t)},r=this.boundClickHandlers.get(t);r&&t.removeEventListener("click",r),t.addEventListener("click",s),this.boundClickHandlers.set(t,s)})}unbindTriggers(){this.boundClickHandlers.forEach((e,t)=>{t.removeEventListener("click",e)}),this.boundClickHandlers.clear()}handleTriggerClick(e){const t=e.getAttribute("data-moment-team-slug")??"",s=e.getAttribute("data-external-event-id")??void 0,r=e.getAttribute("data-moment-list-slug")??void 0,i=e.getAttribute("data-moment-trigger-type");let o="moment";i!=null&&i!==""&&(i==="moment"||i==="list"||i==="schedule"?o=i:console.warn("[MomentSdk] Invalid data-moment-trigger-type, falling back to 'moment':",i));const l=e.getAttribute("data-moment-ids")??e.getAttribute("data-moment-game-id")??"",d=l?l.split(",").filter(Boolean):[],a=e.getAttribute("data-moment-calendar");let c;if(a!=null&&a!==""&&(a==="google"||a==="outlook"?c=a:console.warn("[MomentSdk] Invalid data-moment-calendar, ignoring value:",a)),!t){console.warn("[MomentSdk] Missing data-moment-team-slug on trigger element");return}this.emitAnalytics("embed.trigger.click",{teamSlug:t,triggerType:o,momentIds:d}),this.open({teamSlug:t,externalEventId:s,listSlug:r,triggerType:o,ids:d,calendar:c??void 0})}createModal(e){this.cleanup(),this.previousFocusedElement=document.activeElement||null,this.activePayload=e;const t=this.buildIframeUrl(e);this.overlay=document.createElement("div"),this.overlay.setAttribute("role","presentation"),Object.assign(this.overlay.style,{position:"fixed",inset:"0",zIndex:"999999",display:"flex",alignItems:"center",justifyContent:"center",backgroundColor:"rgba(0, 0, 0, 0.5)",backdropFilter:"blur(2px)",padding:"16px"}),this.overlay.addEventListener("click",l=>{l.target===this.overlay&&this.handleCancel()});const s=l=>{l.key==="Escape"&&this.handleCancel()};document.addEventListener("keydown",s);const r=document.createElement("div");r.setAttribute("role","dialog"),r.setAttribute("aria-modal","true"),r.setAttribute("aria-label","Calendar sync modal"),r.tabIndex=-1,Object.assign(r.style,{position:"relative",width:"100%",maxWidth:"420px",maxHeight:"90vh",borderRadius:"16px",overflow:"hidden",backgroundColor:"#ffffff",boxShadow:"0 25px 50px -12px rgba(0, 0, 0, 0.25)"});const i=document.createElement("button");i.type="button",i.textContent="ⓧ",i.setAttribute("aria-label","Close"),Object.assign(i.style,{position:"absolute",top:"8px",right:"8px",zIndex:"10",border:"none",backgroundColor:"transparent",color:"#999",fontSize:"20px",lineHeight:"1",cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center"}),i.addEventListener("click",()=>this.handleCancel()),this.iframe=document.createElement("iframe"),this.iframe.src=t,this.iframe.setAttribute("allow",""),this.iframe.setAttribute("sandbox","allow-scripts allow-same-origin allow-popups allow-forms"),Object.assign(this.iframe.style,{width:"100%",height:"480px",border:"none",display:"block"}),r.appendChild(i),r.appendChild(this.iframe),this.overlay.appendChild(r),document.body.appendChild(this.overlay);const o=l=>{if(l.key!=="Tab")return;const d=r.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');if(d.length===0){l.preventDefault(),r.focus();return}const a=d[0],c=d[d.length-1],h=document.activeElement;l.shiftKey&&h===a?(l.preventDefault(),c.focus()):!l.shiftKey&&h===c&&(l.preventDefault(),a.focus())};r.addEventListener("keydown",o),i.focus(),this.previousBodyOverflow=document.body.style.overflow,document.body.style.overflow="hidden",this.didManageBodyOverflow=!0,this.setupMessageListener(),this.overlay._escHandler=s,this.overlay._focusTrapHandler=o}buildIframeUrl(e){const t=new URLSearchParams;return t.set("teamSlug",e.teamSlug),e.externalEventId&&t.set("momentSlug",e.externalEventId),e.listSlug&&t.set("listSlug",e.listSlug),t.set("subscriptionType",e.triggerType),e.ids?.length&&t.set("ids",e.ids.join(",")),e.calendar&&t.set("calendar",e.calendar),t.set("returnOrigin",window.location.origin),t.set("embed","1"),`${this.liveBaseUrl}/en/embed/sync?${t.toString()}`}setupMessageListener(){this.messageHandler=e=>{if(e.origin!==this.liveOrigin||e.source!==this.iframe?.contentWindow)return;const t=e.data;if(!(!t||typeof t!="object")){if(t.source==="moment-live-embed"&&t.type==="moment.embed.oauth.start"){const s=t;this.emitAnalytics("oauth.start",{provider:s.provider,teamSlug:s.teamSlug}),this.handleOAuthStart(s);return}if(t.source==="moment-live-embed"&&t.type==="moment.embed.result"){const s=t;this.handleResult(s)}}},window.addEventListener("message",this.messageHandler)}handleOAuthStart(e){this.popupBridge&&(this.popupBridge.cleanup(),this.popupBridge=null),this.popupBridge=new g({liveBaseUrl:this.liveBaseUrl,liveOrigin:this.liveOrigin,iframeWindow:this.iframe?.contentWindow??null,onResult:t=>{this.handleResult(t)},onPopupBlocked:()=>{this.emitAnalytics("oauth.popup.blocked",{provider:e.provider})},onPopupClosed:()=>{this.emitAnalytics("oauth.popup.closed",{provider:e.provider})}}),this.popupBridge.open(e)}handleResult(e){if(e.result==="success")this.emitAnalytics("subscribe.success",{provider:e.provider,triggerType:e.triggerType,momentIds:e.momentIds}),this.opts.onSuccess?.(e);else if(e.result==="error")this.emitAnalytics("subscribe.error",{provider:e.provider,triggerType:e.triggerType,momentIds:e.momentIds}),this.opts.onError?.(e);else if(e.result==="cancelled")return;this.opts.onClose?.(e),this.emitAnalytics("embed.close"),this.closeTimer=setTimeout(()=>this.cleanup(),1500)}handleCancel(){this.emitAnalytics("embed.close");const e={source:"moment-live-embed",type:"moment.embed.result",result:"cancelled",triggerType:this.activePayload?.triggerType??"moment",momentIds:this.activePayload?.ids??[]};this.opts.onClose?.(e),this.cleanup()}cleanup(){if(this.closeTimer&&(clearTimeout(this.closeTimer),this.closeTimer=null),this.popupBridge&&(this.popupBridge.cleanup(),this.popupBridge=null),this.messageHandler&&(window.removeEventListener("message",this.messageHandler),this.messageHandler=null),this.overlay){const{_escHandler:e,_focusTrapHandler:t}=this.overlay;e&&document.removeEventListener("keydown",e);const s=this.overlay.firstElementChild;s&&t&&s.removeEventListener("keydown",t),this.overlay.remove(),this.overlay=null}this.iframe=null,this.activePayload=null,this.didManageBodyOverflow&&(document.body.style.overflow=this.previousBodyOverflow,this.didManageBodyOverflow=!1),this.previousFocusedElement&&typeof this.previousFocusedElement.focus=="function"&&this.previousFocusedElement.focus(),this.previousFocusedElement=null}emitAnalytics(e,t){this.opts.onAnalytics?.({event:e,timestamp:Date.now(),...t})}}function p(n){const e=document.createElement("button");return e.type="button",e.classList.add("moment-sync-trigger"),n.className&&n.className.split(" ").forEach(t=>{t&&e.classList.add(t)}),Object.assign(e.style,{backgroundColor:"#1c1917",color:"#ffffff",border:"1px solid #292524",borderRadius:"4px",padding:"8px 16px",fontSize:"14px",fontWeight:"500",cursor:"pointer",lineHeight:"1.4"}),e.setAttribute("data-moment-team-slug",n.teamSlug),n.externalEventId&&e.setAttribute("data-external-event-id",n.externalEventId),n.listSlug&&e.setAttribute("data-moment-list-slug",n.listSlug),n.triggerType&&e.setAttribute("data-moment-trigger-type",n.triggerType),n.ids?.length&&e.setAttribute("data-moment-ids",n.ids.join(",")),n.calendar&&e.setAttribute("data-moment-calendar",n.calendar),e.textContent=n.label??"Add to Calendar",e}if(typeof window<"u"){const n=window;n.MomentSdk=m,n.createMomentButton=p}u.MomentSdk=m,u.createMomentButton=p,Object.defineProperty(u,Symbol.toStringTag,{value:"Module"})})(this.MomentSdk=this.MomentSdk||{});
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
class m {
|
|
2
|
+
popup = null;
|
|
3
|
+
pollTimer = null;
|
|
4
|
+
messageHandler = null;
|
|
5
|
+
resultReceived = !1;
|
|
6
|
+
opts;
|
|
7
|
+
constructor(e) {
|
|
8
|
+
this.opts = e;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Open the OAuth popup in response to a `moment.embed.oauth.start` message.
|
|
12
|
+
*/
|
|
13
|
+
open(e) {
|
|
14
|
+
this.cleanup(), this.resultReceived = !1;
|
|
15
|
+
const t = this.buildPopupUrl(e), s = Math.round(window.screenX + (window.outerWidth - 500) / 2), r = Math.round(window.screenY + (window.outerHeight - 700) / 2);
|
|
16
|
+
if (this.popup = window.open(
|
|
17
|
+
t,
|
|
18
|
+
"moment-oauth-popup",
|
|
19
|
+
`width=500,height=700,left=${s},top=${r},menubar=no,toolbar=no,location=yes,status=no`
|
|
20
|
+
), !this.popup || this.popup.closed) {
|
|
21
|
+
this.sendStatusToIframe("popup-blocked"), this.opts.onPopupBlocked();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.sendStatusToIframe("popup-opened"), this.startPolling(), this.messageHandler = (i) => {
|
|
25
|
+
if (i.origin !== this.opts.liveOrigin || i.source !== this.popup) return;
|
|
26
|
+
const o = i.data;
|
|
27
|
+
!o || typeof o != "object" || o.source !== "moment-live-embed" || o.type !== "moment.embed.result" || (this.resultReceived = !0, this.sendStatusToIframe("completed"), this.opts.iframeWindow && this.opts.iframeWindow.postMessage(o, this.opts.liveOrigin), this.opts.onResult(o), this.cleanup());
|
|
28
|
+
}, window.addEventListener("message", this.messageHandler);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Clean up popup, polling, and event listeners.
|
|
32
|
+
*/
|
|
33
|
+
cleanup() {
|
|
34
|
+
if (this.pollTimer && (clearInterval(this.pollTimer), this.pollTimer = null), this.messageHandler && (window.removeEventListener("message", this.messageHandler), this.messageHandler = null), this.popup && !this.popup.closed)
|
|
35
|
+
try {
|
|
36
|
+
this.popup.close();
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
this.popup = null;
|
|
40
|
+
}
|
|
41
|
+
buildPopupUrl(e) {
|
|
42
|
+
const t = window.location.origin, s = new URLSearchParams({
|
|
43
|
+
provider: e.provider,
|
|
44
|
+
teamSlug: e.teamSlug,
|
|
45
|
+
ids: e.ids.join(","),
|
|
46
|
+
subscriptionType: e.triggerType,
|
|
47
|
+
returnOrigin: t
|
|
48
|
+
});
|
|
49
|
+
return `${this.opts.liveBaseUrl}/en/embed/oauth-popup?${s.toString()}`;
|
|
50
|
+
}
|
|
51
|
+
startPolling() {
|
|
52
|
+
this.pollTimer = setInterval(() => {
|
|
53
|
+
(!this.popup || this.popup.closed) && (this.pollTimer && (clearInterval(this.pollTimer), this.pollTimer = null), this.resultReceived || (this.sendStatusToIframe("popup-closed"), this.opts.onPopupClosed()), this.cleanup());
|
|
54
|
+
}, 500);
|
|
55
|
+
}
|
|
56
|
+
sendStatusToIframe(e) {
|
|
57
|
+
if (!this.opts.iframeWindow) return;
|
|
58
|
+
const t = {
|
|
59
|
+
source: "moment-sdk",
|
|
60
|
+
type: "moment.embed.oauth.status",
|
|
61
|
+
status: e
|
|
62
|
+
};
|
|
63
|
+
this.opts.iframeWindow.postMessage(t, this.opts.liveOrigin);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const p = ".moment-sync-trigger", h = "https://live.dev.momentco.ai";
|
|
67
|
+
class g {
|
|
68
|
+
opts;
|
|
69
|
+
liveBaseUrl;
|
|
70
|
+
liveOrigin;
|
|
71
|
+
overlay = null;
|
|
72
|
+
iframe = null;
|
|
73
|
+
popupBridge = null;
|
|
74
|
+
messageHandler = null;
|
|
75
|
+
boundClickHandlers = /* @__PURE__ */ new Map();
|
|
76
|
+
activePayload = null;
|
|
77
|
+
previousFocusedElement = null;
|
|
78
|
+
previousBodyOverflow = "";
|
|
79
|
+
didManageBodyOverflow = !1;
|
|
80
|
+
closeTimer = null;
|
|
81
|
+
constructor(e) {
|
|
82
|
+
this.opts = {
|
|
83
|
+
triggerSelector: p,
|
|
84
|
+
...e
|
|
85
|
+
}, this.liveBaseUrl = h;
|
|
86
|
+
try {
|
|
87
|
+
this.liveOrigin = new URL(this.liveBaseUrl).origin;
|
|
88
|
+
} catch {
|
|
89
|
+
throw new Error(`[MomentSdk] Invalid VITE_LIVE_BASE_URL: "${this.liveBaseUrl}"`);
|
|
90
|
+
}
|
|
91
|
+
this.bindTriggers(), this.emitAnalytics("embed.init");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Programmatically open the sync modal.
|
|
95
|
+
*/
|
|
96
|
+
open(e) {
|
|
97
|
+
this.emitAnalytics("embed.open", {
|
|
98
|
+
teamSlug: e.teamSlug,
|
|
99
|
+
triggerType: e.triggerType
|
|
100
|
+
}), this.createModal(e);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Close the modal and clean up all resources.
|
|
104
|
+
*/
|
|
105
|
+
close() {
|
|
106
|
+
this.handleCancel();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Re-bind trigger elements (call after dynamic DOM updates).
|
|
110
|
+
*/
|
|
111
|
+
rebind() {
|
|
112
|
+
this.unbindTriggers(), this.bindTriggers();
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Destroy the SDK instance, removing all event listeners and DOM.
|
|
116
|
+
*/
|
|
117
|
+
destroy() {
|
|
118
|
+
this.cleanup(), this.unbindTriggers();
|
|
119
|
+
}
|
|
120
|
+
// ── Private: trigger binding ──────────────────────────────────────
|
|
121
|
+
bindTriggers() {
|
|
122
|
+
document.querySelectorAll(this.opts.triggerSelector).forEach((t) => {
|
|
123
|
+
const s = (i) => {
|
|
124
|
+
i.preventDefault(), this.handleTriggerClick(t);
|
|
125
|
+
}, r = this.boundClickHandlers.get(t);
|
|
126
|
+
r && t.removeEventListener("click", r), t.addEventListener("click", s), this.boundClickHandlers.set(t, s);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
unbindTriggers() {
|
|
130
|
+
this.boundClickHandlers.forEach((e, t) => {
|
|
131
|
+
t.removeEventListener("click", e);
|
|
132
|
+
}), this.boundClickHandlers.clear();
|
|
133
|
+
}
|
|
134
|
+
handleTriggerClick(e) {
|
|
135
|
+
const t = e.getAttribute("data-moment-team-slug") ?? "", s = e.getAttribute("data-external-event-id") ?? void 0, r = e.getAttribute("data-moment-list-slug") ?? void 0, i = e.getAttribute("data-moment-trigger-type");
|
|
136
|
+
let o = "moment";
|
|
137
|
+
i != null && i !== "" && (i === "moment" || i === "list" || i === "schedule" ? o = i : console.warn(
|
|
138
|
+
"[MomentSdk] Invalid data-moment-trigger-type, falling back to 'moment':",
|
|
139
|
+
i
|
|
140
|
+
));
|
|
141
|
+
const l = e.getAttribute("data-moment-ids") ?? e.getAttribute("data-moment-game-id") ?? "", d = l ? l.split(",").filter(Boolean) : [], a = e.getAttribute("data-moment-calendar");
|
|
142
|
+
let c;
|
|
143
|
+
if (a != null && a !== "" && (a === "google" || a === "outlook" ? c = a : console.warn("[MomentSdk] Invalid data-moment-calendar, ignoring value:", a)), !t) {
|
|
144
|
+
console.warn("[MomentSdk] Missing data-moment-team-slug on trigger element");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.emitAnalytics("embed.trigger.click", {
|
|
148
|
+
teamSlug: t,
|
|
149
|
+
triggerType: o,
|
|
150
|
+
momentIds: d
|
|
151
|
+
}), this.open({
|
|
152
|
+
teamSlug: t,
|
|
153
|
+
externalEventId: s,
|
|
154
|
+
listSlug: r,
|
|
155
|
+
triggerType: o,
|
|
156
|
+
ids: d,
|
|
157
|
+
calendar: c ?? void 0
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// ── Private: modal creation ───────────────────────────────────────
|
|
161
|
+
createModal(e) {
|
|
162
|
+
this.cleanup(), this.previousFocusedElement = document.activeElement || null, this.activePayload = e;
|
|
163
|
+
const t = this.buildIframeUrl(e);
|
|
164
|
+
this.overlay = document.createElement("div"), this.overlay.setAttribute("role", "presentation"), Object.assign(this.overlay.style, {
|
|
165
|
+
position: "fixed",
|
|
166
|
+
inset: "0",
|
|
167
|
+
zIndex: "999999",
|
|
168
|
+
display: "flex",
|
|
169
|
+
alignItems: "center",
|
|
170
|
+
justifyContent: "center",
|
|
171
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
172
|
+
backdropFilter: "blur(2px)",
|
|
173
|
+
padding: "16px"
|
|
174
|
+
}), this.overlay.addEventListener("click", (l) => {
|
|
175
|
+
l.target === this.overlay && this.handleCancel();
|
|
176
|
+
});
|
|
177
|
+
const s = (l) => {
|
|
178
|
+
l.key === "Escape" && this.handleCancel();
|
|
179
|
+
};
|
|
180
|
+
document.addEventListener("keydown", s);
|
|
181
|
+
const r = document.createElement("div");
|
|
182
|
+
r.setAttribute("role", "dialog"), r.setAttribute("aria-modal", "true"), r.setAttribute("aria-label", "Calendar sync modal"), r.tabIndex = -1, Object.assign(r.style, {
|
|
183
|
+
position: "relative",
|
|
184
|
+
width: "100%",
|
|
185
|
+
maxWidth: "420px",
|
|
186
|
+
maxHeight: "90vh",
|
|
187
|
+
borderRadius: "16px",
|
|
188
|
+
overflow: "hidden",
|
|
189
|
+
backgroundColor: "#ffffff",
|
|
190
|
+
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)"
|
|
191
|
+
});
|
|
192
|
+
const i = document.createElement("button");
|
|
193
|
+
i.type = "button", i.textContent = "ⓧ", i.setAttribute("aria-label", "Close"), Object.assign(i.style, {
|
|
194
|
+
position: "absolute",
|
|
195
|
+
top: "8px",
|
|
196
|
+
right: "8px",
|
|
197
|
+
zIndex: "10",
|
|
198
|
+
border: "none",
|
|
199
|
+
backgroundColor: "transparent",
|
|
200
|
+
color: "#999",
|
|
201
|
+
fontSize: "20px",
|
|
202
|
+
lineHeight: "1",
|
|
203
|
+
cursor: "pointer",
|
|
204
|
+
display: "flex",
|
|
205
|
+
alignItems: "center",
|
|
206
|
+
justifyContent: "center"
|
|
207
|
+
}), i.addEventListener("click", () => this.handleCancel()), this.iframe = document.createElement("iframe"), this.iframe.src = t, this.iframe.setAttribute("allow", ""), this.iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-popups allow-forms"), Object.assign(this.iframe.style, {
|
|
208
|
+
width: "100%",
|
|
209
|
+
height: "480px",
|
|
210
|
+
border: "none",
|
|
211
|
+
display: "block"
|
|
212
|
+
}), r.appendChild(i), r.appendChild(this.iframe), this.overlay.appendChild(r), document.body.appendChild(this.overlay);
|
|
213
|
+
const o = (l) => {
|
|
214
|
+
if (l.key !== "Tab") return;
|
|
215
|
+
const d = r.querySelectorAll(
|
|
216
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
217
|
+
);
|
|
218
|
+
if (d.length === 0) {
|
|
219
|
+
l.preventDefault(), r.focus();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const a = d[0], c = d[d.length - 1], u = document.activeElement;
|
|
223
|
+
l.shiftKey && u === a ? (l.preventDefault(), c.focus()) : !l.shiftKey && u === c && (l.preventDefault(), a.focus());
|
|
224
|
+
};
|
|
225
|
+
r.addEventListener("keydown", o), i.focus(), this.previousBodyOverflow = document.body.style.overflow, document.body.style.overflow = "hidden", this.didManageBodyOverflow = !0, this.setupMessageListener(), this.overlay._escHandler = s, this.overlay._focusTrapHandler = o;
|
|
226
|
+
}
|
|
227
|
+
buildIframeUrl(e) {
|
|
228
|
+
const t = new URLSearchParams();
|
|
229
|
+
return t.set("teamSlug", e.teamSlug), e.externalEventId && t.set("momentSlug", e.externalEventId), e.listSlug && t.set("listSlug", e.listSlug), t.set("subscriptionType", e.triggerType), e.ids?.length && t.set("ids", e.ids.join(",")), e.calendar && t.set("calendar", e.calendar), t.set("returnOrigin", window.location.origin), t.set("embed", "1"), `${this.liveBaseUrl}/en/embed/sync?${t.toString()}`;
|
|
230
|
+
}
|
|
231
|
+
// ── Private: message handling ─────────────────────────────────────
|
|
232
|
+
setupMessageListener() {
|
|
233
|
+
this.messageHandler = (e) => {
|
|
234
|
+
if (e.origin !== this.liveOrigin || e.source !== this.iframe?.contentWindow) return;
|
|
235
|
+
const t = e.data;
|
|
236
|
+
if (!(!t || typeof t != "object")) {
|
|
237
|
+
if (t.source === "moment-live-embed" && t.type === "moment.embed.oauth.start") {
|
|
238
|
+
const s = t;
|
|
239
|
+
this.emitAnalytics("oauth.start", {
|
|
240
|
+
provider: s.provider,
|
|
241
|
+
teamSlug: s.teamSlug
|
|
242
|
+
}), this.handleOAuthStart(s);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (t.source === "moment-live-embed" && t.type === "moment.embed.result") {
|
|
246
|
+
const s = t;
|
|
247
|
+
this.handleResult(s);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}, window.addEventListener("message", this.messageHandler);
|
|
251
|
+
}
|
|
252
|
+
handleOAuthStart(e) {
|
|
253
|
+
this.popupBridge && (this.popupBridge.cleanup(), this.popupBridge = null), this.popupBridge = new m({
|
|
254
|
+
liveBaseUrl: this.liveBaseUrl,
|
|
255
|
+
liveOrigin: this.liveOrigin,
|
|
256
|
+
iframeWindow: this.iframe?.contentWindow ?? null,
|
|
257
|
+
onResult: (t) => {
|
|
258
|
+
this.handleResult(t);
|
|
259
|
+
},
|
|
260
|
+
onPopupBlocked: () => {
|
|
261
|
+
this.emitAnalytics("oauth.popup.blocked", {
|
|
262
|
+
provider: e.provider
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
onPopupClosed: () => {
|
|
266
|
+
this.emitAnalytics("oauth.popup.closed", {
|
|
267
|
+
provider: e.provider
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}), this.popupBridge.open(e);
|
|
271
|
+
}
|
|
272
|
+
handleResult(e) {
|
|
273
|
+
if (e.result === "success")
|
|
274
|
+
this.emitAnalytics("subscribe.success", {
|
|
275
|
+
provider: e.provider,
|
|
276
|
+
triggerType: e.triggerType,
|
|
277
|
+
momentIds: e.momentIds
|
|
278
|
+
}), this.opts.onSuccess?.(e);
|
|
279
|
+
else if (e.result === "error")
|
|
280
|
+
this.emitAnalytics("subscribe.error", {
|
|
281
|
+
provider: e.provider,
|
|
282
|
+
triggerType: e.triggerType,
|
|
283
|
+
momentIds: e.momentIds
|
|
284
|
+
}), this.opts.onError?.(e);
|
|
285
|
+
else if (e.result === "cancelled")
|
|
286
|
+
return;
|
|
287
|
+
this.opts.onClose?.(e), this.emitAnalytics("embed.close"), this.closeTimer = setTimeout(() => this.cleanup(), 1500);
|
|
288
|
+
}
|
|
289
|
+
handleCancel() {
|
|
290
|
+
this.emitAnalytics("embed.close");
|
|
291
|
+
const e = {
|
|
292
|
+
source: "moment-live-embed",
|
|
293
|
+
type: "moment.embed.result",
|
|
294
|
+
result: "cancelled",
|
|
295
|
+
triggerType: this.activePayload?.triggerType ?? "moment",
|
|
296
|
+
momentIds: this.activePayload?.ids ?? []
|
|
297
|
+
};
|
|
298
|
+
this.opts.onClose?.(e), this.cleanup();
|
|
299
|
+
}
|
|
300
|
+
// ── Private: cleanup ──────────────────────────────────────────────
|
|
301
|
+
cleanup() {
|
|
302
|
+
if (this.closeTimer && (clearTimeout(this.closeTimer), this.closeTimer = null), this.popupBridge && (this.popupBridge.cleanup(), this.popupBridge = null), this.messageHandler && (window.removeEventListener("message", this.messageHandler), this.messageHandler = null), this.overlay) {
|
|
303
|
+
const { _escHandler: e, _focusTrapHandler: t } = this.overlay;
|
|
304
|
+
e && document.removeEventListener("keydown", e);
|
|
305
|
+
const s = this.overlay.firstElementChild;
|
|
306
|
+
s && t && s.removeEventListener("keydown", t), this.overlay.remove(), this.overlay = null;
|
|
307
|
+
}
|
|
308
|
+
this.iframe = null, this.activePayload = null, this.didManageBodyOverflow && (document.body.style.overflow = this.previousBodyOverflow, this.didManageBodyOverflow = !1), this.previousFocusedElement && typeof this.previousFocusedElement.focus == "function" && this.previousFocusedElement.focus(), this.previousFocusedElement = null;
|
|
309
|
+
}
|
|
310
|
+
// ── Private: analytics ────────────────────────────────────────────
|
|
311
|
+
emitAnalytics(e, t) {
|
|
312
|
+
this.opts.onAnalytics?.({
|
|
313
|
+
event: e,
|
|
314
|
+
timestamp: Date.now(),
|
|
315
|
+
...t
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function f(n) {
|
|
320
|
+
const e = document.createElement("button");
|
|
321
|
+
return e.type = "button", e.classList.add("moment-sync-trigger"), n.className && n.className.split(" ").forEach((t) => {
|
|
322
|
+
t && e.classList.add(t);
|
|
323
|
+
}), Object.assign(e.style, {
|
|
324
|
+
backgroundColor: "#1c1917",
|
|
325
|
+
color: "#ffffff",
|
|
326
|
+
border: "1px solid #292524",
|
|
327
|
+
borderRadius: "4px",
|
|
328
|
+
padding: "8px 16px",
|
|
329
|
+
fontSize: "14px",
|
|
330
|
+
fontWeight: "500",
|
|
331
|
+
cursor: "pointer",
|
|
332
|
+
lineHeight: "1.4"
|
|
333
|
+
}), e.setAttribute("data-moment-team-slug", n.teamSlug), n.externalEventId && e.setAttribute("data-external-event-id", n.externalEventId), n.listSlug && e.setAttribute("data-moment-list-slug", n.listSlug), n.triggerType && e.setAttribute("data-moment-trigger-type", n.triggerType), n.ids?.length && e.setAttribute("data-moment-ids", n.ids.join(",")), n.calendar && e.setAttribute("data-moment-calendar", n.calendar), e.textContent = n.label ?? "Add to Calendar", e;
|
|
334
|
+
}
|
|
335
|
+
if (typeof window < "u") {
|
|
336
|
+
const n = window;
|
|
337
|
+
n.MomentSdk = g, n.createMomentButton = f;
|
|
338
|
+
}
|
|
339
|
+
export {
|
|
340
|
+
g as MomentSdk,
|
|
341
|
+
f as createMomentButton
|
|
342
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MomentTriggerType, CalendarProvider } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Programmatically create a trigger button element with the correct
|
|
4
|
+
* data attributes for the MomentSdk to bind to.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createMomentButton(options: {
|
|
7
|
+
teamSlug: string;
|
|
8
|
+
externalEventId?: string;
|
|
9
|
+
listSlug?: string;
|
|
10
|
+
triggerType?: MomentTriggerType;
|
|
11
|
+
ids?: string[];
|
|
12
|
+
calendar?: CalendarProvider;
|
|
13
|
+
label?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}): HTMLButtonElement;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { MomentSdk } from './moment-sdk';
|
|
2
|
+
import { createMomentButton } from './button';
|
|
3
|
+
export { MomentSdk, createMomentButton };
|
|
4
|
+
export type { MomentSdkInitOptions, MomentSdkOpenPayload, MomentTriggerType, CalendarProvider, MomentResultPayload, MomentOAuthStartPayload, MomentOAuthStatusPayload, MomentAnalyticsEvent, MomentAnalyticsEventName, MomentPostMessage, } from './types';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { MomentSdkInitOptions, MomentSdkOpenPayload } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* MomentSdk — embeddable calendar sync widget.
|
|
4
|
+
*
|
|
5
|
+
* Opens a modal iframe to a Moment Live embed route for provider selection,
|
|
6
|
+
* then bridges OAuth via popup to avoid iframe OAuth restrictions.
|
|
7
|
+
*/
|
|
8
|
+
export declare class MomentSdk {
|
|
9
|
+
private opts;
|
|
10
|
+
private liveBaseUrl;
|
|
11
|
+
private liveOrigin;
|
|
12
|
+
private overlay;
|
|
13
|
+
private iframe;
|
|
14
|
+
private popupBridge;
|
|
15
|
+
private messageHandler;
|
|
16
|
+
private boundClickHandlers;
|
|
17
|
+
private activePayload;
|
|
18
|
+
private previousFocusedElement;
|
|
19
|
+
private previousBodyOverflow;
|
|
20
|
+
private didManageBodyOverflow;
|
|
21
|
+
private closeTimer;
|
|
22
|
+
constructor(options: MomentSdkInitOptions);
|
|
23
|
+
/**
|
|
24
|
+
* Programmatically open the sync modal.
|
|
25
|
+
*/
|
|
26
|
+
open(payload: MomentSdkOpenPayload): void;
|
|
27
|
+
/**
|
|
28
|
+
* Close the modal and clean up all resources.
|
|
29
|
+
*/
|
|
30
|
+
close(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Re-bind trigger elements (call after dynamic DOM updates).
|
|
33
|
+
*/
|
|
34
|
+
rebind(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Destroy the SDK instance, removing all event listeners and DOM.
|
|
37
|
+
*/
|
|
38
|
+
destroy(): void;
|
|
39
|
+
private bindTriggers;
|
|
40
|
+
private unbindTriggers;
|
|
41
|
+
private handleTriggerClick;
|
|
42
|
+
private createModal;
|
|
43
|
+
private buildIframeUrl;
|
|
44
|
+
private setupMessageListener;
|
|
45
|
+
private handleOAuthStart;
|
|
46
|
+
private handleResult;
|
|
47
|
+
private handleCancel;
|
|
48
|
+
private cleanup;
|
|
49
|
+
private emitAnalytics;
|
|
50
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MomentOAuthStartPayload, MomentResultPayload } from './types';
|
|
2
|
+
interface PopupBridgeOptions {
|
|
3
|
+
liveBaseUrl: string;
|
|
4
|
+
liveOrigin: string;
|
|
5
|
+
iframeWindow: Window | null;
|
|
6
|
+
onResult: (payload: MomentResultPayload) => void;
|
|
7
|
+
onPopupBlocked: () => void;
|
|
8
|
+
onPopupClosed: () => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Manages the popup lifecycle for OAuth:
|
|
12
|
+
* 1. Opens a popup to the Live OAuth route
|
|
13
|
+
* 2. Polls for popup close
|
|
14
|
+
* 3. Listens for result postMessage from popup
|
|
15
|
+
* 4. Relays status to iframe
|
|
16
|
+
*/
|
|
17
|
+
export declare class PopupBridge {
|
|
18
|
+
private popup;
|
|
19
|
+
private pollTimer;
|
|
20
|
+
private messageHandler;
|
|
21
|
+
private resultReceived;
|
|
22
|
+
private opts;
|
|
23
|
+
constructor(opts: PopupBridgeOptions);
|
|
24
|
+
/**
|
|
25
|
+
* Open the OAuth popup in response to a `moment.embed.oauth.start` message.
|
|
26
|
+
*/
|
|
27
|
+
open(payload: MomentOAuthStartPayload): void;
|
|
28
|
+
/**
|
|
29
|
+
* Clean up popup, polling, and event listeners.
|
|
30
|
+
*/
|
|
31
|
+
cleanup(): void;
|
|
32
|
+
private buildPopupUrl;
|
|
33
|
+
private startPolling;
|
|
34
|
+
private sendStatusToIframe;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type MomentTriggerType = 'moment' | 'list' | 'schedule';
|
|
2
|
+
export type CalendarProvider = 'google' | 'outlook';
|
|
3
|
+
export interface MomentSdkInitOptions {
|
|
4
|
+
/** CSS selector for trigger elements. Default: ".moment-sync-trigger" */
|
|
5
|
+
triggerSelector?: string;
|
|
6
|
+
/** Called when calendar sync completes successfully. */
|
|
7
|
+
onSuccess?: (payload: MomentResultPayload) => void;
|
|
8
|
+
/** Called when calendar sync fails. */
|
|
9
|
+
onError?: (payload: MomentResultPayload) => void;
|
|
10
|
+
/** Called when the modal is closed (success, error, or cancel). */
|
|
11
|
+
onClose?: (payload: MomentResultPayload) => void;
|
|
12
|
+
/** Optional analytics callback for tracking embed events. */
|
|
13
|
+
onAnalytics?: (event: MomentAnalyticsEvent) => void;
|
|
14
|
+
}
|
|
15
|
+
export interface MomentSdkOpenPayload {
|
|
16
|
+
teamSlug: string;
|
|
17
|
+
externalEventId?: string;
|
|
18
|
+
listSlug?: string;
|
|
19
|
+
triggerType: MomentTriggerType;
|
|
20
|
+
ids?: string[];
|
|
21
|
+
calendar?: CalendarProvider;
|
|
22
|
+
}
|
|
23
|
+
/** Iframe → Parent: request to start OAuth via popup */
|
|
24
|
+
export interface MomentOAuthStartPayload {
|
|
25
|
+
source: 'moment-live-embed';
|
|
26
|
+
type: 'moment.embed.oauth.start';
|
|
27
|
+
provider: CalendarProvider;
|
|
28
|
+
teamSlug: string;
|
|
29
|
+
ids: string[];
|
|
30
|
+
triggerType: MomentTriggerType;
|
|
31
|
+
returnOrigin: string;
|
|
32
|
+
}
|
|
33
|
+
/** Parent → Iframe: relay OAuth popup status */
|
|
34
|
+
export interface MomentOAuthStatusPayload {
|
|
35
|
+
source: 'moment-sdk';
|
|
36
|
+
type: 'moment.embed.oauth.status';
|
|
37
|
+
status: 'popup-opened' | 'popup-blocked' | 'popup-closed' | 'completed';
|
|
38
|
+
}
|
|
39
|
+
/** Result payload (popup → parent, or iframe → parent for cancel) */
|
|
40
|
+
export interface MomentResultPayload {
|
|
41
|
+
source: 'moment-live-embed';
|
|
42
|
+
type: 'moment.embed.result';
|
|
43
|
+
result: 'success' | 'error' | 'cancelled';
|
|
44
|
+
provider?: CalendarProvider;
|
|
45
|
+
triggerType: MomentTriggerType;
|
|
46
|
+
momentIds: string[];
|
|
47
|
+
message?: string;
|
|
48
|
+
}
|
|
49
|
+
export type MomentAnalyticsEventName = 'embed.init' | 'embed.trigger.click' | 'embed.open' | 'oauth.start' | 'oauth.popup.blocked' | 'oauth.popup.closed' | 'subscribe.success' | 'subscribe.error' | 'embed.close';
|
|
50
|
+
export interface MomentAnalyticsEvent {
|
|
51
|
+
event: MomentAnalyticsEventName;
|
|
52
|
+
provider?: CalendarProvider;
|
|
53
|
+
teamSlug?: string;
|
|
54
|
+
triggerType?: MomentTriggerType;
|
|
55
|
+
momentIds?: string[];
|
|
56
|
+
timestamp: number;
|
|
57
|
+
}
|
|
58
|
+
export type MomentPostMessage = MomentOAuthStartPayload | MomentOAuthStatusPayload | MomentResultPayload;
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@momentco-ai/moment-sdk",
|
|
3
|
+
"version": "0.1.0-dev.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Embeddable calendar sync widget for external team websites",
|
|
6
|
+
"author": "Moment Co. <support@momentco.ai> (https://momentco.ai)",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/Moment-Co/moment-sdk#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/Moment-Co/moment-sdk.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Moment-Co/moment-sdk/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"calendar",
|
|
18
|
+
"sync",
|
|
19
|
+
"embed",
|
|
20
|
+
"widget",
|
|
21
|
+
"google-calendar",
|
|
22
|
+
"outlook",
|
|
23
|
+
"moment"
|
|
24
|
+
],
|
|
25
|
+
"packageManager": "pnpm@9.15.5",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22.12.0"
|
|
28
|
+
},
|
|
29
|
+
"main": "dist/moment-sdk.js",
|
|
30
|
+
"module": "dist/moment-sdk.mjs",
|
|
31
|
+
"unpkg": "dist/moment-sdk.js",
|
|
32
|
+
"jsdelivr": "dist/moment-sdk.js",
|
|
33
|
+
"types": "dist/types/sdk/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/types/sdk/index.d.ts",
|
|
37
|
+
"import": "./dist/moment-sdk.mjs",
|
|
38
|
+
"default": "./dist/moment-sdk.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"registry": "https://registry.npmjs.org"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "vite build && tsc --emitDeclarationOnly --outDir dist/types",
|
|
50
|
+
"build:dev": "vite build --mode development && tsc --emitDeclarationOnly --outDir dist/types",
|
|
51
|
+
"build:prod": "vite build --mode production && tsc --emitDeclarationOnly --outDir dist/types",
|
|
52
|
+
"type-check": "tsc --noEmit",
|
|
53
|
+
"typecheck": "pnpm run type-check",
|
|
54
|
+
"lint": "eslint .",
|
|
55
|
+
"lint:fix": "eslint . --fix",
|
|
56
|
+
"format": "prettier --write .",
|
|
57
|
+
"format:check": "prettier --check .",
|
|
58
|
+
"quality": "pnpm run type-check && pnpm run lint && pnpm run format",
|
|
59
|
+
"prepare": "husky"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@eslint/js": "^9.25.0",
|
|
63
|
+
"eslint": "^9.25.0",
|
|
64
|
+
"husky": "^9.1.7",
|
|
65
|
+
"prettier": "^3.2.5",
|
|
66
|
+
"typescript": "^5.9.2",
|
|
67
|
+
"typescript-eslint": "^8.32.1",
|
|
68
|
+
"vite": "^7.1.5"
|
|
69
|
+
}
|
|
70
|
+
}
|