diff --git a/.changeset/brown-planes-wait.md b/.changeset/brown-planes-wait.md new file mode 100644 index 0000000..1827d12 --- /dev/null +++ b/.changeset/brown-planes-wait.md @@ -0,0 +1,7 @@ +--- +"@livechat/agent-app-sdk": minor +"@livechat/helpdesk-sdk": minor +"@livechat/widget-core-sdk": minor +--- + +Added `startTransaction` for all widget types to support One-click Payment flow diff --git a/packages/agent-app-sdk/README.md b/packages/agent-app-sdk/README.md index 9b74568..c1b9685 100644 --- a/packages/agent-app-sdk/README.md +++ b/packages/agent-app-sdk/README.md @@ -106,6 +106,85 @@ createDetailsWidget().then(widget => { Each widget type offers a different set of events that you can listen to. Check them out in the descriptions below. +## Payments + +All widgets allow you to pass a registered charge and display a summary of it to the customer within the payment modal in the Agent App application, enabling them to complete or decline the transaction. + +### Events + +#### `transaction_accepted` + +Emitted when a payment transaction is approved by the customer and successfully processed by the Billing API. + +```ts +interface ITransactionAccepted { + chargeId: string; +} +``` + +#### `transaction_declined` + +Emitted when a payment transaction is declined by the customer (e.g., the user closes the payment modal or clicks the cancel button), and the charge is subsequently marked as declined in the Billing API. + +```ts +interface ITransactionDeclined { + chargeId: string; +} +``` +#### `transaction_failed` + +Emitted when a payment transaction fails and cannot be processed by the billing API. + +```ts +interface ITransactionAccepted { + error: unknown; +} +``` + +#### `update_billing_cycle` + +This event is triggered when a customer selects a different billing cycle for a transaction. It only emits if the `showBillingCyclePicker` flag is set to `true` in the `metadata` object at the start of the transaction. The event includes the new billing cycle number and key charge details, allowing you to register the updated charge with the provided information. + +```ts +interface ITransactionAccepted { + billingCycle: number, + paymentIntent: { + name: string, + price: number, + per_account: boolean, + test: boolean, + return_url: string | null, + months?: number, + trial_days?: number, + quantity?: number, + metadata: { + type: string, + isExternalTransaction: boolean, + showBillingCyclePicker: boolean, + icon: string, + description?: string, + } + } +} +``` + +### Methods + +#### `startTransaction(charge: Charge, metadata: Metadata): Promise` + +This method allows you to pass a registered charge and accompanying metadata to the Agent App. The payment modal will then be displayed to the customer, enabling them to complete the transaction. For more information on registering a charge, refer to the [Billing API documentation](https://platform.text.com/docs/monetization/billing-api). + +```ts +const charge = {...} // Billing API charge object +const metadata = { + icon: "https://icon.url"; + description: "This is a description of the transaction."; + showBillingCyclePicker: true; // optional, use if you want to display the billing cycle picker to the customer +} + +widget.startTransaction(charge, metadata); +``` + ## Details widget (`IDetailsWidget`) A type of widget that has access to the Chat Details context. diff --git a/packages/agent-app-sdk/src/index.ts b/packages/agent-app-sdk/src/index.ts index dbb007d..03a44d2 100644 --- a/packages/agent-app-sdk/src/index.ts +++ b/packages/agent-app-sdk/src/index.ts @@ -1,5 +1,7 @@ +export { Charge, IDirectCharge, IRecurrentCharge, Metadata, TransactionError, TransactionEvent, UpdateBillingCycleEvent } from '@livechat/widget-core-sdk'; export * from './widgets/details'; -export * from './widgets/messagebox'; export * from './widgets/fullscreen'; +export * from './widgets/messagebox'; export * from './widgets/settings'; export * from './widgets/shared/customer-profile'; + diff --git a/packages/agent-app-sdk/src/widgets/details/details-widget.ts b/packages/agent-app-sdk/src/widgets/details/details-widget.ts index c4a38a8..632300a 100644 --- a/packages/agent-app-sdk/src/widgets/details/details-widget.ts +++ b/packages/agent-app-sdk/src/widgets/details/details-widget.ts @@ -1,17 +1,9 @@ -import { - createWidget, - withAmplitude, - createConnection, - IConnection -} from '@livechat/widget-core-sdk'; +import { createConnection, createWidget, IConnection, withAmplitude, withPayments } from '@livechat/widget-core-sdk'; import { withCustomerProfile } from '../shared/customer-profile'; import { withRichMessages } from '../shared/rich-messages'; import assertSection from './custom-sections'; -import { - IDetailsWidgetEvents, - IDetailsWidgetApi, - ISection -} from './interfaces'; +import { IDetailsWidgetApi, IDetailsWidgetEvents, ISection } from './interfaces'; + export function DetailsWidget(connection: IConnection) { const base = createWidget( @@ -33,7 +25,7 @@ export function DetailsWidget(connection: IConnection) { } ); - const widget = withAmplitude(withRichMessages(withCustomerProfile(base))); + const widget = withAmplitude(withRichMessages(withCustomerProfile(withPayments(base)))); return widget; } diff --git a/packages/agent-app-sdk/src/widgets/fullscreen/fullscreen-widget.ts b/packages/agent-app-sdk/src/widgets/fullscreen/fullscreen-widget.ts index 3e2dbe4..5ea5e70 100644 --- a/packages/agent-app-sdk/src/widgets/fullscreen/fullscreen-widget.ts +++ b/packages/agent-app-sdk/src/widgets/fullscreen/fullscreen-widget.ts @@ -1,15 +1,6 @@ -import { - createWidget, - withAmplitude, - createConnection, - IConnection -} from '@livechat/widget-core-sdk'; -import { - IFullscreenWidgetApi, - IFullscreenWidgetEvents, - ReportsFilters -} from './interfaces'; +import { createConnection, createWidget, IConnection, withAmplitude, withPayments } from '@livechat/widget-core-sdk'; import { withPageData } from '../shared/page-data'; +import { IFullscreenWidgetApi, IFullscreenWidgetEvents, ReportsFilters } from './interfaces'; export { ReportsFilters } from './interfaces'; @@ -36,7 +27,8 @@ export function FullscreenWidget( } } ); - return withAmplitude(withPageData(base)); + + return withAmplitude(withPageData(withPayments(base))); } export type IFullscreenWidget = ReturnType; diff --git a/packages/agent-app-sdk/src/widgets/messagebox/messagebox-widget.ts b/packages/agent-app-sdk/src/widgets/messagebox/messagebox-widget.ts index f324f0f..20f3781 100644 --- a/packages/agent-app-sdk/src/widgets/messagebox/messagebox-widget.ts +++ b/packages/agent-app-sdk/src/widgets/messagebox/messagebox-widget.ts @@ -1,16 +1,7 @@ -import { - createWidget, - withAmplitude, - createConnection, - IConnection -} from '@livechat/widget-core-sdk'; +import { createConnection, createWidget, IConnection, withAmplitude, withPayments } from '@livechat/widget-core-sdk'; import { withCustomerProfile } from '../shared/customer-profile'; import { withRichMessages } from '../shared/rich-messages'; -import { - IMessageBoxWidgetApi, - IMessageBoxWidgetEvents, - IRichMessage -} from './interfaces'; +import { IMessageBoxWidgetApi, IMessageBoxWidgetEvents, IRichMessage } from './interfaces'; export function MessageBoxWidget( connection: IConnection @@ -30,7 +21,7 @@ export function MessageBoxWidget( } ); - const widget = withAmplitude(withRichMessages(withCustomerProfile(base))); + const widget = withAmplitude(withRichMessages(withCustomerProfile(withPayments(base)))); return widget; } diff --git a/packages/agent-app-sdk/src/widgets/settings/settings-widget.ts b/packages/agent-app-sdk/src/widgets/settings/settings-widget.ts index 1dca67c..79011c3 100644 --- a/packages/agent-app-sdk/src/widgets/settings/settings-widget.ts +++ b/packages/agent-app-sdk/src/widgets/settings/settings-widget.ts @@ -1,11 +1,6 @@ -import { - createWidget, - withAmplitude, - createConnection, - IConnection -} from '@livechat/widget-core-sdk'; -import { ISettingsWidgetApi, ISettingsWidgetEvents } from './interfaces'; +import { createConnection, createWidget, IConnection, withAmplitude, withPayments } from '@livechat/widget-core-sdk'; import { withPageData } from '../shared/page-data'; +import { ISettingsWidgetApi, ISettingsWidgetEvents } from './interfaces'; export function SettingsWidget(connection: IConnection) { const base = createWidget( @@ -16,7 +11,7 @@ export function SettingsWidget(connection: IConnection) { } } ); - return withAmplitude(withPageData(base)); + return withAmplitude(withPageData(withPayments(base))); } export type ISettingsWidget = ReturnType; diff --git a/packages/helpdesk-sdk/README.md b/packages/helpdesk-sdk/README.md index 295c7c6..e41c605 100644 --- a/packages/helpdesk-sdk/README.md +++ b/packages/helpdesk-sdk/README.md @@ -82,6 +82,85 @@ createDetailsWidget().then(widget => { Each widget type offers a different set of events that you can listen to. Check them out in the descriptions below. +## Payments + +All widgets allow you to pass a registered charge and display a summary of it to the customer within the payment modal in the helpdesk application, enabling them to complete or decline the transaction. + +### Events + +#### `transaction_accepted` + +Emitted when a payment transaction is approved by the customer and successfully processed by the Billing API. + +```ts +interface ITransactionAccepted { + chargeId: string; +} +``` + +#### `transaction_declined` + +Emitted when a payment transaction is declined by the customer (e.g., the user closes the payment modal or clicks the cancel button), and the charge is subsequently marked as declined in the Billing API. + +```ts +interface ITransactionDeclined { + chargeId: string; +} +``` +#### `transaction_failed` + +Emitted when a payment transaction fails and cannot be processed by the billing API. + +```ts +interface ITransactionAccepted { + error: unknown; +} +``` + +#### `update_billing_cycle` + +This event is triggered when a customer selects a different billing cycle for a transaction. It only emits if the `showBillingCyclePicker` flag is set to `true` in the `metadata` object at the start of the transaction. The event includes the new billing cycle number and key charge details, allowing you to register the updated charge with the provided information. + +```ts +interface ITransactionAccepted { + billingCycle: number, + paymentIntent: { + name: string, + price: number, + per_account: boolean, + test: boolean, + return_url: string | null, + months?: number, + trial_days?: number, + quantity?: number, + metadata: { + type: string, + isExternalTransaction: boolean, + showBillingCyclePicker: boolean, + icon: string, + description?: string, + } + } +} +``` + +### Methods + +#### `startTransaction(charge: Charge, metadata: Metadata): Promise` + +This method allows you to pass a registered charge and accompanying metadata to the HelpDesk App. The payment modal will then be displayed to the customer, enabling them to complete the transaction. For more information on registering a charge, refer to the [Billing API documentation](https://platform.text.com/docs/monetization/billing-api). + +```ts +const charge = {...} // Billing API charge object +const metadata = { + icon: "https://icon.url"; + description: "This is a description of the transaction."; + showBillingCyclePicker: true; // optional, use if you want to display the billing cycle picker to the customer +} + +widget.startTransaction(charge, metadata); +``` + ## Details widget (`IDetailsWidget`) A type of widget that has access to the Chat Details context. diff --git a/packages/helpdesk-sdk/src/index.ts b/packages/helpdesk-sdk/src/index.ts index 482e6d0..e098d71 100644 --- a/packages/helpdesk-sdk/src/index.ts +++ b/packages/helpdesk-sdk/src/index.ts @@ -1,2 +1,4 @@ -export * from './widgets/fullscreen'; +export { Charge, IDirectCharge, IRecurrentCharge, Metadata, TransactionError, TransactionEvent, UpdateBillingCycleEvent } from '@livechat/widget-core-sdk'; export * from './widgets/details'; +export * from './widgets/fullscreen'; + diff --git a/packages/helpdesk-sdk/src/widgets/details/details-widget.ts b/packages/helpdesk-sdk/src/widgets/details/details-widget.ts index d854b98..d134862 100644 --- a/packages/helpdesk-sdk/src/widgets/details/details-widget.ts +++ b/packages/helpdesk-sdk/src/widgets/details/details-widget.ts @@ -1,16 +1,7 @@ -import { - createWidget, - withAmplitude, - createConnection, - IConnection -} from '@livechat/widget-core-sdk'; -import { withTicketInfo } from './ticket-info'; +import { createConnection, createWidget, IConnection, withAmplitude, withPayments } from '@livechat/widget-core-sdk'; import assertSection from './custom-sections'; -import { - IDetailsWidgetEvents, - IDetailsWidgetApi, - ISection -} from './interfaces'; +import { IDetailsWidgetApi, IDetailsWidgetEvents, ISection } from './interfaces'; +import { withTicketInfo } from './ticket-info'; export function DetailsWidget(connection: IConnection) { const base = createWidget( @@ -23,12 +14,12 @@ export function DetailsWidget(connection: IConnection) { } ); - const widget = withAmplitude(withTicketInfo(base)); + const widget = withAmplitude(withTicketInfo(withPayments(base))); return widget; } -export interface IDetailsWidget extends ReturnType {} +export interface IDetailsWidget extends ReturnType { } export default function createDetailsWidget(): Promise { let widget: IDetailsWidget; diff --git a/packages/helpdesk-sdk/src/widgets/fullscreen/fullscreen-widget.ts b/packages/helpdesk-sdk/src/widgets/fullscreen/fullscreen-widget.ts index 57d8b08..c7beb56 100644 --- a/packages/helpdesk-sdk/src/widgets/fullscreen/fullscreen-widget.ts +++ b/packages/helpdesk-sdk/src/widgets/fullscreen/fullscreen-widget.ts @@ -1,9 +1,4 @@ -import { - createWidget, - withAmplitude, - createConnection, - IConnection -} from '@livechat/widget-core-sdk'; +import { createConnection, createWidget, IConnection, withAmplitude, withPayments } from '@livechat/widget-core-sdk'; import { IFullscreenWidgetApi, IFullscreenWidgetEvents } from './interfaces'; export function FullscreenWidget( @@ -21,11 +16,11 @@ export function FullscreenWidget( } ); - return withAmplitude(base); + return withAmplitude(withPayments(base)); } export interface IFullscreenWidget - extends ReturnType {} + extends ReturnType { } export default function createFullscreenWidget(): Promise { return createConnection().then(connection => diff --git a/packages/widget-core-sdk/src/index.ts b/packages/widget-core-sdk/src/index.ts index e39d4c2..ea019d4 100644 --- a/packages/widget-core-sdk/src/index.ts +++ b/packages/widget-core-sdk/src/index.ts @@ -1,3 +1,5 @@ -export * from './widget'; export * from './amplitude'; export * from './connection'; +export * from './payments'; +export * from './widget'; + diff --git a/packages/widget-core-sdk/src/payments.ts b/packages/widget-core-sdk/src/payments.ts new file mode 100644 index 0000000..63ab128 --- /dev/null +++ b/packages/widget-core-sdk/src/payments.ts @@ -0,0 +1,144 @@ +import { WidgetMixin } from './widget'; + +enum ChargeType { + DirectCharge = "direct_charge", + RecurrentCharge = "recurrent_charge", +} + +enum OutgoingTransactionEvents { + RegisterTransactionPending = 'register_transaction_pending', + RegisterTransactionSuccess = 'register_transaction_success', + RegisterTransactionFailure = 'register_transaction_failure', +} + +interface IPaymentIntent { + name: string, + price: number, + per_account: boolean, + test: boolean, + return_url: string | null, + months?: number, + trial_days?: number, + quantity?: number, + metadata: { + type: ChargeType, + isExternalTransaction: boolean, + showBillingCyclePicker: boolean, + icon: string, + description?: string, + } +} + +export interface IChargeBase { + id: string; + buyer_organization_id: string; + buyer_license_id: number; + buyer_account_id: string; + buyer_entity_id: string; + seller_client_id: string; + order_client_id: string; + order_organization_id: string; + name: string; + price: number; + return_url: string; + test: boolean; + per_account: boolean; + status: string; + confirmation_url: string; + commission_percent: number; + created_at: string; + updated_at: string; +} + +export interface IDirectCharge extends IChargeBase { + quantity: number; +} + +export interface IRecurrentCharge extends IChargeBase { + trial_days: number; + months: number; + external_id?: string; + trial_ends_at: string | null, + cancelled_at: string | null, + current_charge_at: string | null, + next_charge_at: string | null, +} + +export type TransactionEvent = { chargeId: string } +export type TransactionError = { error: unknown } +export type UpdateBillingCycleEvent = { billingCycle: number, paymentIntent: IPaymentIntent, chargeId: string } + +export type Charge = IDirectCharge | IRecurrentCharge; +export type Metadata = { + icon: string; + description?: string; + showBillingCyclePicker?: boolean; +} + +const getChargeType = (charge: Charge): ChargeType => { + return (charge as IRecurrentCharge).months !== undefined ? ChargeType.RecurrentCharge : ChargeType.DirectCharge; +} + +const createPaymentIntent = (charge: Charge, metadata: Metadata): IPaymentIntent => { + const type = getChargeType(charge) + + const base = { + name: charge.name, + price: charge.price, + per_account: charge.per_account, + test: charge.test, + return_url: null, + metadata: { + type, + isExternalTransaction: true, + icon: metadata.icon, + description: metadata.description, + showBillingCyclePicker: metadata.showBillingCyclePicker, + } + } + + return type === ChargeType.RecurrentCharge + ? { ...base, months: (charge as IRecurrentCharge).months, trial_days: (charge as IRecurrentCharge).trial_days } + : { ...base, quantity: (charge as IDirectCharge).quantity } +} + +export interface IWithPaymentsApi { + startTransaction(charge: Charge, metadata: Metadata): void +} + +export interface IWithPaymentsEvents { + transaction_declined: TransactionEvent; + transaction_accepted: TransactionEvent; + transaction_failed: TransactionError; + update_billing_cycle: UpdateBillingCycleEvent; +} + +export const withPayments: WidgetMixin< + IWithPaymentsApi, + IWithPaymentsEvents +> = widget => { + return { + ...widget, + startTransaction(charge: Charge, metadata: Metadata) { + try { + if (!charge) { + throw new Error('You have to provide charge details'); + } + + if (!metadata || !metadata.icon) { + throw new Error('You have to provide metadata with icon'); + } + + // Process the charge to ensure compatibility with OneClickPayment flow + const paymentIntent = createPaymentIntent(charge, metadata) + + // Dispatch events to be handled by the OneClickPayment provider + widget.sendMessage(OutgoingTransactionEvents.RegisterTransactionPending, { paymentIntent }); + widget.sendMessage(OutgoingTransactionEvents.RegisterTransactionSuccess, { charge }); + } catch (error) { + widget.sendMessage(OutgoingTransactionEvents.RegisterTransactionFailure, { error }); + throw error + } + }, + }; +};