Flutter & Order Checkout

Hello,
Is it possible to use the Checkout API, specifically the Square Order Checkout with a Flutter application?

Yes, it is possible to use the Checkout API, specifically the Square Order Checkout, with a Flutter application. Flutter allows for the integration of native code, which means you can implement the Checkout API using platform-specific implementations for iOS and Android. You would need to use platform channels to communicate between your Dart code and the native code that interacts with the Square Checkout API. :slightly_smiling_face:

1 Like

Thank you for your response.

I have begun the process using the OAuth API and Square Order Checkout. I am using the Flutter Square Connect Package which requires OAuth PKCE Flow.

Redirect URI & Authorization URL
Currently, I am unsure about the redirectUri I am using for the PKCE flow.

If you can, please give some insight on this and whether the redirect Uri should be the same as the Authroization URL.

OAuth Flow Error.
I am receiving the following error after dispatching FetchData() and navigating to the authorizationUrl

To start OAuth flow for a sandbox account, first launch the seller test account from the Developer Dashboard.

I have read previous responses on the topic, but even after opening my developer dashboard for the same account, I am having the same issue.

Here is my code below.

Cart Page
Future fetchData() async {
try {
final authorizationUrl = await authService.createAuthorizationUrl();
logger.i(‘Authorized URL: $authorizationUrl’);

  final authorizationCode = await navigateToAuthorizationWebView(authorizationUrl);

  if (authorizationCode != null) {
    final accessToken = await authService.obtainAccessToken(authorizationCode);

    if (accessToken != null) {
      squareApiService = SquareApiService(accessToken);

      // ignore: use_build_context_synchronously
      final orderState = context.read<OrderBloc>().state;
      if (orderState is OrderLoaded && orderState.order.isNotEmpty) {
        final order = orderState.order.first;

        // ignore: use_build_context_synchronously
        final checkoutUrl = await squareApiService.createSquareOrder(order, context.read<EmailProvider>());

        if (checkoutUrl != null) {
            // ignore: use_build_context_synchronously
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => CheckoutWebView(checkoutUrl: checkoutUrl),
              ),
            );
        } else {
          logger.e('Failed to get Square Checkout URL');
        }
      }
    }
  }
} catch (error) {
  logger.e('Error fetching data: $error');
}

}

AuthService:

class AuthService {
static const String clientId = ‘sandbox-sq0idb-cFyOYArfjFiKfKdzGLpWLw’;
static String redirectUri = ‘https://connect.squareupsandbox.com/oauth2/authorize?client_id=$clientId&redirect_uri=$redirectUri&response_type=code&scope=MERCHANT_PROFILE_READ PAYMENTS_WRITE’;
late String codeVerifier;
late String codeChallenge;
var logger = Logger();

AuthService() {
generateCodeVerifierAndChallenge();
}

String generateRandomString(int length) {
const chars = ‘abcdefghijklmnopqrstuvwxyz0123456789’;
final random = Random.secure();

return List.generate(length, (index) {
  final randomIndex = random.nextInt(chars.length);
  return chars[randomIndex];
}).join();

}

void generateCodeVerifierAndChallenge() {
codeVerifier = generateRandomString(43);
codeChallenge = base64UrlEncode(sha256.convert(utf8.encode(codeVerifier)).bytes);
}

Future createAuthorizationUrl() async {
return ‘https://connect.squareupsandbox.com/oauth2/authorize?client_id=$clientId&redirect_uri=$redirectUri&response_type=code&scope=MERCHANT_PROFILE_READ PAYMENTS_WRITE’;
}

Future<String?> obtainAccessToken(String authorizationCode) async {
try {
final response = await http.post(
Uri.parse(‘https://connect.squareupsandbox.com/oauth2/token’),
headers: {‘Content-Type’: ‘application/x-www-form-urlencoded’},
body: {
‘client_id’: clientId,
‘client_secret’: ‘sandbox-sq0csb-Hv7uKDoiCaf3j6Mhiy7XOr1GvFwuO0eVvf0utiVMKhk’,
‘code’: authorizationCode,
‘redirect_uri’: redirectUri,
‘grant_type’: ‘authorization_code’,
‘code_verifier’: codeVerifier,
},
);

  if (response.statusCode == 200) {
    final tokenResponse = SquareTokenResponse.fromJson(json.decode(response.body));
    return tokenResponse.accessToken;
  } else {
    // Handle errors
    logger.i('Token request failed with status code: ${response.statusCode}');
    return null;
  }
} catch (error) {
  // Handle network or other errors
  logger.e('Error obtaining access token: $error');
  return null;
}

}
}

class SquareTokenResponse {
final String accessToken;

SquareTokenResponse(this.accessToken);

factory SquareTokenResponse.fromJson(Map<String, dynamic> json) {
return SquareTokenResponse(json[‘access_token’]);
}
}

SquareApiService:

class SquareApiService {
late final SquareApiClient squareApiClient;
var logger = Logger();

SquareApiService(String accessToken) {
squareApiClient = SquareApiClient(
accessToken: accessToken,
);
}

Future<String?> createSquareOrder(order, EmailProvider emailProvider) async {
try {
final email = emailProvider.email;

  final response = await squareApiClient.createCheckout(
    locationId: '{LN54Q2JCMFA26}',
    body: CreateCheckoutRequest(
      idempotencyKey: '${DateTime.now().millisecondsSinceEpoch}',
      order: CreateCheckoutRequestOrder(
        idempotencyKey: '${DateTime.now().millisecondsSinceEpoch}',
        order: Order(
          lineItems: [
            OrderLineItem(
              quantity: '1',
              uid: order.orderID,
              metadata: {
                'location': order.location ?? '',
                'orderType': order.orderType ?? '',
                'nickname': order.nickname ?? '',
                'address': order.address ?? '',
                'zipCode': order.zipCode ?? '',
                'pickupInfo': order.pickupInfo ?? '',
                'phoneNumber': order.phoneNumber ?? '',
                'privateEntry': order.privateEntry?.toString() ?? '',
                'products': order.products?.toString() ?? '',
                'totalCost': order.totalCost?.toString() ?? '',
                'deliveryFee': order.deliveryFee?.toString() ?? '',
              },
            )
          ],
        ),
      ),
      prePopulateBuyerEmail: email,
    ),
  );

  logger.i(response);
} catch (error) {
  logger.i(error);
}
return null;

}

Future<String?> getSquareCheckoutUrl(Order order) async {
try {
final response = await squareApiClient.createCheckout(
locationId: ‘{LN54Q2JCMFA26}’,
body: CreateCheckoutRequest(
idempotencyKey: ‘${DateTime.now().millisecondsSinceEpoch}’,
order: CreateCheckoutRequestOrder(
idempotencyKey: ‘${DateTime.now().millisecondsSinceEpoch}’,
order: const Order(
lineItems: [
OrderLineItem(
quantity: ‘1’,
name: ‘Product Name’,
basePriceMoney: Money(
amount: 100,
currency: Currency.japaneseYen,
),
),
],
),
),
),
);

  final checkoutResponse = CreateCheckoutResponse.fromJson(json.decode(response.toString()));
  final checkoutUrl = checkoutResponse.checkout?.checkoutPageUrl;
  return checkoutUrl;
} catch (error) {
  logger.e('Error getting checkout URL: $error');
  return null;
}

}
}

In summary,

  1. Please give guidance on the use of the redirect and authorization URL.

  2. Please help resolve the OAuth error which requires the sandbox account to be launched before Square can start the OAuth Flow.

Thank you for your assistance.

This is in response to the floating prompt in this thread.

My follow-up question has not been answered.

It would be helpful to be told that my question is too long and should be shortened in order to make the best use of the moderator’s time.

Regarding the redirectUri and authorizationUrl, the redirectUri is the URL that Square will redirect to after the user has authorized your application. It is not the same as the authorizationUrl. The authorizationUrl is the URL that you will direct the user to in order to start the OAuth flow.

The redirectUri should be a URL that you control and can handle the response from Square, such as a callback endpoint on your server. It should be registered with Square in your application settings and must match exactly with the one you include in the authorizationUrl.

Is the Sandbox seller Dashboard launched? If that Dashboard isn’t launched it will throw this error when trying to OAuth. :slightly_smiling_face:

Thank you for your response.

I have created a dummy account and used the production URL to avoiding the sandbox launch requirement.

The below code launches my authorization URL and prompts the user the login to Square or Create an account.

However, once logged in, the user is simply directed to their Square Dashboard and not redirected to my app where the OAuth Response is supposed to be handled.

In my code, the authorizationURL is hard coded. Shouldn’t this be auto-generated in order to start the OAuth flow?

The code is below:

import ‘dart:convert’;
import ‘dart:math’;
import ‘package:crypto/crypto.dart’;
import ‘package:http/http.dart’ as http;
import ‘package:logger/logger.dart’;

class AuthService {
static const String clientId = ‘sq0idp-MHHCxCrIt-BapPtii7YhPA’;
static String redirectUri = ‘https://localhost-3000/oauth/callback’;
late String codeVerifier;
late String codeChallenge;
var logger = Logger();

AuthService() {
generateCodeVerifierAndChallenge();
}

String generateRandomString(int length) {
const chars = ‘abcdefghijklmnopqrstuvwxyz0123456789’;
final random = Random.secure();

return List.generate(length, (index) {
  final randomIndex = random.nextInt(chars.length);
  return chars[randomIndex];
}).join(); 

}

void generateCodeVerifierAndChallenge() {
codeVerifier = generateRandomString(43);
codeChallenge = base64UrlEncode(sha256.convert(utf8.encode(codeVerifier)).bytes);
}

Future createAuthorizationUrl() async {
return ‘https://connect.squareup.com/oauth2/authorize?client_id=$clientId&redirect_uri=$redirectUri&response_type=code&scope=MERCHANT_PROFILE_READ PAYMENTS_WRITE’;
}

Future<String?> obtainAccessToken(String authorizationCode) async {
try {
final response = await http.post(
Uri.parse(‘https://connect.squareupsandbox.com/oauth2/token’),
headers: {‘Content-Type’: ‘application/x-www-form-urlencoded’},
body: {
‘client_id’: clientId,
‘client_secret’: ‘sq0csp-akrNgDNH4h2pNZLMhnaJK0yrx9TmAElEqvFyyN5FwYM’,
‘code’: authorizationCode,
‘redirect_uri’: redirectUri,
‘grant_type’: ‘authorization_code’,
‘code_verifier’: codeVerifier,
},
);

  if (response.statusCode == 200) {
    final tokenResponse = SquareTokenResponse.fromJson(json.decode(response.body));
    return tokenResponse.accessToken;
  } else {
    logger.i('Token request failed with status code: ${response.statusCode}');
    return null;
  }
} catch (error) {
  logger.e('Error obtaining access token: $error');
  return null;
}

}

Future handleOAuthResponse(Map<String, String> queryParams) async {
final String authorizationCode = queryParams[‘code’] ?? ‘’;

final accessToken = await obtainAccessToken(authorizationCode);

if (accessToken != null) {
  logger.i('Access Token: $accessToken');
} else {
  logger.e('Failed to obtain access token');
}

}

}

class SquareTokenResponse {
final String accessToken;

SquareTokenResponse(this.accessToken);

factory SquareTokenResponse.fromJson(Map<String, dynamic> json) {
return SquareTokenResponse(json[‘access_token’]);
}
}

It looks like you have hard-coded the redirectUri in your code to https://localhost-3000/oauth/callback. This means that after the user logs in to Square, they will be redirected to that URL. However, if your app is not set up to handle the OAuth response at that URL, the user will not be redirected back to your app.

To fix this issue, you need to make sure that the redirectUri in your code matches the redirect URI that you have set up in your Square Developer Dashboard. Additionally, you should ensure that your app is set up to handle the OAuth response at that redirect URI.

Once you have the correct redirectUri set up in your code and your app is set up to handle the OAuth response, the authorization flow should work as expected and the user should be redirected back to your app after logging in to Square.


:slightly_smiling_face:

OK. So the Square Order API requires the user to sign into Square each time they checkout from the app.

If this is the case, I don’t think this would be the ideal user experience and I would prefer using separate APIs that would allow my app to submit an Order to my clients backend, then process the payment for the user against the order object.

Any advice on how to accomplish this?

Hey @placebo66!

This isn’t quite how the Orders API works. The OAuth API is the only part that requires a user to log in, and that’s only for when a Square Merchant is initially authorizing your application to access their account. Customers who are checking out do not need to sign into Square.

This is the general pattern that the Orders API follows. After your application has been authorized to an account (via OAuth), you can use Orders API to create an order for that merchant, then process a payment with the Payments API.

1 Like

Thank you Josh!

I think the Orders API will work best for my situation.

My intent is to allow customers to pay for orders submitted to Square without requiring users to create or sign into a Square Account.

Is this correct?

The Orders API appears to be a better fit, since submitted orders are seen in the Sellers Dashboard and the Payment API can be used to pay for the the order against the order_ID.

This is ideal as long as the user/client is not required to create or sign into Square.

To clarify, none of Square’s APIs for processing a payment require the user to login or have a Square account. Checkout API can also be used without logging in.

The main reason you would use Checkout API is if you do not already have a checkout experience built out for your site or app. You also have the option of creating orders with the Orders API, then paying for them with Checkout API.

If you’d like to try the APIs out yourself, you can try calling them them straight from your browser via API Explorer.

Let me know if there’s anything I can clear up here!