facebook-nodejs-business-sdk
Version:
SDK for the Facebook Marketing API in Javascript and Node.js
544 lines (505 loc) • 23.5 kB
JavaScript
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
* @flow
*/
import FacebookAdsApi from '../../api';
import Business from '../business';
import ExtendedCredit from '../extended-credit';
import ExtendedCreditAllocationConfig from '../extended-credit-allocation-config';
import AdAccount from '../ad-account';
import Page from '../page'
/**
* These are accounts owned by a tech provider's business, representing
* a user of the tech provider's application.
*
* This class allows the tech provider to decide which assets are shared from an existing
* facebook user's account/business portfolio (which the user has granted the tech provider access to),
* and which assets are created and owned by the tech provider. This class allows the Tech Provider
* to provide monetization experiences for the third party user using a higher level interface
* than the underlying facebook graph apis.
*
* This class simplifies:
* 1. Creating Assets that are owned by the tech provider (Ad Accounts, Pages, Pixels, etc) .
* 2. Accessing Assets from a users facebook account or business account (Pages, Pixels, etc).
* 3. Combining tech provider owned and user owned assets. (ex: a Product Catalog owned by the tech provider using CAPI events from a user owned dataset)
* 3. Allocating a portion of the tech provider's Credit Line to the account.
* 4. Access Token Usage and Management
* 5. Providing Monetization Services to the third party user
*
* All management operations are performed using the {@link ThirdPartyAccountManager} (e.g. CRUD operations)
*
* Creation:
* - {@link ThirdPartyAccountManager.createAccount()} must be used to create a new ThirdPartyAccount
*
* Loading:
* - {@link ThirdPartyAccountManager.getAccount()} must be used to load an existing ThirdPartyAccount
* - {@link ThirdPartyAccountManager.listAccountsAccounts()} must be used to load existing ThirdPartyAccounts
*
* Deletion:
* - {@link ThirdPartyAccountManager.deleteAccount()} must be used to delete an existing ThirdPartyAccount
*
* Usage:
* - Use `getFacebookPageId()` to retrieve the Facebook Page used for advertising by this account
* - Use `disconnectFacebookPage()` to disconnect the Facebook Page used for advertising by this account
* - Use `updateFacebookPage()` to update the Facebook Page used for advertising by this account
*/
class ThirdPartyAccount {
/**
* The GraphAPI resource id that backs the ThirdPartyAccount, essentially
* acting as a container. Currently the only backing type is Business
*/
accountId: string;
/**
* Delegate used to simplify certain common operations that may require
* the tech provider's business admin system account to perform the operation.
* e.g. generating a new access token for a child when updating the page.
*
* This member simplifies usage of the interfaces for both this class and the Manager class.
* It also greatly simplifies important parts of the implementation.
*
* Dependency Inversion/Injection can be abstracted from the callers because a ThirdPartyAccount
* can only be created by the factory methods in {@link ThirdPartyAccountManager}.
*/
#accountManager: ThirdPartyAccountManager;
/** see {@link ThirdPartyAccount.accessToken} */
#accountToken: string;
constructor(accountId: string, accountManager: ThirdPartyAccountManager, accessToken: string) {
this.accountId = accountId;
this.#accountManager = accountManager;
this.#accountToken = accessToken;
}
/**
* Return the Facebook Page associated with the account.
* This is the Facebook Page that was shared with the account
* during creation. If the page was updated, this will return
* the updated page.
*/
async getFacebookPageId(): Promise<string> {
let api = await this.getApi();
let business = new Business(this.accountId, {api: api});
let adAccounts = await business.getOwnedAdAccounts([AdAccount.Fields.end_advertiser]);
let pageId = adAccounts[0].end_advertiser;
return pageId;
}
/**
* Disconnects the Facebook Page that was previously connected
* to the third party account. Advertising will not be possible
* until a new Facebook Page is connected to the account
*/
async disconnectFacebookPage(): Promise<void> {
let pageId = await this.getFacebookPageId();
if (!pageId) {
return; // don't do anything if no page to disconnect
}
let api = await this.getApi();
let business = new Business(this.accountId, {api: api});
await business.deletePages({
[Page.Fields.id]: pageId,
});
}
/**
* Updates the Facebook Page associated with this account.
*
* This method should be used when a user of the tech provider's application
* wants to change the Facebook Page that is linked to this account.
* If the provided page is the existing page, it no-ops and returns the existing page id.
* If the provided page is a different page, it disconnects the existing page and connects
* the new page to the account.
*
* The method ensures that the new page is properly set up for advertising.
* After updating the page, it refreshes the access token to ensure that the
* account has the correct permissions for the new page.
*
* @param {string} pageId - The ID of the new Facebook Page to be associated with the account.
* @returns {Promise<string>} A promise that resolves to the ID of the updated Facebook Page.
*
* Usage:
* - Use `updateFacebookPage(pageId)` to change the Facebook Page linked to the account.
* - Ensure that the page ID provided is valid and accessible by the tech provider's application
* - For example, having the user grant access to the Page using Facebook Login for Business.
* - If the page is a personal page, the user must grant the tech provider's application
* pages_manage_metadata and pages_read_engagement permissions.
* - If the page is a business page, the user must grant the tech provider's application
* business_management, ads_management, and pages_read_engagement permissions.
* - For more information, see the Facebook Login for Business documentation: https://developers.facebook.com/docs/facebook-login/facebook-login-for-business
*
* @example
*
* ```
* const account = new ThirdPartyAccount(accountId, accountManager, accessToken);
* account.updateFacebookPage('new-page-id')
* .then(updatedPageId => {
* console.log('Page successfully updated. Page ID:', updatedPageId);
* }).catch(error => {
* console.error(error, 'Error updating page');
* });
* ```
*/
async updateFacebookPage(pageId: string): Promise<string> {
let currentPageId = await this.getFacebookPageId();
if (pageId === currentPageId) {
return pageId;
}
await this.disconnectFacebookPage();
let api = await this.getApi();
let backingResource: Business = new Business(this.accountId, { api: api });
let response: any = await backingResource.createClientPage([], {
'page_id': pageId,
'permitted_tasks': ['ADVERTISE', 'ANALYZE'],
});
let updatedPageId = response['id'];
// New token must be generated because access control was updated. Old token is stale and
// does not have access to the new page.
await this.refreshToken();
return updatedPageId;
}
/**
* Refreshed the token that provides access to this account.
* This token is hidden by default to prevent logging or accidentally
* leaking it.
*/
async refreshToken(): Promise<void> {
let accountToken = await this.#accountManager.regenerateAccessToken(this.accountId);
this.#accountToken = accountToken;
}
/**
* Because the token can be periodically refreshed, this method
* is used to prevent the old token from accidentally being used.
* @returns the GraphAPI instance for this account
*/
async getApi(): Promise<FacebookAdsApi> {
let token = this.#accountToken;
return FacebookAdsApi.init(token);
}
}
export type { ThirdPartyAccount };
/**
* Read only view of the third party account
* Returned by listAccounts operations
*/
interface ThirdPartyAccountInfo {
id: string;
name: string;
}
/**
* Page ID to use to back the ads
* This is the identity of the advertiser
*/
type PageProps = {
id: string;
}
/**
* Defines the credit allocation to be
* given to the third party account.
* The tech provider must have a valid Credit Line.
*/
type CreditAllocationProps = {
creditLineId: string;
currencyAmount?: string;
currency?: string;
}
/**
* Properties needed to create the account
* For now, a page is necessary in order to
* create the account. It is used as the identity
* of the advertiser and must be a real Facebook Page
*/
type CreateAccountProps = {
name: string;
page?: PageProps;
creditAllocation?: CreditAllocationProps;
userAccessToken: string;
}
type DeleteAccountProps = {
accountId: string;
}
/**
* ThirdPartyAccountManager is responsible for creating and managing ThirdPartyAccounts
* that represent users of a tech provider's application.
*
* This class provides functionality to:
* 1. Create new third-party accounts with associated assets (Ad Accounts, Pages, Pixels)
* 2. Retrieve existing accounts by ID
* 3. listAccounts all accounts associated with the tech provider's application
* 4. Delete accounts when they are no longer needed
* 5. Manage access tokens for account operations
*
* Each account created through this manager allows the tech provider to:
* - Create assets owned by the tech provider on behalf of the user
* - Access assets shared by the user from their Facebook account
* - Allocate portions of the tech provider's credit line to the user
* - Manage all assets with a single access token
*
* @see ThirdPartyAccount for account usage after creation
*/
export class ThirdPartyAccountManager {
/**
* The ID of the tech provider's application.
*/
#appId: string;
/**
* For now, the backing resource for a ThirdPartyAccount is a Child Business Portfolio.
* This is the ID of the tech provider's business that owns the Child Business Portfolios.
*/
#techProvBusinessId: string;
/**
* The system account token for the tech provider's business. This is used to perform
* operations that require admin access to the business. e.g. creating a new child business.
*/
#systemToken: string;
constructor(appId: string, techProvBusinessId: string, systemAccountToken: string) {
this.#appId = appId;
this.#techProvBusinessId = techProvBusinessId;
this.#systemToken = systemAccountToken;
}
/**
* Creates a third party account under this tech provider's business. This account is used to represent
* a third party user on the tech provider's platform.
*
* This account allows the tech provider's application to:
* 1. Create Assets (Ad Accounts, Pages, Pixels, etc) that are owned by the tech provider.
* 2. Access Assets (Pages, Pixels, etc) from a users facebook account or business account.
* 3. Allocate a portion of the tech provider's Credit Line to the account.
* 4. Use one access token to manage both created and shared assets.
*
* This account allows the tech provider to decide which assets are shared from an existing
* facebook user, and which assets are created and owned by the tech provider. This allows the Tech Provider
* to provide monetization experiences for the third party user using a higher level abstraction without worrying
* about access token management.
*
* @param {CreateAccountProps} props - Configuration for the new account including name, page, pixel, ad account details, and user access token
*
* @returns a {@link Promise<ThirdPartyAccount>} that resolves to the newly created {@link ThirdPartyAccount} t
*
* @example
*
* ```
* const manager = new ThirdPartyAccountManager(appId, partnerBusinessId, partnerAccessToken);
* const account = await manager.createAccount({
* name: 'Client Account',
* page: { id: 'page-id' },
* creditAllocation: { creditLineId: 'credit-line-id', currencyAmount: '1000', currency: 'USD' }
* userAccessToken: 'user-access-token',
* });
* ```
*/
async createAccount(props: CreateAccountProps): Promise<ThirdPartyAccount> {
// Use the user access token if present to create child business, otherwise use parent token
let api = FacebookAdsApi.init(props.userAccessToken);
let partnerBusiness = new Business(this.#techProvBusinessId, {api: api});
// Create the child business to represent the third party
// This is currently the backing resource for the account
let childBusiness: Business = (await partnerBusiness.createOwnedBusiness([],
{
'id': this.#techProvBusinessId,
[Business.Fields.name]: props.name,
[Business.Fields.vertical]: 'OTHER',
'shared_page_id': props.page?.id,
'page_permitted_tasks': ['ADVERTISE', 'ANALYZE'],
'timezone_id': '1',
}
));
// Calls for creating assets in the child business require the parent system user access token
api = FacebookAdsApi.init(this.#systemToken);
// Create a token for the account which is used to manage all assets
childBusiness = new Business(childBusiness.id, {api: api});
let response: any = await childBusiness.createAccessToken([],
{
'id': childBusiness.id,
'app_id': this.#appId,
'scope': 'ads_management,business_management',
}
);
let accountToken = response.exportAllData()['access_token'];
// Share a Line of Credit from your business to the Advertiser
let extendedCredit = new ExtendedCredit(props.creditAllocation?.creditLineId, {api: api});
await extendedCredit.createOwningCreditAllocationConfig([],
{
'id': props.creditAllocation?.creditLineId,
'receiving_business_id': childBusiness.id,
'amount': props.creditAllocation?.currencyAmount,
'partition_type':'FIXED_WITHOUT_PARTITION',
}
).catch(error => {}) // ignore to allow setting up extended credit later and during app development
api = FacebookAdsApi.init(accountToken);
// Get an id from the credit allocation to use for the ad account
childBusiness = new Business(childBusiness.id, {api: api});
let extendedCredits = await childBusiness.getExtendedCredits([ExtendedCredit.Fields.id], {api: api});
let fundingSource = extendedCredits[0];
// Create the ad account
let adAccount = await childBusiness.createAdAccount([],
{
[AdAccount.Fields.name]: props.name,
[AdAccount.Fields.currency]: props.creditAllocation?.currency,
[AdAccount.Fields.timezone_id]: '1',
[AdAccount.Fields.partner]: 'NONE',
[AdAccount.Fields.media_agency]: 'NONE',
[AdAccount.Fields.end_advertiser]: props.page?.id,
'funding_source_id': fundingSource.id,
}
);
// Fetch the system user id from the child business
let systemUsers = await childBusiness.getSystemUsers([]);
let systemUserId = systemUsers[0].id;
// Assign system user to the ad account
await adAccount.createAssignedUser([],
{
'user': systemUserId,
'tasks': 'MANAGE,ADVERTISE,ANALYZE',
[AdAccount.Fields.business]: childBusiness.id,
}
);
return Promise.resolve(new ThirdPartyAccount(childBusiness.id, this, accountToken));
}
/**
* This method returns a {@link ThirdPartyAccount} instance for the given account ID. If an account token
* is provided, it will be used directly. Otherwise, a new token will be generated, which requires
* an additional API call.
*
* @param {string} accountId - The ID of the account to retrieve.
* @param {string} [accountToken] - Optional. The access token for the account. Using this token is recommended to prevent an extra API call.
* @returns {Promise<ThirdPartyAccount>} A promise that resolves to the `ThirdPartyAccount` instance.
*
* Usage:
* - Use `getAccount(accountId, accountToken)` to retrieve a third-party account with the specified ID.
* - If the account token is not provided, the method will generate a new token.
*
* Example:
* ```
* const manager = new ThirdPartyAccountManager(appId, techProvBusinessId, systemAccountToken);
* manager.getAccount('account-id', 'optional-account-token').then(
* account => {
* console.log('Retrieved account:', account);
* }).catch(error => {
* console.error('Error retrieving account', error);
* });
* ```
*/
async getAccount(accountId: string, accountToken?: string): Promise<ThirdPartyAccount> {
if (accountToken) {
return Promise.resolve(new ThirdPartyAccount(accountId, this, accountToken));
}
// if the token isn't passed, we must generate it
let regeneratedToken = await this.regenerateAccessToken(accountId);
return Promise.resolve(new ThirdPartyAccount(accountId, this, regeneratedToken));
}
async regenerateAccessToken(accountId: string): Promise<string> {
let api = FacebookAdsApi.init(this.#systemToken);
// Create a token for the account which is used to manage all assets.
// Currently the accounts backing resource is a Child Business Portfolio.
let childBusiness = new Business(accountId, { api: api });
let response = await childBusiness.createAccessToken([],
{
'id': accountId,
'app_id': this.#appId,
'scope': 'ads_management,business_management',
}
).catch((error) => {
console.log(error);
throw error;
});
return response.exportAllData()['access_token'];
}
/**
* Deletes an account from a tech provider's application and business.
*
* WARNING: This operation is irreversible.
*
* This operation will pause all active campaigns and stop all active ads, then,
* all assets created within the account (Ad Accounts, Catalogs, etc.) will also be deleted,
* and all user owned asset connections will be severed. Finally, the ThirdPartyAccount and backingResource
* will be deleted.
*
* In order to re-create an account for a user, the tech provider's app will have to have a user
* re-authenticate to getAccount a user access token for reconnecting the assets to a new account, as the token
* from the previous login will almost certianly be invalid. User access tokens have a short lifetime.
*
* @param {DeleteAccountProps} props - Object containing the childBusinessId to delete
* @returns {Promise<boolean>} A promise that resolves to true if deletion was successful, false otherwise
*
* Usage:
* - Use `deleteAccount()` when a client relationship ends or when an account needs to be removed
* - This method is useful when a client no longer is spending an the tech provider needs to reclaim
* some of its credit line (i.e. that portion of the credit line that was allocated to the account
* will be reclaimed for use by other accounts)
* - This operation is also useful when testing the integration because a tech provider is limited
* to 2 accounts when an app is in development mode.
* - This operation cannot be undone, and all data associated with the account will be lost
*
* @example
*
* ```
* const manager = new ThirdPartyAccountManager(appId, partnerBusinessId, partnerAccessToken);
* manager.deleteAccount({ childBusinessId: 'business-id-to-delete' }).then(
* success => {
* if (success) {
* console.log('Account successfully deleted');
* } else {
* console.log('Account does not exist');
* }
* }
* ).catch(error => {
* console.error('Error when trying to delete account', error);
* });
* ```
*/
async deleteAccount(props: DeleteAccountProps): Promise<bool> {
let api = FacebookAdsApi.init(this.#systemToken);
let business = new Business(this.#techProvBusinessId, {api: api});
let response = await business.deleteOwnedBusinesses({
'client_id': props.accountId,
}).catch((error) => {
console.log(error);
throw error;
});
return response['success'] ? response['success'] : false;
}
/**
* listAccountss third-party accounts associated with tech provider's app.
*
* By default, this method will return a maximum of 10 accounts.
*
* No matter what max is set, 10 results are returned in each API page.
*
* @returns {Promise<Array<ThirdPartyAccountInfo>>} A promise that resolves to an array of ThirdPartyAccount instances.
*
* Usage:
* - Use `listAccounts()` to listAccounts all third-party accounts managed by the partner business.
*
* @example
*
* ```
* const manager = new ThirdPartyAccountManager(appId, techProvBusinessId, systemAccountToken);
* manager.listAccounts().then(
* accounts => {
* accounts.forEach(account => {
* console.log(account);
* });
* });
* ```
*/
async listAccounts(max: number = 10): Promise<Array<ThirdPartyAccountInfo>> {
let api = FacebookAdsApi.init(this.#systemToken);
let business = new Business(this.#techProvBusinessId, {api: api});
let pageLimit = Math.min(max, 10);
let cursor = await business.getOwnedBusinesses([Business.Fields.id, Business.Fields.name], { limit: pageLimit });
let accounts:Array<ThirdPartyAccountInfo> = [];
// Unfortunately, Cursor does not implement Iteratable
while (cursor && accounts.length < max) {
for (var i = 0; i < cursor.length && accounts.length + i < max; i++) {
let backingResource: any = cursor[i];
let accountInfo: ThirdPartyAccountInfo = {
id: backingResource.id,
name: backingResource['name'],
} ;
accounts.push(accountInfo);
}
cursor = cursor.hasNext() ? await cursor.next() : null;
}
return accounts;
}
}