Loop Crypto Custom Integration

Prerequisites

Install the required dependencies:

npm install @getopenpay/openpay-js-react @loop-crypto/connect

Example

import { 
  ElementsForm, 
  FieldName, 
  SubmitMethods 
} from '@getopenpay/openpay-js-react';
import { initLoopConnect, LoopConnectPayIn } from '@loop-crypto/connect';
import { useState, useEffect } from 'react';

function LoopCheckoutForm() {
  const [error, setError] = useState<string | undefined>(undefined);
  const [amount, setAmount] = useState<string | undefined>(undefined);
  const [isLoopInitialized, setIsLoopInitialized] = useState(false);
  const [createdCustomer, setCreatedCustomer] = useState(null);
  const [createdPaymentMethod, setCreatedPaymentMethod] = useState(null);
  const [isAuthorized, setIsAuthorized] = useState(false);

  const onLoad = (totalAmountAtoms: number) => {
    setAmount(`$${totalAmountAtoms / 100}`);
  };

  const onCheckoutError = (message: string) => {
    setError(message);
  };

  const onCheckoutSuccess = (invoiceUrls: string[]) => {
    console.log('Payment successful! Invoice URLs:', invoiceUrls);
  };

  return (
    <ElementsForm
      checkoutSecureToken={`checkout-secure-token-uuid`}
      onChange={() => setError(undefined)}
      onLoad={onLoad}
      onValidationError={(message) => setError(message)}
      onCheckoutSuccess={onCheckoutSuccess}
      onCheckoutError={onCheckoutError}
    >
      {({ submitWith, loop }) => {
        useEffect(() => {
          if (loop?.isAvailable && loop.config) {
            initLoopConnect({
              jwtToken: loop.config.apiToken,
              entityId: loop.config.entityId,
              merchantId: loop.config.merchantId,
              environment: loop.config.environment,
              onInitialized: () => setIsLoopInitialized(true),
              onInitFailed: (detail) => {
                submitWith(SubmitMethods.loopCrypto, {
                  success: false,
                  failureDetails: detail,
                });
              },
            });
          }
        }, [loop]);

        if (!loop?.isAvailable) {
          return <div>Loop Crypto is not available for this checkout</div>;
        }

        if (!isLoopInitialized) {
          return <div>Loading Loop widget...</div>;
        }

        return (
          <>
            <div>
              <input type="text" placeholder="Given name" data-opid={FieldName.FIRST_NAME} />
              <input type="text" placeholder="Family name" data-opid={FieldName.LAST_NAME} />
              <input type="email" placeholder="Email" data-opid={FieldName.EMAIL} />
              <input type="text" placeholder="Country" data-opid={FieldName.COUNTRY} />
              <input type="text" placeholder="ZIP" data-opid={FieldName.ZIP_CODE} />
            </div>

            <div>
              <LoopConnectPayIn
                paymentUsdAmount={0}
                minimumAuthorizationUsdAmount={loop.widget?.minAuthorizationUsdAmount}
                customerRefId={loop.widget?.customerRefId}
                addToExistingAuthorization={true}
                awaitConfirmation={true}
                stateNotification="embedded"
                onPayInCustomerCreated={(customer) => {
                  const minCustomer = {
                    customerId: customer.customerId,
                    customerRefId: customer.customerRefId,
                  };
                  const paymentMethod = customer.paymentMethods[0];
                  
                  setCreatedCustomer(minCustomer);
                  setCreatedPaymentMethod(paymentMethod);
                  
                  if (isAuthorized) {
                    submitWith(SubmitMethods.loopCrypto, {
                      success: true,
                      customer: minCustomer,
                      paymentMethod: paymentMethod,
                    });
                  }
                }}
                onPayInAuthorizationConfirmed={(detail) => {
                  setIsAuthorized(true);
                  
                  if (createdCustomer && createdPaymentMethod) {
                    submitWith(SubmitMethods.loopCrypto, {
                      success: true,
                      customer: createdCustomer,
                      paymentMethod: createdPaymentMethod,
                    });
                  }
                }}
                onPayInStateChange={(detail) => {
                  console.log('Loop state:', detail.state, detail.message);
                }}
                onPayInFailed={(detail) => {
                  submitWith(SubmitMethods.loopCrypto, {
                    success: false,
                    failureDetails: detail,
                  });
                }}
              />
            </div>

            {error && <small>{error}</small>}
          </>
        );
      }}
    </ElementsForm>
  );
}

export default LoopCheckoutForm;

Key Components

LoopConnect

Please see the office docs of LoopConnect here: https://docs.loopcrypto.xyz/docs/loopconnect-library

LoopConfig

The LoopConfigobject provided by ElementsForm contains:

interface LoopConfig {
  isAvailable: boolean;
  config?: {
    apiToken: string;
    entityId: string;
    merchantId: string;
    environment: string;
  };
  widget?: {
    paymentUsdAmount: number;
    suggestedAuthorizationUsdAmount: number;
    minAuthorizationUsdAmount: number;
    customerRefId: string;
    allowCustomerToChooseAuth: boolean;
  };
}

Submission Flow

The Loop integration follows a two-step confirmation flow:

  1. Customer Creation: Widget creates a Loop customer and payment method

  2. Authorization Confirmation: Blockchain confirms the authorization

Both callbacks (onPayInCustomerCreated and onPayInAuthorizationConfirmed) must fire before submitting. The order can vary, so track both states and submit when both are complete.

Best Practices

  1. State Management: Track customer creation and authorization separately as they can happen in any order

  2. Form Validation: Validate required form fields before enabling the widget

  3. Loading States: Show appropriate loading indicators during initialization and submission