In today’s cloud-native world, breaking down monolithic applications into smaller, more manageable pieces, microservices has become the norm. Microservices enable flexibility, scalability, and resilience.
In this article, we will walk through the steps of building an e-commerce platform using microservices. The architecture will leverage technologies like gRPC, Dapr, containerization, and Blazor for the frontend.
What is Microservices?
Microservices divide an application into small, loosely coupled services, each focused on a single business function. In the context of an e-commerce platform, you might have separate services like:
ProductService to manage product information
OrderService to handle orders and payments
InventoryService to track product stock levels
By using microservices, you can scale each service independently, deploy them separately, and ensure that each service can evolve without affecting others. This leads to faster development cycles, better fault isolation, and improved resilience.
Key Concepts
Single Responsibility: Each microservice focuses on one function (e.g., user management, order processing).
Independently Deployable: Teams can develop, test, deploy, and scale each service independently.
Decentralized Data Management: Each service owns its own database to reduce dependencies and avoid shared data sources.
Technology Agnostic: Services can be written in different languages and use different data stores.
Benefits
Scalability: Scale only the services that need it.
Flexibility: Use different tech stacks per service.
Resilience: Failure in one service doesn’t crash the whole system.
Faster Development: Teams can work in parallel and deploy continuously.
Using gRPC, Dapr, and Message Brokers
gRPC: Lightweight Communication for Distributed Systems
gRPC is an efficient, high-performance protocol developed by Google. It’s ideal for internal service-to-service communication, especially in microservices architectures. It provides:
High performance: Uses HTTP/2 for multiplexed streams.
Type safety: Defined in Protocol Buffers (Protobuf), which ensures that data exchanged between services is strongly typed.
Streaming: Supports bidirectional streaming, ideal for handling real-time data exchanges.
For example, OrderService receives orders via gRPC, allowing it to quickly process large amounts of data from clients and communicate with other services in the backend.
Dapr: Communication and State for Distributed Apps
Dapr(Distributed Application Runtime) simplifies the development of distributed applications by providing:
Service Invocation: Makes it easy for microservices to call each other over HTTP/gRPC.
Pub/Sub Messaging: Enables event-driven architecture. Services can publish and subscribe to events without tightly coupling the logic between them.
State Management: Stores key-value pairs (like inventory data) in a distributed, consistent way.
For example, OrderService publishes an order.created event to InventoryService, and InventoryService responds by updating stock levels and publishing an inventory.updated event. All of this communication is handled by Dapr.
Message Brokers (RabbitMQ or Azure Service Bus): Reliable Messaging Between Decoupled Services
While Dapr’s pub/sub component abstracts messaging middleware like RabbitMQ or Azure Service Bus, these systems are still relevant for large-scale applications. They provide reliable messaging between services. Dapr can plug into either, depending on your infrastructure.
Project Architecture
Blazor Frontend (SPA)
The Blazor WebAssembly app will serve as the frontend for our e-commerce platform. Blazor enables you to build rich, interactive web UIs with C# instead of JavaScript. The frontend communicates with the backend via:
RESTful API calls to ProductService to fetch product data.
gRPC calls to OrderService to place orders.
API Gateway
The API Gateway is the entry point to the system. It routes client requests to the appropriate service:
ProductService (REST for browsing products)
OrderService (gRPC for placing orders)
The API Gateway can be implemented using YARP (Yet Another Reverse Proxy) or as a simple .NET service, routing requests based on the type of communication needed.
ProductService
The ProductService handles product-related tasks:
Exposes REST API endpoints for browsing and searching products.
Stores product data in a database (SQL or NoSQL) using Entity Framework Core.
Because this service needs to be accessed externally (via the Blazor frontend), it does not use gRPC. REST APIs are more suitable for external communications where JSON is typically used.
OrderService
The OrderService handles everything related to order creation:
Receives Orders via gRPC from the Blazor frontend via the API Gateway.
Publishes an order.created event to Dapr’s pub/sub component for other services to react to.
Subscribes to inventory.updated events to track stock levels and update the order status.
InventoryService
InventoryService uses Dapr’s state management to persist inventory data in a distributed store (e.g., Redis, Cosmos DB).
Subscribes to order.created events from Dapr and updates inventory levels accordingly.
Publishes inventory.updated events once stock is updated, so OrderService can react and finalize orders.
Putting the Pieces Together
Here’s a quick walkthrough of how the services interact in the system:
1️⃣ User adds items to the cart (via Blazor → ProductService via REST)
2️⃣ User places an order (via Blazor → OrderService via gRPC)
3️⃣ OrderService:
Saves the order in the database
Publishes an order.created event to Dapr Pub/Sub
4️⃣ InventoryService:
Subscribes to order.created event
Updates inventory levels in the state store
Publishes inventory.updated event when done
5️⃣ OrderService:
Subscribes to inventory.updated event
Updates the order status based on inventory changes
ProductService: Exposing Product Information via REST API
This service will expose a REST API endpoint that returns product details. For simplicity, we’ll use ASP.NET Core with Entity Framework Core for data persistence.
This service is simple: it exposes two endpoints:
A GET /api/products to retrieve all products.
A GET /api/products/{id} to retrieve a single product.
using Microsoft.AspNetCore.Mvc; using ProductService.Data; using ProductService.Models;
namespace ProductService.Controllers { [ApiController] [Route("api/products")] public class ProductController : ControllerBase { private readonly ApplicationDbContext _context;
public ProductController(ApplicationDbContext context) { _context = context; }
// GET api/products/{id} [HttpGet("{id}")] public async Task<IActionResult> GetProduct(int id) { var product = await _context.Products.FindAsync(id); if (product == null) { return NotFound(); } return Ok(product); }
// GET api/products [HttpGet] public async Task<IActionResult> GetProducts() { var products = await _context.Products.ToListAsync(); return Ok(products); } } }
ProductService: Data Context and Model
// ProductService/Data/ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore; using ProductService.Models;
namespace ProductService.Data { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
public DbSet<Product> Products { get; set; } } }
// ProductService/Models/Product.cs namespace ProductService.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public int Stock { get; set; } } }
OrderService: Handling the Order Creation via gRPC
OrderService publishes an order.created event via Dapr Pub/Sub, which InventoryService listens to.
gRPC and Event Publishing
This is a Protocol Buffers (proto3) file used by gRPC to define the structure of messages and services for remote procedure calls.
This is the gRPC definition for placing an order. The PlaceOrder method receives a productId and quantity and returns an order ID and status.
// OrderService/Protos/order.proto
syntax = "proto3";
package order;
service OrderService { rpc PlaceOrder (OrderRequest) returns (OrderResponse); }
When an order is placed, it creates an order record in the database and publishes an order.created event to Dapr Pub/Sub. The InventoryService will be subscribed to this event.
// OrderService/Services/OrderService.cs
using Grpc.Core; using Dapr.Client; using OrderService.Protos; using OrderService.Models;
public class OrderService : OrderService.OrderServiceBase { private readonly DaprClient _daprClient; private readonly ApplicationDbContext _context;
public override async Task<OrderResponse> PlaceOrder(OrderRequest request, ServerCallContext context) { // Retrieve product from ProductService var product = await GetProductById(request.ProductId);
if (product == null || product.Stock < request.Quantity) { return new OrderResponse { Status = "Insufficient stock" }; }
// Create order var order = new Order { ProductId = request.ProductId, Quantity = request.Quantity, Status = "Pending" }; _context.Orders.Add(order); await _context.SaveChangesAsync();
// Publish the order created event await _daprClient.PublishEventAsync("pubsub", "order.created", order);
return new OrderResponse { OrderId = order.Id, Status = "Order created" }; }
private async Task<Product> GetProductById(int productId) { // Simulate gRPC call to ProductService var channel = GrpcChannel.ForAddress("http://productservice"); var client = new ProductService.ProductServiceClient(channel); var response = await client.GetProductAsync(new GetProductRequest { Id = productId }); return response != null ? new Product { Id = response.Id, Name = response.Name, Price = response.Price, Stock = response.Stock } : null; } }
OrderService Subscribing to inventory.updated
OrderService uses [Topic("pubsub", "inventory.updated")] to subscribe to the inventory.updated event. When InventoryService publishes this event after updating the inventory, OrderService can then update the order status to “Shipped” (or any other status).
// OrderService/Services/OrderService.cs
[Topic("pubsub", "inventory.updated")] // Dapr event subscription public async Task HandleInventoryUpdatedAsync(Product product) { var order = await _context.Orders .Where(o => o.ProductId == product.Id && o.Status == "Pending") .FirstOrDefaultAsync();
if (order != null) { // If inventory was updated successfully, mark the order as "Shipped" order.Status = "Shipped"; _context.Orders.Update(order); await _context.SaveChangesAsync(); } }
InventoryService: Handling Inventory and Events
The InventoryService listens for the order.created event and updates the inventory when an order is placed.
InventoryService: Event Subscription and Inventory Update
// InventoryService/Services/InventoryService.cs
using Dapr.Client; using InventoryService.Models; using InventoryService.Protos;
public class InventoryService { private readonly DaprClient _daprClient; private readonly ApplicationDbContext _context;
// Subscribe to the 'order.created' event [Topic("pubsub", "order.created")] public async Task HandleOrderCreatedAsync(Order order) { var product = await _context.Products.FindAsync(order.ProductId);
Once the stock is updated, the InventoryService publishes an inventory.updated event. This is an event that OrderService subscribes to for order status updates.
InventoryService Subscribing to order.created
InventoryService uses the [Topic("pubsub", "order.created")] attribute to subscribe to the event. When the event is triggered, it reduces the stock and publishes the inventory.updated event.
// InventoryService/Services/InventoryService.cs
[Topic("pubsub", "order.created")] // Dapr event subscription public async Task HandleOrderCreatedAsync(Order order) { var product = await _context.Products.FindAsync(order.ProductId);
In the Blazor frontend, you will interact with the OrderService to place orders, and then display the status of the orders and products. We’ll show how to integrate the frontend with these services.
Blazor Client Setup:
Place Order: The Blazor frontend will send a gRPC request to OrderService to place an order.
View Products: The Blazor frontend can make REST API calls to ProductService to view products.
Display Order Status: Once the order is placed and the stock is updated, the frontend will show the order status.
Blazor Component: Place Order
In your Blazor frontend, you can create a component to place an order.
This simple Blazor component:
Takes the product ID and quantity from the user.
Sends a gRPC request to OrderService to place the order.
Displays the status returned by OrderService (e.g., “Order created” or “Insufficient stock”).
public class Product { public string Name { get; set; } public decimal Price { get; set; } } }
Containerization
Containerization is crucial for ensuring that the microservices run consistently across different environments. In this architecture, each service (including the Blazor frontend) will be packaged as a Docker container.
Each microservice includes:
A Dockerfile that specifies how the container is built.
Dapr sidecar (running alongside the service) to handle Pub/Sub, state management, and service invocation.
Example Docker File for OrderService
# Use the official .NET image as the base image FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80
# Use the SDK image to build the app FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY ["OrderService/OrderService.csproj", "OrderService/"] RUN dotnet restore "OrderService/OrderService.csproj" COPY . . WORKDIR "/src/OrderService" RUN dotnet build "OrderService.csproj" -c Release -o /app/build
FROM build AS publish RUN dotnet publish "OrderService.csproj" -c Release -o /app/publish
FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "OrderService.dll"]
Deploying with Dapr and Azure
After building the services and container images, you can deploy the microservices on Azure Kubernetes Service (AKS) or Azure Container Apps. Dapr will run as a sidecar in each container, providing all the necessary microservice patterns for service invocation, state management, and pub/sub communication.
This system illustrates how microservices can work together with gRPC for fast communication and Dapr for event-driven interactions. The Blazor frontend allows users to interact with these services seamlessly, placing orders, checking stock, and viewing product details.
Each service operates independently, allowing for flexible scaling, deployment, and maintenance while keeping the overall architecture clean and loosely coupled.
{"id":"2","mode":"button","open_style":"in_modal","currency_code":"USD","currency_symbol":"$","currency_type":"decimal","blank_flag_url":"https:\/\/robhutton.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/blank.gif","flag_sprite_url":"https:\/\/robhutton.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/flags.png","default_amount":500,"top_media_type":"featured_image","featured_image_url":"https:\/\/robhutton.com\/wp-content\/uploads\/Screenshot-2025-02-19-163019-100x74.png","featured_embed":"","header_media":null,"file_download_attachment_data":null,"recurring_options_enabled":false,"recurring_options":{"never":{"selected":true,"after_output":"One time only"},"weekly":{"selected":false,"after_output":"Every week"},"monthly":{"selected":false,"after_output":"Every month"},"yearly":{"selected":false,"after_output":"Every year"}},"strings":{"current_user_email":"","current_user_name":"","link_text":"Tip Jar","complete_payment_button_error_text":"Check info and try again","payment_verb":"Pay","payment_request_label":"robhutton.com","form_has_an_error":"Please check and fix the errors above","general_server_error":"Something isn't working right at the moment. Please try again.","form_title":"robhutton.com","form_subtitle":null,"currency_search_text":"Country or Currency here","other_payment_option":"Other payment option","manage_payments_button_text":"Manage your payments","thank_you_message":"Thank you for being a supporter!","payment_confirmation_title":"robhutton.com","receipt_title":"Your Receipt","print_receipt":"Print Receipt","email_receipt":"Email Receipt","email_receipt_sending":"Sending receipt...","email_receipt_success":"Email receipt successfully sent","email_receipt_failed":"Email receipt failed to send. Please try again.","receipt_payee":"Paid to","receipt_statement_descriptor":"This will show up on your statement as","receipt_date":"Date","receipt_transaction_id":"Transaction ID","receipt_transaction_amount":"Amount","refund_payer":"Refund from","login":"Log in to manage your payments","manage_payments":"Manage Payments","transactions_title":"Your Transactions","transaction_title":"Transaction Receipt","transaction_period":"Plan Period","arrangements_title":"Your Plans","arrangement_title":"Manage Plan","arrangement_details":"Plan Details","arrangement_id_title":"Plan ID","arrangement_payment_method_title":"Payment Method","arrangement_amount_title":"Plan Amount","arrangement_renewal_title":"Next renewal date","arrangement_action_cancel":"Cancel Plan","arrangement_action_cant_cancel":"Cancelling is currently not available.","arrangement_action_cancel_double":"Are you sure you'd like to cancel?","arrangement_cancelling":"Cancelling Plan...","arrangement_cancelled":"Plan Cancelled","arrangement_failed_to_cancel":"Failed to cancel plan","back_to_plans":"\u2190 Back to Plans","update_payment_method_verb":"Update","sca_auth_description":"Your have a pending renewal payment which requires authorization.","sca_auth_verb":"Authorize renewal payment","sca_authing_verb":"Authorizing payment","sca_authed_verb":"Payment successfully authorized!","sca_auth_failed":"Unable to authorize! Please try again.","login_button_text":"Log in","login_form_has_an_error":"Please check and fix the errors above","uppercase_search":"Search","lowercase_search":"search","uppercase_page":"Page","lowercase_page":"page","uppercase_items":"Items","lowercase_items":"items","uppercase_per":"Per","lowercase_per":"per","uppercase_of":"Of","lowercase_of":"of","back":"Back to plans","zip_code_placeholder":"Zip\/Postal Code","download_file_button_text":"Download File","input_field_instructions":{"tip_amount":{"placeholder_text":"How much would you like to tip?","initial":{"instruction_type":"normal","instruction_message":"How much would you like to tip? Choose any currency."},"empty":{"instruction_type":"error","instruction_message":"How much would you like to tip? Choose any currency."},"invalid_curency":{"instruction_type":"error","instruction_message":"Please choose a valid currency."}},"recurring":{"placeholder_text":"Recurring","initial":{"instruction_type":"normal","instruction_message":"How often would you like to give this?"},"success":{"instruction_type":"success","instruction_message":"How often would you like to give this?"},"empty":{"instruction_type":"error","instruction_message":"How often would you like to give this?"}},"name":{"placeholder_text":"Name on Credit Card","initial":{"instruction_type":"normal","instruction_message":"Enter the name on your card."},"success":{"instruction_type":"success","instruction_message":"Enter the name on your card."},"empty":{"instruction_type":"error","instruction_message":"Please enter the name on your card."}},"privacy_policy":{"terms_title":"Terms and conditions","terms_body":"Voluntary nature: Tips are non-refundable and given voluntarily. No goods\/services in exchange: Tipping doesn\u2019t entitle the user to any product, service, or preferential treatment. Payment processing: Mention that payments are processed securely through a third-party provider. No liability: You aren\u2019t responsible for transaction failures, fraud, or technical issues.","terms_show_text":"View Terms","terms_hide_text":"Hide Terms","initial":{"instruction_type":"normal","instruction_message":"I agree to the terms."},"unchecked":{"instruction_type":"error","instruction_message":"Please agree to the terms."},"checked":{"instruction_type":"success","instruction_message":"I agree to the terms."}},"email":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email address"},"success":{"instruction_type":"success","instruction_message":"Enter your email address"},"blank":{"instruction_type":"error","instruction_message":"Enter your email address"},"not_an_email_address":{"instruction_type":"error","instruction_message":"Make sure you have entered a valid email address"}},"note_with_tip":{"placeholder_text":"Your note here...","initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"empty":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"not_empty_initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"saving":{"instruction_type":"normal","instruction_message":"Saving note..."},"success":{"instruction_type":"success","instruction_message":"Note successfully saved!"},"error":{"instruction_type":"error","instruction_message":"Unable to save note note at this time. Please try again."}},"email_for_login_code":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email to log in."},"success":{"instruction_type":"success","instruction_message":"Enter your email to log in."},"blank":{"instruction_type":"error","instruction_message":"Enter your email to log in."},"empty":{"instruction_type":"error","instruction_message":"Enter your email to log in."}},"login_code":{"initial":{"instruction_type":"normal","instruction_message":"Check your email and enter the login code."},"success":{"instruction_type":"success","instruction_message":"Check your email and enter the login code."},"blank":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."},"empty":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."}},"stripe_all_in_one":{"initial":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"empty":{"instruction_type":"error","instruction_message":"Enter your credit card details here."},"success":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"invalid_number":{"instruction_type":"error","instruction_message":"The card number is not a valid credit card number."},"invalid_expiry_month":{"instruction_type":"error","instruction_message":"The card's expiration month is invalid."},"invalid_expiry_year":{"instruction_type":"error","instruction_message":"The card's expiration year is invalid."},"invalid_cvc":{"instruction_type":"error","instruction_message":"The card's security code is invalid."},"incorrect_number":{"instruction_type":"error","instruction_message":"The card number is incorrect."},"incomplete_number":{"instruction_type":"error","instruction_message":"The card number is incomplete."},"incomplete_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incomplete."},"incomplete_expiry":{"instruction_type":"error","instruction_message":"The card's expiration date is incomplete."},"incomplete_zip":{"instruction_type":"error","instruction_message":"The card's zip code is incomplete."},"expired_card":{"instruction_type":"error","instruction_message":"The card has expired."},"incorrect_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incorrect."},"incorrect_zip":{"instruction_type":"error","instruction_message":"The card's zip code failed validation."},"invalid_expiry_year_past":{"instruction_type":"error","instruction_message":"The card's expiration year is in the past"},"card_declined":{"instruction_type":"error","instruction_message":"The card was declined."},"missing":{"instruction_type":"error","instruction_message":"There is no card on a customer that is being charged."},"processing_error":{"instruction_type":"error","instruction_message":"An error occurred while processing the card."},"invalid_request_error":{"instruction_type":"error","instruction_message":"Unable to process this payment, please try again or use alternative method."},"invalid_sofort_country":{"instruction_type":"error","instruction_message":"The billing country is not accepted by SOFORT. Please try another country."}}}},"fetched_oembed_html":false}
{"id":"4","mode":"text_link","open_style":"in_modal","currency_code":"USD","currency_symbol":"$","currency_type":"decimal","blank_flag_url":"https:\/\/robhutton.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/blank.gif","flag_sprite_url":"https:\/\/robhutton.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/flags.png","default_amount":500,"top_media_type":"featured_image","featured_image_url":"https:\/\/robhutton.com\/wp-content\/uploads\/Screenshot-2025-02-19-163019-100x74.png","featured_embed":"","header_media":null,"file_download_attachment_data":null,"recurring_options_enabled":true,"recurring_options":{"never":{"selected":true,"after_output":"One time only"},"weekly":{"selected":false,"after_output":"Every week"},"monthly":{"selected":false,"after_output":"Every month"},"yearly":{"selected":false,"after_output":"Every year"}},"strings":{"current_user_email":"","current_user_name":"","link_text":"Like my articles? Tips are appreciated.","complete_payment_button_error_text":"Check info and try again","payment_verb":"Pay","payment_request_label":"","form_has_an_error":"Please check and fix the errors above","general_server_error":"Something isn't working right at the moment. Please try again.","form_title":"","form_subtitle":null,"currency_search_text":"Country or Currency here","other_payment_option":"Other payment option","manage_payments_button_text":"Manage your payments","thank_you_message":"Thank you for being a supporter!","payment_confirmation_title":"","receipt_title":"Your Receipt","print_receipt":"Print Receipt","email_receipt":"Email Receipt","email_receipt_sending":"Sending receipt...","email_receipt_success":"Email receipt successfully sent","email_receipt_failed":"Email receipt failed to send. Please try again.","receipt_payee":"Paid to","receipt_statement_descriptor":"This will show up on your statement as","receipt_date":"Date","receipt_transaction_id":"Transaction ID","receipt_transaction_amount":"Amount","refund_payer":"Refund from","login":"Log in to manage your payments","manage_payments":"Manage Payments","transactions_title":"Your Transactions","transaction_title":"Transaction Receipt","transaction_period":"Plan Period","arrangements_title":"Your Plans","arrangement_title":"Manage Plan","arrangement_details":"Plan Details","arrangement_id_title":"Plan ID","arrangement_payment_method_title":"Payment Method","arrangement_amount_title":"Plan Amount","arrangement_renewal_title":"Next renewal date","arrangement_action_cancel":"Cancel Plan","arrangement_action_cant_cancel":"Cancelling is currently not available.","arrangement_action_cancel_double":"Are you sure you'd like to cancel?","arrangement_cancelling":"Cancelling Plan...","arrangement_cancelled":"Plan Cancelled","arrangement_failed_to_cancel":"Failed to cancel plan","back_to_plans":"\u2190 Back to Plans","update_payment_method_verb":"Update","sca_auth_description":"Your have a pending renewal payment which requires authorization.","sca_auth_verb":"Authorize renewal payment","sca_authing_verb":"Authorizing payment","sca_authed_verb":"Payment successfully authorized!","sca_auth_failed":"Unable to authorize! Please try again.","login_button_text":"Log in","login_form_has_an_error":"Please check and fix the errors above","uppercase_search":"Search","lowercase_search":"search","uppercase_page":"Page","lowercase_page":"page","uppercase_items":"Items","lowercase_items":"items","uppercase_per":"Per","lowercase_per":"per","uppercase_of":"Of","lowercase_of":"of","back":"Back to plans","zip_code_placeholder":"Zip\/Postal Code","download_file_button_text":"Download File","input_field_instructions":{"tip_amount":{"placeholder_text":"How much would you like to tip?","initial":{"instruction_type":"normal","instruction_message":"How much would you like to tip? Choose any currency."},"empty":{"instruction_type":"error","instruction_message":"How much would you like to tip? Choose any currency."},"invalid_curency":{"instruction_type":"error","instruction_message":"Please choose a valid currency."}},"recurring":{"placeholder_text":"Recurring","initial":{"instruction_type":"normal","instruction_message":"How often would you like to give this?"},"success":{"instruction_type":"success","instruction_message":"How often would you like to give this?"},"empty":{"instruction_type":"error","instruction_message":"How often would you like to give this?"}},"name":{"placeholder_text":"Name on Credit Card","initial":{"instruction_type":"normal","instruction_message":"Enter the name on your card."},"success":{"instruction_type":"success","instruction_message":"Enter the name on your card."},"empty":{"instruction_type":"error","instruction_message":"Please enter the name on your card."}},"privacy_policy":{"terms_title":"Terms and conditions","terms_body":"Voluntary nature: Tips are non-refundable and given voluntarily. No goods\/services in exchange: Tipping doesn\u2019t entitle the user to any product, service, or preferential treatment. Payment processing: Mention that payments are processed securely through a third-party provider. No liability: You aren\u2019t responsible for transaction failures, fraud, or technical issues.","terms_show_text":"View Terms","terms_hide_text":"Hide Terms","initial":{"instruction_type":"normal","instruction_message":"I agree to the terms."},"unchecked":{"instruction_type":"error","instruction_message":"Please agree to the terms."},"checked":{"instruction_type":"success","instruction_message":"I agree to the terms."}},"email":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email address"},"success":{"instruction_type":"success","instruction_message":"Enter your email address"},"blank":{"instruction_type":"error","instruction_message":"Enter your email address"},"not_an_email_address":{"instruction_type":"error","instruction_message":"Make sure you have entered a valid email address"}},"note_with_tip":{"placeholder_text":"Your note here...","initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"empty":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"not_empty_initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"saving":{"instruction_type":"normal","instruction_message":"Saving note..."},"success":{"instruction_type":"success","instruction_message":"Note successfully saved!"},"error":{"instruction_type":"error","instruction_message":"Unable to save note note at this time. Please try again."}},"email_for_login_code":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email to log in."},"success":{"instruction_type":"success","instruction_message":"Enter your email to log in."},"blank":{"instruction_type":"error","instruction_message":"Enter your email to log in."},"empty":{"instruction_type":"error","instruction_message":"Enter your email to log in."}},"login_code":{"initial":{"instruction_type":"normal","instruction_message":"Check your email and enter the login code."},"success":{"instruction_type":"success","instruction_message":"Check your email and enter the login code."},"blank":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."},"empty":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."}},"stripe_all_in_one":{"initial":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"empty":{"instruction_type":"error","instruction_message":"Enter your credit card details here."},"success":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"invalid_number":{"instruction_type":"error","instruction_message":"The card number is not a valid credit card number."},"invalid_expiry_month":{"instruction_type":"error","instruction_message":"The card's expiration month is invalid."},"invalid_expiry_year":{"instruction_type":"error","instruction_message":"The card's expiration year is invalid."},"invalid_cvc":{"instruction_type":"error","instruction_message":"The card's security code is invalid."},"incorrect_number":{"instruction_type":"error","instruction_message":"The card number is incorrect."},"incomplete_number":{"instruction_type":"error","instruction_message":"The card number is incomplete."},"incomplete_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incomplete."},"incomplete_expiry":{"instruction_type":"error","instruction_message":"The card's expiration date is incomplete."},"incomplete_zip":{"instruction_type":"error","instruction_message":"The card's zip code is incomplete."},"expired_card":{"instruction_type":"error","instruction_message":"The card has expired."},"incorrect_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incorrect."},"incorrect_zip":{"instruction_type":"error","instruction_message":"The card's zip code failed validation."},"invalid_expiry_year_past":{"instruction_type":"error","instruction_message":"The card's expiration year is in the past"},"card_declined":{"instruction_type":"error","instruction_message":"The card was declined."},"missing":{"instruction_type":"error","instruction_message":"There is no card on a customer that is being charged."},"processing_error":{"instruction_type":"error","instruction_message":"An error occurred while processing the card."},"invalid_request_error":{"instruction_type":"error","instruction_message":"Unable to process this payment, please try again or use alternative method."},"invalid_sofort_country":{"instruction_type":"error","instruction_message":"The billing country is not accepted by SOFORT. Please try another country."}}}},"fetched_oembed_html":false}