@teleporthq/teleport-plugin-next-workflows 0.43.75 → 0.44.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.
Files changed (248) hide show
  1. package/__tests__/_helpers/fake-store-database.ts +428 -0
  2. package/__tests__/_helpers/invoice-store.ts +382 -0
  3. package/__tests__/booking-calendar-feed-route.test.ts +494 -0
  4. package/__tests__/custom-node-result-taken-branch.test.ts +107 -0
  5. package/__tests__/data-api-date-parser.test.ts +45 -0
  6. package/__tests__/email-locale-senders.test.ts +3 -1
  7. package/__tests__/email-sender-name.test.ts +513 -0
  8. package/__tests__/graph-utils-topological-order.test.ts +155 -1
  9. package/__tests__/invoice-billing-details.test.ts +384 -0
  10. package/__tests__/invoice-data-access-replace.test.ts +236 -2
  11. package/__tests__/invoice-order-items-settle.test.ts +1 -1
  12. package/__tests__/invoice-payment-email.test.ts +191 -0
  13. package/__tests__/invoice-regenerate-email.test.ts +288 -0
  14. package/__tests__/invoice-regenerate-route.test.ts +9 -7
  15. package/__tests__/invoice-reservation.test.ts +1 -1
  16. package/__tests__/invoice-tender.test.ts +1 -1
  17. package/__tests__/middleware-admin-panel-roles.test.ts +127 -0
  18. package/__tests__/page-arrival-remount.test.ts +247 -0
  19. package/__tests__/payment-drivers-module.test.ts +19 -0
  20. package/__tests__/raw-query-error-code.test.ts +47 -0
  21. package/__tests__/runtime-template-token-resolution.test.ts +16 -0
  22. package/__tests__/segment-reply-changed-keys.test.ts +31 -0
  23. package/__tests__/segment-route-skipped-branch.test.ts +101 -0
  24. package/__tests__/segment-splitter-checkout-order.test.ts +306 -0
  25. package/__tests__/streaming-on-end-env-filter.test.ts +18 -18
  26. package/__tests__/streaming-on-end-order.test.ts +387 -0
  27. package/dist/cjs/account-delete-route-generator.d.ts +1 -0
  28. package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
  29. package/dist/cjs/account-delete-route-generator.js +2 -1
  30. package/dist/cjs/account-delete-route-generator.js.map +1 -1
  31. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  32. package/dist/cjs/api-route-generator.js +23 -13
  33. package/dist/cjs/api-route-generator.js.map +1 -1
  34. package/dist/cjs/auth-generator.d.ts +1 -0
  35. package/dist/cjs/auth-generator.d.ts.map +1 -1
  36. package/dist/cjs/auth-generator.js +77 -28
  37. package/dist/cjs/auth-generator.js.map +1 -1
  38. package/dist/cjs/booking-calendar-feed-route-generator.d.ts +28 -0
  39. package/dist/cjs/booking-calendar-feed-route-generator.d.ts.map +1 -0
  40. package/dist/cjs/booking-calendar-feed-route-generator.js +296 -0
  41. package/dist/cjs/booking-calendar-feed-route-generator.js.map +1 -0
  42. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  43. package/dist/cjs/data-api-route-generator.js +6 -1
  44. package/dist/cjs/data-api-route-generator.js.map +1 -1
  45. package/dist/cjs/executor-generator.d.ts.map +1 -1
  46. package/dist/cjs/executor-generator.js +39 -28
  47. package/dist/cjs/executor-generator.js.map +1 -1
  48. package/dist/cjs/graph-utils.d.ts +31 -1
  49. package/dist/cjs/graph-utils.d.ts.map +1 -1
  50. package/dist/cjs/graph-utils.js +112 -10
  51. package/dist/cjs/graph-utils.js.map +1 -1
  52. package/dist/cjs/index.d.ts +1 -1
  53. package/dist/cjs/index.d.ts.map +1 -1
  54. package/dist/cjs/index.js +2 -1
  55. package/dist/cjs/index.js.map +1 -1
  56. package/dist/cjs/invoice/api-routes-code.d.ts +12 -3
  57. package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
  58. package/dist/cjs/invoice/api-routes-code.js +152 -21
  59. package/dist/cjs/invoice/api-routes-code.js.map +1 -1
  60. package/dist/cjs/invoice/data-access-code.d.ts.map +1 -1
  61. package/dist/cjs/invoice/data-access-code.js +133 -21
  62. package/dist/cjs/invoice/data-access-code.js.map +1 -1
  63. package/dist/cjs/invoice/email-sender-code.d.ts.map +1 -1
  64. package/dist/cjs/invoice/email-sender-code.js +15 -25
  65. package/dist/cjs/invoice/email-sender-code.js.map +1 -1
  66. package/dist/cjs/invoice/index.js +1 -1
  67. package/dist/cjs/invoice/index.js.map +1 -1
  68. package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
  69. package/dist/cjs/invoice/invoice-assembly-code.js +50 -6
  70. package/dist/cjs/invoice/invoice-assembly-code.js.map +1 -1
  71. package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
  72. package/dist/cjs/invoice/invoice-html-code.js +13 -2
  73. package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
  74. package/dist/cjs/nodes/data/data-raw-query.d.ts.map +1 -1
  75. package/dist/cjs/nodes/data/data-raw-query.js +11 -1
  76. package/dist/cjs/nodes/data/data-raw-query.js.map +1 -1
  77. package/dist/cjs/nodes/email/email-mailgun.d.ts.map +1 -1
  78. package/dist/cjs/nodes/email/email-mailgun.js +4 -2
  79. package/dist/cjs/nodes/email/email-mailgun.js.map +1 -1
  80. package/dist/cjs/nodes/email/email-postmark.d.ts.map +1 -1
  81. package/dist/cjs/nodes/email/email-postmark.js +5 -2
  82. package/dist/cjs/nodes/email/email-postmark.js.map +1 -1
  83. package/dist/cjs/nodes/email/email-resend.d.ts.map +1 -1
  84. package/dist/cjs/nodes/email/email-resend.js +5 -2
  85. package/dist/cjs/nodes/email/email-resend.js.map +1 -1
  86. package/dist/cjs/nodes/email/email-sendgrid.d.ts.map +1 -1
  87. package/dist/cjs/nodes/email/email-sendgrid.js +8 -2
  88. package/dist/cjs/nodes/email/email-sendgrid.js.map +1 -1
  89. package/dist/cjs/nodes/general/general-custom-js.d.ts.map +1 -1
  90. package/dist/cjs/nodes/general/general-custom-js.js +11 -0
  91. package/dist/cjs/nodes/general/general-custom-js.js.map +1 -1
  92. package/dist/cjs/nodes/storage/storage-local-remove.d.ts.map +1 -1
  93. package/dist/cjs/nodes/storage/storage-local-remove.js +2 -0
  94. package/dist/cjs/nodes/storage/storage-local-remove.js.map +1 -1
  95. package/dist/cjs/nodes/storage/storage-local-set.d.ts.map +1 -1
  96. package/dist/cjs/nodes/storage/storage-local-set.js +4 -0
  97. package/dist/cjs/nodes/storage/storage-local-set.js.map +1 -1
  98. package/dist/cjs/page-arrival.d.ts +10 -0
  99. package/dist/cjs/page-arrival.d.ts.map +1 -0
  100. package/dist/cjs/page-arrival.js +86 -0
  101. package/dist/cjs/page-arrival.js.map +1 -0
  102. package/dist/cjs/payments/index.d.ts +10 -4
  103. package/dist/cjs/payments/index.d.ts.map +1 -1
  104. package/dist/cjs/payments/index.js +18 -7
  105. package/dist/cjs/payments/index.js.map +1 -1
  106. package/dist/cjs/segment-splitter.d.ts.map +1 -1
  107. package/dist/cjs/segment-splitter.js +65 -27
  108. package/dist/cjs/segment-splitter.js.map +1 -1
  109. package/dist/cjs/transactional-email-code.d.ts.map +1 -1
  110. package/dist/cjs/transactional-email-code.js +32 -18
  111. package/dist/cjs/transactional-email-code.js.map +1 -1
  112. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  113. package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
  114. package/dist/cjs/workflow-component-plugin.js +35 -149
  115. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  116. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  117. package/dist/cjs/workflow-project-plugin.js +16 -4
  118. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  119. package/dist/cjs/workflow-scope.d.ts +19 -0
  120. package/dist/cjs/workflow-scope.d.ts.map +1 -0
  121. package/dist/cjs/workflow-scope.js +147 -0
  122. package/dist/cjs/workflow-scope.js.map +1 -0
  123. package/dist/esm/account-delete-route-generator.d.ts +1 -0
  124. package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
  125. package/dist/esm/account-delete-route-generator.js +2 -1
  126. package/dist/esm/account-delete-route-generator.js.map +1 -1
  127. package/dist/esm/api-route-generator.d.ts.map +1 -1
  128. package/dist/esm/api-route-generator.js +23 -13
  129. package/dist/esm/api-route-generator.js.map +1 -1
  130. package/dist/esm/auth-generator.d.ts +1 -0
  131. package/dist/esm/auth-generator.d.ts.map +1 -1
  132. package/dist/esm/auth-generator.js +77 -28
  133. package/dist/esm/auth-generator.js.map +1 -1
  134. package/dist/esm/booking-calendar-feed-route-generator.d.ts +28 -0
  135. package/dist/esm/booking-calendar-feed-route-generator.d.ts.map +1 -0
  136. package/dist/esm/booking-calendar-feed-route-generator.js +291 -0
  137. package/dist/esm/booking-calendar-feed-route-generator.js.map +1 -0
  138. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  139. package/dist/esm/data-api-route-generator.js +6 -1
  140. package/dist/esm/data-api-route-generator.js.map +1 -1
  141. package/dist/esm/executor-generator.d.ts.map +1 -1
  142. package/dist/esm/executor-generator.js +39 -28
  143. package/dist/esm/executor-generator.js.map +1 -1
  144. package/dist/esm/graph-utils.d.ts +31 -1
  145. package/dist/esm/graph-utils.d.ts.map +1 -1
  146. package/dist/esm/graph-utils.js +109 -9
  147. package/dist/esm/graph-utils.js.map +1 -1
  148. package/dist/esm/index.d.ts +1 -1
  149. package/dist/esm/index.d.ts.map +1 -1
  150. package/dist/esm/index.js +1 -1
  151. package/dist/esm/index.js.map +1 -1
  152. package/dist/esm/invoice/api-routes-code.d.ts +12 -3
  153. package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
  154. package/dist/esm/invoice/api-routes-code.js +152 -21
  155. package/dist/esm/invoice/api-routes-code.js.map +1 -1
  156. package/dist/esm/invoice/data-access-code.d.ts.map +1 -1
  157. package/dist/esm/invoice/data-access-code.js +133 -21
  158. package/dist/esm/invoice/data-access-code.js.map +1 -1
  159. package/dist/esm/invoice/email-sender-code.d.ts.map +1 -1
  160. package/dist/esm/invoice/email-sender-code.js +15 -25
  161. package/dist/esm/invoice/email-sender-code.js.map +1 -1
  162. package/dist/esm/invoice/index.js +1 -1
  163. package/dist/esm/invoice/index.js.map +1 -1
  164. package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
  165. package/dist/esm/invoice/invoice-assembly-code.js +50 -6
  166. package/dist/esm/invoice/invoice-assembly-code.js.map +1 -1
  167. package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
  168. package/dist/esm/invoice/invoice-html-code.js +13 -2
  169. package/dist/esm/invoice/invoice-html-code.js.map +1 -1
  170. package/dist/esm/nodes/data/data-raw-query.d.ts.map +1 -1
  171. package/dist/esm/nodes/data/data-raw-query.js +11 -1
  172. package/dist/esm/nodes/data/data-raw-query.js.map +1 -1
  173. package/dist/esm/nodes/email/email-mailgun.d.ts.map +1 -1
  174. package/dist/esm/nodes/email/email-mailgun.js +4 -2
  175. package/dist/esm/nodes/email/email-mailgun.js.map +1 -1
  176. package/dist/esm/nodes/email/email-postmark.d.ts.map +1 -1
  177. package/dist/esm/nodes/email/email-postmark.js +5 -2
  178. package/dist/esm/nodes/email/email-postmark.js.map +1 -1
  179. package/dist/esm/nodes/email/email-resend.d.ts.map +1 -1
  180. package/dist/esm/nodes/email/email-resend.js +5 -2
  181. package/dist/esm/nodes/email/email-resend.js.map +1 -1
  182. package/dist/esm/nodes/email/email-sendgrid.d.ts.map +1 -1
  183. package/dist/esm/nodes/email/email-sendgrid.js +8 -2
  184. package/dist/esm/nodes/email/email-sendgrid.js.map +1 -1
  185. package/dist/esm/nodes/general/general-custom-js.d.ts.map +1 -1
  186. package/dist/esm/nodes/general/general-custom-js.js +11 -0
  187. package/dist/esm/nodes/general/general-custom-js.js.map +1 -1
  188. package/dist/esm/nodes/storage/storage-local-remove.d.ts.map +1 -1
  189. package/dist/esm/nodes/storage/storage-local-remove.js +2 -0
  190. package/dist/esm/nodes/storage/storage-local-remove.js.map +1 -1
  191. package/dist/esm/nodes/storage/storage-local-set.d.ts.map +1 -1
  192. package/dist/esm/nodes/storage/storage-local-set.js +4 -0
  193. package/dist/esm/nodes/storage/storage-local-set.js.map +1 -1
  194. package/dist/esm/page-arrival.d.ts +10 -0
  195. package/dist/esm/page-arrival.d.ts.map +1 -0
  196. package/dist/esm/page-arrival.js +81 -0
  197. package/dist/esm/page-arrival.js.map +1 -0
  198. package/dist/esm/payments/index.d.ts +10 -4
  199. package/dist/esm/payments/index.d.ts.map +1 -1
  200. package/dist/esm/payments/index.js +16 -6
  201. package/dist/esm/payments/index.js.map +1 -1
  202. package/dist/esm/segment-splitter.d.ts.map +1 -1
  203. package/dist/esm/segment-splitter.js +66 -28
  204. package/dist/esm/segment-splitter.js.map +1 -1
  205. package/dist/esm/transactional-email-code.d.ts.map +1 -1
  206. package/dist/esm/transactional-email-code.js +32 -18
  207. package/dist/esm/transactional-email-code.js.map +1 -1
  208. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  209. package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
  210. package/dist/esm/workflow-component-plugin.js +32 -146
  211. package/dist/esm/workflow-component-plugin.js.map +1 -1
  212. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  213. package/dist/esm/workflow-project-plugin.js +16 -4
  214. package/dist/esm/workflow-project-plugin.js.map +1 -1
  215. package/dist/esm/workflow-scope.d.ts +19 -0
  216. package/dist/esm/workflow-scope.d.ts.map +1 -0
  217. package/dist/esm/workflow-scope.js +142 -0
  218. package/dist/esm/workflow-scope.js.map +1 -0
  219. package/package.json +4 -4
  220. package/src/account-delete-route-generator.ts +3 -1
  221. package/src/api-route-generator.ts +23 -13
  222. package/src/auth-generator.ts +83 -26
  223. package/src/booking-calendar-feed-route-generator.ts +293 -0
  224. package/src/data-api-route-generator.ts +6 -1
  225. package/src/executor-generator.ts +39 -28
  226. package/src/graph-utils.ts +143 -11
  227. package/src/index.ts +1 -0
  228. package/src/invoice/api-routes-code.ts +166 -21
  229. package/src/invoice/data-access-code.ts +139 -21
  230. package/src/invoice/email-sender-code.ts +15 -25
  231. package/src/invoice/index.ts +1 -1
  232. package/src/invoice/invoice-assembly-code.ts +50 -6
  233. package/src/invoice/invoice-html-code.ts +13 -2
  234. package/src/nodes/data/data-raw-query.ts +11 -1
  235. package/src/nodes/email/email-mailgun.ts +11 -2
  236. package/src/nodes/email/email-postmark.ts +12 -2
  237. package/src/nodes/email/email-resend.ts +12 -2
  238. package/src/nodes/email/email-sendgrid.ts +15 -2
  239. package/src/nodes/general/general-custom-js.ts +10 -0
  240. package/src/nodes/storage/storage-local-remove.ts +4 -0
  241. package/src/nodes/storage/storage-local-set.ts +6 -0
  242. package/src/page-arrival.ts +115 -0
  243. package/src/payments/index.ts +18 -5
  244. package/src/segment-splitter.ts +90 -36
  245. package/src/transactional-email-code.ts +32 -21
  246. package/src/workflow-component-plugin.ts +43 -188
  247. package/src/workflow-project-plugin.ts +17 -4
  248. package/src/workflow-scope.ts +184 -0
@@ -1,10 +1,14 @@
1
1
  import { NodeHandlerGenerator, handlerToString } from '../types'
2
2
 
3
+ // Same-tab notice of the removal, as storage-local-set gives for a write.
3
4
  async function storage_local_remove(config: any, context: Record<string, unknown>) {
4
5
  const key = config.key
5
6
 
6
7
  try {
7
8
  localStorage.removeItem(key)
9
+ window.dispatchEvent(
10
+ new CustomEvent('teleport:local-storage', { detail: { key, value: null } })
11
+ )
8
12
  return { success: true }
9
13
  } catch (err: unknown) {
10
14
  return { success: false, error: (err as Error).message }
@@ -1,5 +1,8 @@
1
1
  import { NodeHandlerGenerator, handlerToString } from '../types'
2
2
 
3
+ // A write in the visitor's own tab fires no `storage` event. This one lets page
4
+ // scripts react at once: Google consent mode is granted by the cookie banner's
5
+ // Accept, which is this step (teleport-project-generator-next google-consent-mode).
3
6
  async function storage_local_set(config: any, context: Record<string, unknown>) {
4
7
  const key = config.key
5
8
  const value = config.value
@@ -7,6 +10,9 @@ async function storage_local_set(config: any, context: Record<string, unknown>)
7
10
  try {
8
11
  const serialized = typeof value === 'string' ? value : JSON.stringify(value)
9
12
  localStorage.setItem(key, serialized)
13
+ window.dispatchEvent(
14
+ new CustomEvent('teleport:local-storage', { detail: { key, value: serialized } })
15
+ )
10
16
  return { success: true }
11
17
  } catch (err: unknown) {
12
18
  return { success: false, error: (err as Error).message }
@@ -0,0 +1,115 @@
1
+ import {
2
+ ComponentUIDL,
3
+ UIDLElement,
4
+ UIDLNode,
5
+ UIDLWorkflow,
6
+ UIDLWorkflows,
7
+ } from '@teleporthq/teleport-types'
8
+ import { UIDLUtils } from '@teleporthq/teleport-shared'
9
+ import { getRelevantWorkflows, resolveLifecycleTriggerElementId } from './workflow-scope'
10
+
11
+ /**
12
+ * Lifecycle triggers that fire because the page appeared. They run when the
13
+ * page or component carrying them mounts, so the page has to remount on every
14
+ * arrival (see `PageArrival.remountPageOnArrival`).
15
+ */
16
+ const PAGE_ARRIVAL_TRIGGER_TYPES = new Set([
17
+ 'event-page-loaded',
18
+ 'event-element-visible',
19
+ 'event-component-loaded',
20
+ ])
21
+
22
+ export const isPageArrivalWorkflow = (workflow: UIDLWorkflow): boolean =>
23
+ PAGE_ARRIVAL_TRIGGER_TYPES.has(workflow.trigger.type)
24
+
25
+ /** Every static `id` in a node tree — the ids an element-visible trigger can observe. */
26
+ const collectStaticElementIds = (node: UIDLNode): Set<string> => {
27
+ const ids = new Set<string>()
28
+ UIDLUtils.traverseElements(node, (element) => {
29
+ const id = element.attrs?.id
30
+ if (id?.type === 'static' && typeof id.content === 'string') {
31
+ ids.add(id.content)
32
+ }
33
+ })
34
+ return ids
35
+ }
36
+
37
+ /**
38
+ * The project component a node renders. Local dependencies are resolved before
39
+ * the pages are generated, which rewrites `semanticType` to the component's
40
+ * class name — the original key still matches a component that was not renamed.
41
+ */
42
+ const findRenderedComponent = (
43
+ element: UIDLElement,
44
+ projectComponents: Record<string, ComponentUIDL>
45
+ ): ComponentUIDL | undefined => {
46
+ if (element.dependency?.type !== 'local') {
47
+ return undefined
48
+ }
49
+ const reference = element.semanticType || element.elementType
50
+ return (
51
+ projectComponents[reference] ||
52
+ Object.values(projectComponents).find(
53
+ (component) => UIDLUtils.getComponentClassName(component) === reference
54
+ )
55
+ )
56
+ }
57
+
58
+ /** The components a node tree renders, directly or through other components. */
59
+ const collectRenderedComponents = (
60
+ node: UIDLNode,
61
+ projectComponents: Record<string, ComponentUIDL>,
62
+ rendered: Set<ComponentUIDL>
63
+ ): Set<ComponentUIDL> => {
64
+ UIDLUtils.traverseElements(node, (element) => {
65
+ const component = findRenderedComponent(element, projectComponents)
66
+ if (component && !rendered.has(component)) {
67
+ rendered.add(component)
68
+ collectRenderedComponents(component.node, projectComponents, rendered)
69
+ }
70
+ })
71
+ return rendered
72
+ }
73
+
74
+ /**
75
+ * Mirrors what the workflow plugin emits into the component: the workflows it
76
+ * finds relevant, with an element-visible trigger kept only when the observed
77
+ * element is rendered by the component itself.
78
+ */
79
+ const componentRunsOnArrival = (component: ComponentUIDL, workflows: UIDLWorkflows): boolean => {
80
+ const arrivalWorkflows = getRelevantWorkflows(workflows, component, false).filter(
81
+ isPageArrivalWorkflow
82
+ )
83
+ if (arrivalWorkflows.length === 0) {
84
+ return false
85
+ }
86
+ const elementIds = collectStaticElementIds(component.node)
87
+ return arrivalWorkflows.some((workflow) => {
88
+ const targetId = resolveLifecycleTriggerElementId(
89
+ workflow.trigger.type,
90
+ (workflow.trigger.config || {}) as Record<string, unknown>
91
+ )
92
+ return (
93
+ workflow.trigger.type !== 'event-element-visible' ||
94
+ (targetId !== null && elementIds.has(targetId))
95
+ )
96
+ })
97
+ }
98
+
99
+ /**
100
+ * True when a component the page renders runs a workflow when it appears — an
101
+ * element-visible trigger on one of its elements, a component-loaded trigger.
102
+ * The component mounts again only when the page around it does, so such a page
103
+ * remounts on every arrival even when it has no arrival trigger of its own.
104
+ */
105
+ export const pageRendersArrivalComponent = (
106
+ pageNode: UIDLNode,
107
+ workflows: UIDLWorkflows,
108
+ projectComponents?: Record<string, ComponentUIDL>
109
+ ): boolean => {
110
+ if (!projectComponents) {
111
+ return false
112
+ }
113
+ const rendered = collectRenderedComponents(pageNode, projectComponents, new Set())
114
+ return Array.from(rendered).some((component) => componentRunsOnArrival(component, workflows))
115
+ }
@@ -2,6 +2,7 @@ import {
2
2
  FileType,
3
3
  ProjectPluginStructure,
4
4
  ProjectUIDL,
5
+ UIDLEcommercePaymentProvider,
5
6
  UIDLWorkflows,
6
7
  } from '@teleporthq/teleport-types'
7
8
  import { generatePaymentCoreCode } from './payment-core-code'
@@ -63,6 +64,18 @@ export const isPaymentWebhookConfig = (webhookConfig: unknown): boolean =>
63
64
  ((webhookConfig || {}) as { signatureAlgorithm?: unknown }).signatureAlgorithm ===
64
65
  PAYMENT_DRIVER_SIGNATURE_ALGORITHM
65
66
 
67
+ /**
68
+ * The providers the site is paid through: the store's (`ecommerceSettings`),
69
+ * or — on a site that takes payments without a store, for its bookings —
70
+ * `paymentSettings`'.
71
+ */
72
+ export const readUidlPaymentProviders = (
73
+ uidl: Pick<ProjectUIDL, 'ecommerceSettings' | 'paymentSettings'> | undefined
74
+ ): UIDLEcommercePaymentProvider[] =>
75
+ (uidl?.ecommerceSettings
76
+ ? uidl.ecommerceSettings.paymentProviders
77
+ : uidl?.paymentSettings?.paymentProviders) || []
78
+
66
79
  /**
67
80
  * The providers this store needs a driver for: every provider its checkout
68
81
  * offers, every provider a payment webhook verifies through, and Stripe +
@@ -71,11 +84,11 @@ export const isPaymentWebhookConfig = (webhookConfig: unknown): boolean =>
71
84
  * payment, in which case no driver module is written.
72
85
  */
73
86
  export const resolveStorePaymentDriverIds = (
74
- uidl: Pick<ProjectUIDL, 'ecommerceSettings'> & { workflows?: UIDLWorkflows },
87
+ uidl: Pick<ProjectUIDL, 'ecommerceSettings' | 'paymentSettings'> & { workflows?: UIDLWorkflows },
75
88
  usedNodeTypes: Iterable<string>
76
89
  ): PaymentDriverId[] => {
77
90
  const wanted = new Set<PaymentDriverId>()
78
- for (const provider of uidl.ecommerceSettings?.paymentProviders || []) {
91
+ for (const provider of readUidlPaymentProviders(uidl)) {
79
92
  const id = String(provider?.type || '').toLowerCase()
80
93
  if (isPaymentDriverId(id)) {
81
94
  wanted.add(id)
@@ -98,13 +111,13 @@ const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
98
111
 
99
112
  /**
100
113
  * The secret the merchant saved each live provider credential under, as the
101
- * editor exported it (`ecommerceSettings.paymentProviders[].credentials`).
114
+ * editor exported it (`paymentProviders[].credentials` of either block).
102
115
  */
103
116
  export const readDeclaredPaymentCredentials = (
104
- uidl: Pick<ProjectUIDL, 'ecommerceSettings'> | undefined
117
+ uidl: Pick<ProjectUIDL, 'ecommerceSettings' | 'paymentSettings'> | undefined
105
118
  ): DeclaredPaymentCredentials => {
106
119
  const declared: DeclaredPaymentCredentials = {}
107
- for (const provider of uidl?.ecommerceSettings?.paymentProviders || []) {
120
+ for (const provider of readUidlPaymentProviders(uidl)) {
108
121
  const id = String(provider?.type || '').toLowerCase()
109
122
  if (!isPaymentDriverId(id)) {
110
123
  continue
@@ -1,6 +1,6 @@
1
1
  import { UIDLWorkflow, UIDLWorkflowNode, UIDLWorkflowEdge } from '@teleporthq/teleport-types'
2
2
  import { WorkflowSegment, WorkflowExecutionEnv } from './types'
3
- import { getLoopBodyNodes, getNodeById, getTopologicalOrder } from './graph-utils'
3
+ import { getDependents, getLoopBodyNodes, getNodeById, getTopologicalOrder } from './graph-utils'
4
4
  import { nodeRegistry } from './nodes'
5
5
  import { AWAIT_RESULT_CONFIG_KEY } from './await-result'
6
6
  import { WELCOME_EMAIL_CONFIG_KEYS } from './transactional-email-code'
@@ -144,25 +144,54 @@ export const splitIntoSegments = (workflow: UIDLWorkflow): WorkflowSegment[] =>
144
144
  ? workflow.edges.filter((e) => !errorBranchIds.has(e.source) && !errorBranchIds.has(e.target))
145
145
  : workflow.edges
146
146
 
147
- const order = getTopologicalOrder(mainNodes, mainEdges)
148
147
  // Loop bodies must execute as a single unit at runtime. The split-by-env
149
148
  // walk below would otherwise drop AI/server steps in one segment and the
150
149
  // client-only post-processing into another, leaving the loop's body half-
151
- // executed for every iteration. We reorder the topo so all body nodes sit
152
- // immediately after their owning loop, and we record a single forced env
153
- // (server if any body or the loop itself needs server, else client) so the
154
- // forthcoming env walk emits one segment for the loop + body.
155
- const { reorderedOrder, forcedEnvByNodeId } = enforceLoopBodyIntegrity(
156
- order,
150
+ // executed for every iteration. Every loop + body gets a single forced env
151
+ // (server if any body or the loop itself needs server, else client), and the
152
+ // topo order is rearranged so all body nodes sit immediately after their
153
+ // owning loop, so the env walk emits one segment for the loop + body.
154
+ const forcedEnvByNodeId = resolveLoopClusterEnvs(mainNodes, mainEdges)
155
+ const order = getTopologicalOrder(
157
156
  mainNodes,
158
- mainEdges
157
+ mainEdges,
158
+ (node) => forcedEnvByNodeId.get(node.id) ?? resolveFixedExecutionEnv(node)
159
159
  )
160
+ const reorderedOrder = hoistLoopBodies(order, mainNodes, mainEdges)
160
161
  const segments: WorkflowSegment[] = []
161
162
  let currentEnv: WorkflowExecutionEnv | null = null
162
163
  let currentNodeIds: string[] = []
163
164
  let segClientCount = 0
164
165
  let segServerCount = 0
165
166
 
167
+ // A server route runs its segment's nodes by stepNumber, never by edge, so a
168
+ // node must not join a segment holding one of its inputs at a later step (a
169
+ // join also reached by a shorter path, a value read across branches): the
170
+ // route would run it first. Loop bodies run inside their loop, at the loop's
171
+ // step, and never leave its segment.
172
+ const inputsById = new Map<string, string[]>()
173
+ Object.entries(getDependents(mainNodes, mainEdges)).forEach(([source, dependents]) =>
174
+ dependents.forEach((id) => inputsById.set(id, [...(inputsById.get(id) || []), source]))
175
+ )
176
+ const loopStepByBodyNodeId = new Map<string, number>()
177
+ mainNodes
178
+ .filter((n) => n.type === 'general-loop')
179
+ .forEach((loop) =>
180
+ getLoopBodyNodes(loop.id, mainEdges).forEach((id) =>
181
+ loopStepByBodyNodeId.set(
182
+ id,
183
+ Math.min(loopStepByBodyNodeId.get(id) ?? Infinity, loop.stepNumber || 0)
184
+ )
185
+ )
186
+ )
187
+ const runsAtStep = (nodeId: string): number =>
188
+ loopStepByBodyNodeId.get(nodeId) ?? (getNodeById(mainNodes, nodeId)?.stepNumber || 0)
189
+ const readsALaterStepOfThisSegment = (node: UIDLWorkflowNode): boolean =>
190
+ !loopStepByBodyNodeId.has(node.id) &&
191
+ (inputsById.get(node.id) || []).some(
192
+ (inputId) => currentNodeIds.includes(inputId) && runsAtStep(inputId) > (node.stepNumber || 0)
193
+ )
194
+
166
195
  const flushSegment = () => {
167
196
  if (currentNodeIds.length === 0 || currentEnv === null) {
168
197
  return
@@ -196,7 +225,10 @@ export const splitIntoSegments = (workflow: UIDLWorkflow): WorkflowSegment[] =>
196
225
  const forced = forcedEnvByNodeId.get(nodeId)
197
226
  const nodeEnv = forced ?? resolveExecutionEnv(node, currentEnv)
198
227
 
199
- if (currentEnv !== null && nodeEnv !== currentEnv) {
228
+ if (
229
+ currentEnv !== null &&
230
+ (nodeEnv !== currentEnv || (nodeEnv === 'server' && readsALaterStepOfThisSegment(node)))
231
+ ) {
200
232
  flushSegment()
201
233
  }
202
234
  currentEnv = nodeEnv
@@ -208,10 +240,7 @@ export const splitIntoSegments = (workflow: UIDLWorkflow): WorkflowSegment[] =>
208
240
  }
209
241
 
210
242
  /**
211
- * Re-arrange the topological node order so every loop's body nodes sit
212
- * directly after the loop, and pick a single execution env for the
213
- * (loop + body) cluster. Nested loops are processed inner-first so the outer
214
- * loop's body window already contains the inner body in the right order.
243
+ * The single execution env of every (loop + body) cluster.
215
244
  *
216
245
  * Why an explicit env override is needed: a body that mixes universal /
217
246
  * server / client nodes (e.g. fetch → AI prompt → custom-js with
@@ -224,19 +253,58 @@ export const splitIntoSegments = (workflow: UIDLWorkflow): WorkflowSegment[] =>
224
253
  * runs the same string of JS regardless of where it executes); the inverse
225
254
  * is not true for AI / HTTP-with-secret nodes. When no member is server, we
226
255
  * keep the cluster on the client so DOM- or window-bound bodies still work.
256
+ * A node of nested loops takes its outermost loop's env, which is server
257
+ * whenever an inner loop's is.
227
258
  */
228
- const enforceLoopBodyIntegrity = (
229
- order: string[],
259
+ const resolveLoopClusterEnvs = (
230
260
  mainNodes: UIDLWorkflowNode[],
231
261
  mainEdges: UIDLWorkflowEdge[]
232
- ): {
233
- reorderedOrder: string[]
234
- forcedEnvByNodeId: Map<string, WorkflowExecutionEnv>
235
- } => {
262
+ ): Map<string, WorkflowExecutionEnv> => {
236
263
  const forcedEnvByNodeId = new Map<string, WorkflowExecutionEnv>()
264
+ for (const loop of mainNodes.filter((n) => n.type === 'general-loop')) {
265
+ const bodyIds = getLoopBodyNodes(loop.id, mainEdges)
266
+ if (bodyIds.length === 0) {
267
+ continue
268
+ }
269
+ const clusterMembers = [loop.id, ...bodyIds]
270
+ .map((id) => getNodeById(mainNodes, id))
271
+ .filter((n): n is UIDLWorkflowNode => Boolean(n))
272
+ const clusterEnv: WorkflowExecutionEnv = clusterMembers.some(
273
+ (member) => resolveExecutionEnv(member, null) === 'server'
274
+ )
275
+ ? 'server'
276
+ : 'client'
277
+ for (const member of clusterMembers) {
278
+ if (forcedEnvByNodeId.get(member.id) !== 'server') {
279
+ forcedEnvByNodeId.set(member.id, clusterEnv)
280
+ }
281
+ }
282
+ }
283
+ return forcedEnvByNodeId
284
+ }
285
+
286
+ /**
287
+ * Where a node runs whatever ran before it, or `null` for a node that takes the
288
+ * env of the node before it (an if-statement): it fits either side.
289
+ */
290
+ const resolveFixedExecutionEnv = (node: UIDLWorkflowNode): WorkflowExecutionEnv | null => {
291
+ const env = resolveExecutionEnv(node, 'server')
292
+ return env === resolveExecutionEnv(node, 'client') ? env : null
293
+ }
294
+
295
+ /**
296
+ * Re-arrange the topological node order so every loop's body nodes sit
297
+ * directly after the loop. Nested loops are processed inner-first so the outer
298
+ * loop's body window already contains the inner body in the right order.
299
+ */
300
+ const hoistLoopBodies = (
301
+ order: string[],
302
+ mainNodes: UIDLWorkflowNode[],
303
+ mainEdges: UIDLWorkflowEdge[]
304
+ ): string[] => {
237
305
  const loopNodes = mainNodes.filter((n) => n.type === 'general-loop')
238
306
  if (loopNodes.length === 0) {
239
- return { reorderedOrder: order, forcedEnvByNodeId }
307
+ return order
240
308
  }
241
309
 
242
310
  // Process inner loops before outer ones: an inner loop's body is a subset
@@ -253,20 +321,6 @@ const enforceLoopBodyIntegrity = (
253
321
  }
254
322
 
255
323
  const clusterIds = new Set<string>([loopId, ...bodyIds])
256
- const clusterMembers = Array.from(clusterIds)
257
- .map((id) => getNodeById(mainNodes, id))
258
- .filter((n): n is UIDLWorkflowNode => Boolean(n))
259
-
260
- let clusterEnv: WorkflowExecutionEnv = 'client'
261
- for (const member of clusterMembers) {
262
- if (resolveExecutionEnv(member, null) === 'server') {
263
- clusterEnv = 'server'
264
- break
265
- }
266
- }
267
- for (const member of clusterMembers) {
268
- forcedEnvByNodeId.set(member.id, clusterEnv)
269
- }
270
324
 
271
325
  // Hoist body nodes (preserving their existing relative order) immediately
272
326
  // after the loop. Other nodes keep their relative position.
@@ -283,7 +337,7 @@ const enforceLoopBodyIntegrity = (
283
337
  ]
284
338
  }
285
339
 
286
- return { reorderedOrder: working, forcedEnvByNodeId }
340
+ return working
287
341
  }
288
342
 
289
343
  /**
@@ -5,6 +5,7 @@
5
5
  * every route stays byte-for-byte consistent.
6
6
  */
7
7
  import { UIDLLocalizedEmailTemplates } from '@teleporthq/teleport-types'
8
+ import { EmailSender } from '@teleporthq/teleport-shared'
8
9
  import { getEmailProviderDependencies } from './invoice/email-sender-code'
9
10
  import { EMAIL_LOCALE_FILE_NAME, EMAIL_LOCALE_PATH } from './email-locale/email-locale-scope'
10
11
 
@@ -39,24 +40,28 @@ export interface TransactionalEmailLedger {
39
40
  relativePrefix: string
40
41
  }
41
42
 
42
- // Provider-specific `__sendProviderEmail({ from, to, subject, html, apiKey,
43
- // tokenValues?, userId? })`. No attachment (unlike the invoice sender). When
44
- // no provider is configured the function is a no-op so the route stays
45
- // self-contained and never throws. With `ledger`, the provider function is
46
- // emitted as `__sendProviderEmailRaw` and `__sendProviderEmail` records every
47
- // attempt (success or throw) before returning / rethrowing.
43
+ // Provider-specific `__sendProviderEmail({ from, fromName?, to, subject, html,
44
+ // apiKey, tokenValues?, userId? })`, preceded by the `__emailSender` helper that
45
+ // names the sender. No attachment (unlike the invoice sender). When no provider
46
+ // is configured the function is a no-op so the route stays self-contained and
47
+ // never throws. With `ledger`, the provider function is emitted as
48
+ // `__sendProviderEmailRaw` and `__sendProviderEmail` records every attempt
49
+ // (success or throw) before returning / rethrowing.
48
50
  export const generateProviderSendFunction = (
49
51
  provider?: string | null,
50
52
  ledger?: TransactionalEmailLedger
51
53
  ): string => {
52
54
  const raw = generateRawProviderSendFunction(provider)
53
- if (!ledger || !provider || !SUPPORTED_EMAIL_PROVIDERS.has(provider)) {
55
+ if (!provider || !SUPPORTED_EMAIL_PROVIDERS.has(provider)) {
54
56
  return raw
55
57
  }
56
- return `${raw.replace(
57
- 'async function __sendProviderEmail(',
58
- 'async function __sendProviderEmailRaw('
59
- )}
58
+ const senderHelper = EmailSender.generateEmailSenderHelperCode()
59
+ if (!ledger) {
60
+ return `${senderHelper}
61
+ ${raw}`
62
+ }
63
+ return `${senderHelper}
64
+ ${raw.replace('async function __sendProviderEmail(', 'async function __sendProviderEmailRaw(')}
60
65
 
61
66
  var __sentEmailLog = require('${ledger.relativePrefix}/utils/email/sent-email-log');
62
67
 
@@ -67,12 +72,14 @@ async function __sendProviderEmail(msg) {
67
72
  } catch (err) {
68
73
  failure = err;
69
74
  }
75
+ var sender = __emailSender({ from: msg.from, name: msg.fromName });
70
76
  // Recorded in the background; the route settles the ledger before it replies.
71
77
  __sentEmailLog.recordSentEmail({
72
78
  emailType: ${JSON.stringify(ledger.emailType)},
73
79
  audience: 'customer',
74
80
  to: msg.to,
75
- from: msg.from,
81
+ from: sender.email,
82
+ fromName: sender.name,
76
83
  subject: msg.subject,
77
84
  html: msg.html,
78
85
  payload: msg.tokenValues,
@@ -93,20 +100,26 @@ const generateRawProviderSendFunction = (provider?: string | null): string => {
93
100
  return `async function __sendProviderEmail(msg) {
94
101
  var Resend = require('resend').Resend;
95
102
  var resend = new Resend(msg.apiKey);
96
- var result = await resend.emails.send({ from: msg.from, to: [msg.to], subject: msg.subject, html: msg.html });
103
+ var sender = __emailSender({ from: msg.from, name: msg.fromName });
104
+ var result = await resend.emails.send({ from: sender.header, to: [msg.to], subject: msg.subject, html: msg.html });
97
105
  if (result && result.error) { throw new Error(result.error.message || 'Failed to send email'); }
98
106
  }`
99
107
  case 'sendgrid':
108
+ // SendGrid reads a "Name <email>" string's name verbatim, quotes included,
109
+ // so a named sender goes as its two parts.
100
110
  return `async function __sendProviderEmail(msg) {
101
111
  var sgMail = require('@sendgrid/mail');
102
112
  sgMail.setApiKey(msg.apiKey);
103
- await sgMail.send({ to: msg.to, from: msg.from, subject: msg.subject, html: msg.html });
113
+ var sender = __emailSender({ from: msg.from, name: msg.fromName });
114
+ var from = sender.name ? { email: sender.email, name: sender.name } : sender.email;
115
+ await sgMail.send({ to: msg.to, from: from, subject: msg.subject, html: msg.html });
104
116
  }`
105
117
  case 'postmark':
106
118
  return `async function __sendProviderEmail(msg) {
107
119
  var postmark = require('postmark');
108
120
  var client = new postmark.ServerClient(msg.apiKey);
109
- await client.sendEmail({ From: msg.from, To: msg.to, Subject: msg.subject, HtmlBody: msg.html });
121
+ var sender = __emailSender({ from: msg.from, name: msg.fromName });
122
+ await client.sendEmail({ From: sender.header, To: msg.to, Subject: msg.subject, HtmlBody: msg.html });
110
123
  }`
111
124
  case 'mailgun':
112
125
  return `async function __sendProviderEmail(msg) {
@@ -116,7 +129,8 @@ const generateRawProviderSendFunction = (provider?: string | null): string => {
116
129
  if (!domain) { throw new Error('Mailgun domain not configured'); }
117
130
  var mailgun = new Mailgun(formData);
118
131
  var mg = mailgun.client({ username: 'api', key: msg.apiKey });
119
- await mg.messages.create(domain, { from: msg.from, to: [msg.to], subject: msg.subject, html: msg.html });
132
+ var sender = __emailSender({ from: msg.from, name: msg.fromName });
133
+ await mg.messages.create(domain, { from: sender.header, to: [msg.to], subject: msg.subject, html: msg.html });
120
134
  }`
121
135
  case 'mailersend':
122
136
  return `async function __sendProviderEmail(msg) {
@@ -125,11 +139,8 @@ const generateRawProviderSendFunction = (provider?: string | null): string => {
125
139
  var Sender = require('mailersend').Sender;
126
140
  var Recipient = require('mailersend').Recipient;
127
141
  var mailerSend = new MailerSend({ apiKey: msg.apiKey });
128
- var fromStr = String(msg.from || '').trim();
129
- var m = /^(.*)<([^<>]+)>\\s*$/.exec(fromStr);
130
- var fromEmail = m ? m[2].trim() : fromStr;
131
- var fromName = m ? m[1].trim() : '';
132
- var sender = new Sender(fromEmail, fromName || fromEmail);
142
+ var from = __emailSender({ from: msg.from, name: msg.fromName });
143
+ var sender = new Sender(from.email, from.name || from.email);
133
144
  var params = new EmailParams().setFrom(sender).setTo([new Recipient(msg.to)]).setSubject(msg.subject).setHtml(msg.html);
134
145
  await mailerSend.email.send(params);
135
146
  }`