Web Payments SDK

Migrate to the Web Payments SDK

The SqPaymentForm library is deprecated as of May 13, 2021 and will only receive critical security updates until it is retired.

Your existing payment form code contains elements that can be re-used with the Web Payments SDK (the SDK). This guide gives you code examples from the SqPaymentForm along with code examples from the Web Payments SDK that show how your reusable SqPaymentForm code can be migrated.

Note

This guide does not provide an end-to-end walk through for taking payments with the Web Payments SDK. If that is what you want, see Take a Card Payment with Web Payments SDK

Why migrate your code to Web Payments? Permalink Get a link to this section

If you have not yet decided to migrate your SqPaymentForm application to the Web Payments SDK, consider the following advantages of the SDK:

  • A clear logical separation between payment methods

  • Individual payment methods can be easily wrapped with the UI they need.

  • Reduced object model complexity with Payment methods that support only the APIs they need.

  • Factory method and promise based API rather than configuration object with callbacks.

The code examples in this topic show you how these advantages are gained. In addition to simpler code patterns, the SDK provides the following features:

  • Speed. The SDK is served from a global CDN which decreases page loading time.

  • Additional payment methods. New payment methods include ACH bank transfer and gift cards. Digital wallets are also supported.

  • Dynamic postal code field. You don't need to determine whether your application must provide and validate the buyer postal code. The SDK shows this field based on the buyer card number. Your application code does not make this determination.

  • Typescript Support. The SDK is entirely authored in Typescript, allowing you to import type definitions and use them in your code. Your development can benefit from typed auto-completions in non-typescript projects.

Note

If you must support IE11 before it is retired on June 15, 2022, you will need to use SqPaymentForm with that browser.

In this section, the code patterns used for SqPaymentForm and the SDK are contrasted. The code examples show you how to update your application for the SDK.

Initialization Patterns Permalink Get a link to this section

SqPaymentForm and the SDK must both be initialized before use but are done differently:

  • SqPaymentForm. Uses a single object configuration for one or many payment methods, in the case of gift cards you could only initialize that payment method. If you wanted an additional payment method, you needed to initialize a second instance of SqPaymentForm.

  • Web Payments SDK. There is one pattern for all payment methods. Not every payment method has the same set of APIs, but the core initialization flow is the same for most methods.

    1. Create Payment method

    2. Attach it to your page

    3. Tokenize the buyer's payment

    4. Submit the token to the Payments API

Token response objects Permalink Get a link to this section

The Web Payments SDK returns a single object called a TokenResult, which contains the payment token. It is returned by the tokenize function on a payment method.

SqPaymentForm returns a payment token as a parameter of the cardNonceResponseReceived callback function. Unlike the Web Payments SDK which returns the entire tokenize result in a single object, SqPaymentForm splits the results over multiple objects.

The following sections provide the structures of the SqPaymentForm and Web Payments SDK result objects and points out the places where the structures are different.

SqPaymentForm Permalink Get a link to this section

The cardNonceResponseReceived callback is called with the following parameters:

Errors Permalink Get a link to this section

A collection of field-specific input errors.

{
  "errors": [
    {
      "type": "",
      "message": "",
      "field": ""
    }
  ]
} 

Nonce Permalink Get a link to this section

The payment token generated by the payment form

{
  "nonce": "cnon:card-nonce-ok"
} 

cardData Permalink Get a link to this section

Non-identifying information about the payment card

{
  "cardData": {
    "card_brand": "americanExpress",
    "last_4": "1234",
    "exp_month": "12",
    "billing_postal_code": "98222",
    "digital_wallet_type": "APPLE_PAY" 
  }
}

billingContact Permalink Get a link to this section

The billing address of the buyer

{
    "billingContact": {
      "familyName": "Smith",
      "givenName": "Bill",
      "email": "bsmith@example.com",
      "country": "AU",
      "countryName": "Australia",
      "region": "Queensland",
      "city": "Brisbane",
      "addressLInes": [
        "123 Main St",
        "Unit 5-234"
      ],
      "postalCode": "QLD 4001",
      "phone": "+61 0755 552 222"
    }
}

shippingContact Permalink Get a link to this section

The shipping address of the buyer

{
   "shippingContact": {
      "familyName": "Smith",
      "givenName": "Bill",
      "email": "bsmith@example.com",
      "country": "AU",
      "countryName": "Australia",
      "region": "Queensland",
      "city": "Brisbane",
      "addressLInes": [
        "123 Main St",
        "Unit 5-234"
      ],
      "postalCode": "QLD 4001",
      "phone": "+61 0755 552 222"
   }
}

shippingOption Permalink Get a link to this section

Any shipping options chosen in a digital wallet form

{
  "shippingOption": {
    "id": "123",
    "label": "Next Day Shipping",
    "amount": "12.00"
  }
}

Web Payments SDK Permalink Get a link to this section

Web Payments SDK returns a token in a TokenResult. The object provides the same information as returned by the SqPaymentForm but in a different structure. Structural changes include:

  • The billing address is part of the details.card object and represented by the card.billing field.

  • The SqPaymentForm cardData object is now the details.card object.

  • Shipping contact and shipping options objects are now part of the details.shipping object.

    • country and countryName are now contact.countryCode and shippingContact.countryCode. The name of the country is no longer provided.

    • The address field region is now state.

  • The SqPaymentForm nonce is now a token.

  • There is a new bankAccount object for ACH Bank Transfer payments.

TokenResult Permalink Get a link to this section

{
  "details": {
    "bankAccount": {   
      "accountNumberSuffix": "",
      "accountType": "",
      "bankName": ""
    },
    "card": {
      "billing": {
        "addressLines": [
          "123 Main St",
          "Unit 5-234"
        ],
        "city": "Brisbane",
        "countryCode": "AU",
        "familyName": "Smith",
        "givenName": "Bill",
        "postalCode": "QLD 4001",
        "state": "Queensland"
      },
      "brand": "americanExpress",
      "cardType": "CREDIT",
      "expMonth": 12,
      "expYear": 2021,
      "lastFour": "1234",
      "prepaidType": "NOT_PREPAID"
    },
    "giftCard": {
      "type": ""
    },
    "method": "Apple Pay",
    "shipping": {
      "contact": {
        "addressLines": [
          "123 Main St",
          "Unit 5-234"
        ],
        "city": "Brisbane",
        "countryCode": "AU",
        "email": "bsmith@example.com",
        "familyName": "Smith",
        "givenName": "Bill",
        "phone": "+61 0755 552 222",
        "postalCode": "QLD 4001",
        "state": "Queensland"
      },
      "option": {
        "amount": "12.00",
        "id": "123",
        "label": "Next Day Shipping"
      }
    }
  },
  "errors": {
    "field": "",
    "message": "",
    "type": ""
  },
  "status": "OK",
  "token": "cnon:card-nonce-ok"
}

Card form styling Permalink Get a link to this section

You can give payment card entry forms a custom style to match the other pages in your application. The styling mechanism in the SDK is different than SqPaymentForm but the colors and fonts that you used in SqPaymentForm can be used in the SDK.

SqPaymentForm Permalink Get a link to this section

  • Styling values are part of the global configuration object or required external CSS for positioning. Supported keys are documented in InputStyle objects.

 inputStyle:
   {
     //Set font attributes on card entry fields
     fontSize: '16px',
     fontWeight: 500,
     fontFamily: 'futura',
     placeholderFontWeight: 300,
     autoFillColor: '#FFFFFF',    //Sets color of card nbr & exp. date
     color: '#FFFFFF',            //Sets color of CVV & Zip
     placeholderColor: '#A5A5A5', //Sets placeholder text color
     //Set form appearance
     backgroundColor: '#000',  //Card entry background color
     cardIconColor: '#A5A5A5', //Card Icon color
     borderRadius: '50px',
     boxShadow: "0px 2px 6px rgba(0,0,0,.02)," +
       "0px 4px 8px rgba(0,0,0, 0.04), 0px 8px 30px " +
       "rgba(0,0,0, 0.04), 0px 1px 2px rgba(0,0,0, 0.08)",
     //Set form appearance in error state
     error:
     {
       cardIconColor: '#FF1ADA', //Sets color of card icon
       color: '#FF4DB8',         //Sets color of card entry text
       backgroundColor: '#000',  //Card entry background color
       fontWeight: 500,
       fontFamily: 'futura'      //Font of the input field in error
     },
     //Set appearance of hint text below form
     details:
     {
       hidden: false,    //Shows or hides hint text
       color: '#A5A5A5', //Sets hint text color
       fontSize: '12px', //Not inherited from parent, Sets size of
       //text, defaults to 12px
       fontWeight: 500,  //Not inherited from parent
       fontFamily: 'futura', //Not inherited from parent, required to render form
       error:
       { //Sets attributes of hint text in when form
         color: '#FF4DB8', //is in error state
         fontSize: '12px'
       }
     }
   }
 }   

Web Payments SDK Permalink Get a link to this section

The Web Payments SDK credit card element is fully customizable by using the CardClassSelectors object that holds your style values. That object can be provided as an argument to the Payments.card or Card.configure methods.

Note

The Web Payments SDK provides a single credit card element for all credit card inputs. We no longer support the positioning of individual credit card fields. Square has found the single credit card element results in a higher conversion to payment.

 const darkModeCardStyle = {
   '.input-container': {
     borderColor: '#2D2D2D',
     borderRadius: '6px',
   },
   '.input-container.is-focus': {
     borderColor: '#006AFF',
   },
   '.input-container.is-error': {
     borderColor: '#ff1600',
   },
   '.message-text': {
     color: '#999999',
   },
   '.message-icon': {
     color: '#999999',
   },
   '.message-text.is-error': {
     color: '#ff1600',
   },
   '.message-icon.is-error': {
     color: '#ff1600',
   },
   input: {
     backgroundColor: '#2D2D2D',
     color: '#FFFFFF',
     fontFamily: 'helvetica neue, sans-serif',
   },
   'input::placeholder': {
     color: '#999999',
   },
   'input.is-error': {
     color: '#ff1600',
   },
 };

 async function initializeCard(payments) {
   const card = await payments.card({
     style: darkModeCardStyle,
   });
   await card.attach('#card-container');
   return card;
 }      

Google Pay styling Permalink Get a link to this section

The Web Payments SDK has simplified the styling of the Google Pay button without losing the flexibility to customize the button within Google Pay style guidelines.

web-payments : migration : googlepay image

SqPaymentForm Permalink Get a link to this section

In the SqPaymentForm, you set the dimensions of an HTML button and provided the Google Pay button image using an .svg string and then were required to set all aspects of the button style.

/* Indicates how Google Pay button will appear */
.button-google-pay {
  min-width: 200px;
  min-height: 40px;
  padding: 11px 24px;
  margin: 10px;
  background-color: #000;
  background-image: url(data:image/svg+xml,%3Csvg%20width%3D%22103%22%20height%3D%2217%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M.148%202.976h3.766c.532%200%201.024.117%201....C%2Fsvg%3E);
  background-origin: content-box;
  background-position: center;
  background-repeat: no-repeat;
  background-size: contain;
  border: 0;
  border-radius: 4px;
  box-shadow: 0 1px 1px 0 rgba(60, 64, 67, 0.30), 0 1px 3px 1px rgba(60, 64, 67, 0.15);
  outline: 0;
  cursor: pointer;
  display: none;
}

Note

The .svg data in the previous example is truncated for clarity.

Web Payments SDK Permalink Get a link to this section

With the Web Payments SDK, all you need to do is attach the GooglePay payment method to your DOM. You can optionally set the button color to white instead of the default black. You may also set the width of the button.

googlePay.attach('#googlePay', { buttonColor: 'default', buttonType: 'long' });

Apple Pay styling Permalink Get a link to this section

Apple Pay styling has remained mostly the same as done with SqPaymentForm. Developers needed to include their own CSS for styling the Apple Pay button. With the Web Payments SDK, this is still needed. See the Apple Pay walkthrough for information about integrating Apple Pay using the Web Payments SDK.

Digital wallet payment requests Permalink Get a link to this section

Payment request objects in the SDK are essentially the same as they are in SqPaymentForm. Two object keys have changed as follows:

  • requestShippingAddress is now requestShippingContact

  • requestBillingInfo is now requestBillingContact

The request payment objects in the SDK are initialized in a different way than in SqPaymentForm

SqPaymentForm Permalink Get a link to this section

Initialize the payment request object in a callback declared in the configuration object:

createPaymentRequest: function () {
   var paymentRequestJson = {
     requestShippingAddress: true,
     requestBillingInfo: true,
     shippingContact: {
       familyName: "CUSTOMER LAST NAME", 
       givenName: "CUSTOMER FIRST NAME",
       email: "mycustomer@example.com",
       country: "USA",
       region: "CA",
       city: "San Francisco",
       addressLines: [
         "1455 Market St #600"
       ],
       postalCode: "94103",
       phone:"14255551212"
     },
     currencyCode: "USD",
     countryCode: "US",
     total: {
       label: "MERCHANT NAME",
       amount: "85.00",
       pending: false
     },
     lineItems: [
       {
         label: "Subtotal",
         amount: "60.00",
         pending: false
       },
       {
         label: "Shipping",
         amount: "19.50",
         pending: true
       },
       {
         label: "Tax",
         amount: "5.50",
         pending: false
       }
     ],
     shippingOptions: [
       {
         id: 'shipping-option-1',
         label: 'Free',
         amount: '0.00'
       },
       {
         amount: '10.00',
         id: 'shipping-option-2',
         label: 'Expedited'
       }
    ]
   };

   return paymentRequestJson;
 },

Web Payments SDK Permalink Get a link to this section

Initialize a payment request object by calling a function on the Payments object:

function buildPaymentRequest(payments) {

   let lineItems = [
     {amount: '60.00', label: 'subtotal', pending: 'false'},
     {amount: '19.50', label: 'Shipping', pending: 'true'},
     {amount: '5.50', label: 'Tax', pending: 'false'},
   ];

   const defaultShippingContact = {
       familyName: "CUSTOMER LAST NAME",
       givenName: "CUSTOMER FIRST NAME",
       email: "mycustomer@example.com",
       country: "USA",
       region: "CA",
       city: "San Francisco",
       addressLines: [
         "1455 Market St #600"
       ],
       postalCode: "94103",
       phone:"14255551212"
     };

   const defaultShippingOptions = [
     {
         id: 'shipping-option-1',
         label: 'Free',
         amount: '0.00'
     },
     {
         amount: '10.00',
         id: 'shipping-option-2',
         label: 'Expedited',
     },
   ];

   //calculateTotal is a helper method that sums the lineItems and 
   //returns a new LineItem with the total amount.
   let total = calculateTotal(lineItems);

   const paymentRequestDetails = {
     countryCode: 'US',
     currencyCode: 'USD',
     lineItems,
     requestBillingContact: true,
     requestShippingContact: true,
     shippingOptions: defaultShippingOptions,
     shippingContact: defaultShippingContact,
     total,
   };

   const req = payments.paymentRequest(paymentRequestDetails);
   return req;
 }

Digital wallet shipping events Permalink Get a link to this section

Shipping contact and shipping option update notifications:

SqPaymentForm Permalink Get a link to this section

Notifications were handled by a part of the callbacks section of the SqPaymentForm configuration object. These callbacks required calling the Done callback parameter to mark the action as complete.

callbacks: {
 ...
 shippingOptionChanged: function(shippingOption, done) {
   const newLineItems = getNewLineItems(shippingOption);
   const newTotal = {
     label: "MERCHANT NAME",
     amount: calculateNewTotal(shippingOption),
     pending: false
   };
   done({
     lineItems: newLineItems,
     total: newTotal
   });
 }
shippingContactChanged: function (shippingContact, done) {
   var valid = true;
   var shippingErrors = {};

   if (!shippingContact.postalCode) {
     shippingErrors.postalCode = "postal code is required";
     valid = false;
   }

   if (!valid) {
     done({shippingContactErrors: shippingErrors});
     return;
   }

   // Shipping address unserviceable.
   if (shippingContact.country !== 'US' || shippingContact.country !== 'CA') {
     done({error: 'Shipping to outside of the U.S. and Canada is not available.'});
     return;
   }

   // Update total, lineItems, and shippingOptions for Canadian address.
   if (shippingContact.country === 'CA') {
     const tax = 1000 * 0.08;
     done({
       total: {
         label: "MERCHANT NAME",
         amount: (1000 + tax).toFixed(2),
         pending: true
       },
       lineItems: [
         {
           label: "Subtotal",
           amount: "1000.00",
           pending: false
         },
         {
           label: "Tax",
           amount: tax.toFixed(2),
           pending: false
         }
       ],
       shippingOptions: [
         {
           id: "1",
           label: "International Shipping",
           amount: "20.00"
         }
       ]
     });
     return;
   }

 // Everything looks good and nothing to update
   done();
 }

Web Payments SDK Permalink Get a link to this section

The SDK uses event listeners to handle the notifications. Returning the updated line items is all that is required to finish handling the notification.

 req.addEventListener('shippingoptionchanged', (option) => {
   // Add your business logic here.
   // This gives you the shipping option the buyer selected and allows
   // you to update totals based on the price of each shipping option.
   const newLineItems = updateOrAddLineItems(lineItems, {
     Shipping: option.amount,
   });
   const total = calculateTotal(newLineItems);

   // Update the line items so that they can be referenced later in other
   // eventListener calls.
   lineItems = newLineItems;

   return {
     lineItems,
     total,
   };
 });

 // newAmountByLabel will be in the format of { labelName: amount }, e.g.
 // { 'Shipping':'10.00', 'Tax':'3.00' }
 function updateOrAddLineItems(currentLineItems, newAmountsByLabel) {
   // A list  of which newAmounts labels exist in the current line items
   const updatedLineItem = new Set();

   const newLineItems = currentLineItems.map((lineItem) => {
     updatedLineItem.add(lineItem.label)
     if (newAmountsByLabel[lineItem.label] !== undefined) {
       return Object.assign({}, lineItem, {
           amount: newAmountsByLabel[lineItem.label] });
     }
     return lineItem;
   });

   Object.entries(newAmountsByLabel).forEach(([label, amount]) => {
     if (!updatedLineItem.has(label)) {
       newLineItems.push({ label, amount, pending: false });
     }
   });  
   return newLineItems;
 }

Other events Permalink Get a link to this section

Payment cards, GiftCards, and digital wallets emit events driven by user input, letting you perform dynamic page updates in response to changes in the fields.

With the SqPaymentForm, developers add a callback function to the SqPaymentForm global configuration object which handled every event that the card form emitted on any user interaction. With the SDK, the developer uses the Card addEventListner function to subscribe to events such as cardBrandChanged.

SqPaymentForm Permalink Get a link to this section

This callback example is invoked when a buyer types the first four characters of a card number. The card brand is either set or changed.

 var paymentForm = new SqPaymentForm({
   // Initialize the payment form elements
   applicationId: "{REPLACE ME WITH YOUR APPLICATION ID}",
   inputClass: 'sq-input',
   // SqPaymentForm callback functions
   callbacks: {
     /*
     * callback function: inputEventReceived
     * Triggered when: visitors interact with SqPaymentForm iframe elements.
     */
     inputEventReceived: function (inputEvent) {
       switch (inputEvent.eventType) {
         case 'cardBrandChanged':
           alert(`Card brand changed to ${cardInputEvent.cardBrand}`);
           break;
       }
     }
   }
 });

Web Payments SDK Permalink Get a link to this section

The cardBrandChanged event is handled with a function definition and a single call to the Card addEventListner function.

 const payments = window.Square.payments();
 const card = await payments.card();
 const cardChangedCallback = async (cardInputEvent) => {
   alert(`Card brand changed to ${cardInputEvent.detail.cardBrand}`);
 };
 card.addEventListener("cardBrandChanged", cardChangedCallback);
 card.addEventListener(“errorClassAdded”, errorClassAddedCallback);

In the Web Payments SDK, payment card event handling is contained in the Card payment method. Each event can be handled with separate functions, allowing for code isolation. To add a second event listener, define a function to handle the event and call the addEventListner function with the event you are interested in and the function you defined for the event.

Strong Customer Authentication Permalink Get a link to this section

In the SqPaymentForm and the Web Payments SDK, a payment details object is declared and a verifyBuyer function is called when a payment token is returned. With the SDK, the verifyBuyer function is called on the Card object instead of on SqPaymentForm itself.

SqPaymentForm Permalink Get a link to this section

  1. Create an object that holds the relevant details of the payment

       const verificationDetails = { 
         intent: 'CHARGE', 
         amount: '1.00', 
         currencyCode: 'USD', 
         billingContact: {
           givenName: 'Jane',
           familyName: 'Doe'
         }
       };    
    
  2. Call the verifyBuyer function after the buyer has keyed in their card information and a payment token has been returned in the cardNonceResponseReceived callback.

     const cardPaymentForm = new SqPaymentForm({
       // Initialize the payment form elements
       // … other PaymentForm configuration options.  
       callbacks: {
           /*
           * callback function: cardNonceResponseReceived
           * Triggered when: SqPaymentForm completes a card nonce request
           */
           cardNonceResponseReceived: (errors, nonce, cardData) => {
               cardPaymentForm.verifyBuyer(nonce, 
                 verificationDetails, 
                 (err, verificationResult) => processPayment(nonce, verificationResult.token));
           }
       }
     });
    

Web Payments SDK Permalink Get a link to this section

Declare a helper function that takes the new payment token as an argument. Inside of the helper, create an object that holds the relevant details of the payment. Call the helper function after Card tokenize has returned the new token.

const payments = window.Square.payments();
const card = payments.card();

async function verifyBuyerHelper(paymentToken) {
  const verificationDetails = {
    amount: '1.00',
    /* collected from the buyer */
    billingContact: {
      addressLines: ['123 Main Street', 'Apartment 1'],
      familyName: 'Doe',
      givenName: 'John',
      email: 'jondoe@gmail.com',
      country: 'GB',
      phone: '3214563987',
      region: 'LND',
      city: 'London',
    },
    currencyCode: 'GBP',
    intent: 'CHARGE',
  };

  const verificationResults = await payments.verifyBuyer(
    token,
    verificationDetails
  );
  return verificationResults.token;
}
// tokenize the card form to get a token
const paymentToken = card.tokenize(); 

// use the generated nonce to verifyBuyer
const verificationToken = verifyBuyerHelper(paymentToken); 

// call your backend with nonce and verification token
processPayment(token, verificationToken);

Simple card migration example Permalink Get a link to this section

The following examples show a complete implementation of card payments in SqPaymentForm and in the Web Payments SDK

SqPaymentForm Permalink Get a link to this section

<script type="text/javascript">

     // Create and initialize a payment form object
     const paymentForm = new SqPaymentForm({
       // Initialize the payment form elements

       //TODO: Replace with your sandbox application ID
       applicationId: "{APPLICATION_ID}",
       inputClass: 'sq-input',
       // SqPaymentForm callback functions
       callbacks: {
           /*
           * callback function: cardNonceResponseReceived
           * Triggered when: SqPaymentForm completes a card nonce request
           */
           cardNonceResponseReceived: function (errors, nonce, cardData) {
           if (errors) {
               // Log errors from nonce generation to the browser developer console.
               console.error('Encountered errors:');
               errors.forEach(function (error) {
                   console.error('  ' + error.message);
               });
               alert('Encountered errors, check browser developer console for more details');
                return;
           }
           alert(`The generated nonce is:\n${nonce}`);
        }
      }
    });
   </script>

Web Payments SDK Permalink Get a link to this section

This is sufficient code to actually take a payment. For a complete example that demonstrates best programming practices, see Take a Card Payment with Web Payments SDK .


const payments = Square.payments({APPLICATION_ID}, {LOCATION_ID})
const darkModeCardStyle = {
  '.input-container': {
    borderColor: '#2D2D2D',
    borderRadius: '6px',
  },
  '.input-container.is-focus': {
    borderColor: '#006AFF',
  },
  '.input-container.is-error': {
    borderColor: '#ff1600',
  },
  '.message-text': {
    color: '#999999',
  },
  '.message-icon': {
    color: '#999999',
  },
  '.message-text.is-error': {
    color: '#ff1600',
  },
  '.message-icon.is-error': {
    color: '#ff1600',
  },
  input: {
    backgroundColor: '#2D2D2D',
    color: '#FFFFFF',
    fontFamily: 'helvetica neue, sans-serif',
  },
  'input::placeholder': {
    color: '#999999',
  },
  'input.is-error': {
    color: '#ff1600',
  },
};
const card = await payments.card({
  styles: darkModeStyle
});
card.attach("#your-element");
let tokenResult;

const button = document.getElementById('your-element');
button.addEventListener(‘click’, e => {
  e.preventDefault();
  tokenResult = card.tokenize();
});

Related topics Permalink Get a link to this section