stripe-samples / checkout-single-subscription Goto Github PK
View Code? Open in Web Editor NEWLearn how to combine Checkout and Billing for fast subscription pages
Home Page: https://checkout.stripe.dev/?mode=subscription
License: MIT License
Learn how to combine Checkout and Billing for fast subscription pages
Home Page: https://checkout.stripe.dev/?mode=subscription
License: MIT License
var redirectToCheckout = function(planId) {
// Make the call to Stripe.js to redirect to the checkout page
// with the current quantity
stripe
.redirectToCheckout({
items: [{ plan: planId, quantity: 1 }],
successUrl:
"https://" + DOMAIN + "/success.html?session_id={CHECKOUT_SESSION_ID}",
cancelUrl: "https://" + DOMAIN + "/canceled.html"
})
.then(handleResult);
};
MISS protocol in default client only redirect (in javascript)
Hello, I'm working through the client-only integration, and noticed a discrepancy between the live demo, at https://4iupj.sse.codesandbox.io/ , and the GitHub repo.
If you inspect the live demo in browser devtools, there's an extra code block, which results in the session information being displayed on the 'success' page (copied at the bottom).
That fetch
path doesn't exist in the sample or on my own page. Am I correct in assuming that
session
returns? And,I'm getting all the necessary logs and session info in my own server side code via a Webhook, but I wanted to make sure that I'm not missing a more direct way to acquire the session info on the client side.
Discrepant Code
var sessionId = urlParams.get("session_id")
if (sessionId) {
fetch("/checkout-session?sessionId=" + sessionId).then(function (result) {
return result.text();
}).then(function (session) {
var sessionJSON = JSON.stringify(session, null, 2);
document.querySelector("#stripejson").textContent = sessionJSON;
})
}
Thanks!
Hello, the client-only integration is great!
I'm running into a bug (maybe?) when attempting to test this locally.
To reproduce
successURL:
to "https://localhost:5000" + "?session_id={CHECKOUT_SESSION_ID}"
,This site can’t provide a secure connectionlocalhost sent an invalid response. ERR_SSL_PROTOCOL_ERROR
This seems to be a bug, because when loading Checkout locally, it simply issues a warning in the console You may test your Stripe.js integration over HTTP. However, live Stripe.js integrations must use HTTPS.
Current Workaround
I've created a live testing environment, but this is extremely slow to test, and hard to debug via logging, because any change requires a redeploy. Being able to use Localhost would
greatly speed up the dev process, on both counts (speed and readability).
Question
If this is an intentional feature, is there a recommended workaround to serve Localhost over https? (I did due-diligence Google this, but most solutions are more involved than someone using the client-only integration is probably comfortable with! So just checking here, first.)
Additional Information
Not sure if it's relevant, but I'm using the Firebase local emulator to locally serve and test my web app.
Thanks!
Hello,
I would like to set up a codesandbox.io client only example to share it.
I guess the server part with the start command in the package.json would not be needed.
Can you help me on that one ?
A clear and concise description of what the bug is.
Steps to reproduce the behavior, please provide code snippets or a repository:
A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
Add any other context about the problem here.
The sample Startup.cs code references a type that does not exist in the namespaces or libraries referenced in the using statements.
Steps to reproduce the behavior, please provide code snippets or a repository:
A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
Add any other context about the problem here.
How to update the home page with Stripe.
I changed the value in .env file, but the price in home page don't change
Is it normal ? if yes what can I do ?
PS : When I click on the price button the redirection works and the price in payement page is good, but not in home page.
We recently added go and dotnet server languages and need to update the cli.json file to include these.
Hi there,
I have the following code taken from multiple flask subscription docs on the web. Everything works perfectly fine, however, stripeData = event["data"]["object"]
looks like its not returning the customer info, as "didn't get stored" gets return in the success endpoint. I am so confused why this is the case when the web hook is successful per the payments going through in the dashboard and when connecting to stripe-cli, the customer info even gets shown. However, by its own it doesn't. Would really appreciate some clarity.
stripe_keys = {
"secret_key":'sk_test_5...GKIv8T67w0f8000ypWZx9om',
"publishable_key": 'pk_test_51Hhm...X6q2FLqW',
"price_id": 'price_1HhmCyBBlU..f',
"endpoint_secret":'whsec_lFkitrmzG...u0PbD4riv7'
}
stripe.api_key = stripe_keys["secret_key"]
@blueprint.route("/stripe_webhook", methods=["POST"])
def stripe_webhook():
# print("Webhook called ")
payload = request.get_data(as_text=True)
sig_header = request.headers.get("Stripe-Signature")
event = None
try:
event = stripe.Webhook.construct_event(
payload, sig_header, stripe_keys["endpoint_secret"]
)
except ValueError as e:
return "Invalid payload", 400
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return "Invalid signature", 400
if event["type"] == "checkout.session.completed":
try:
stripeData = event["data"]["object"]
stripe_customer_id = stripeData['customer']
stripe_subscription_id = stripeData['subscription']
user = User.query.filter_by(id=str(session['id'])).first()
user = User(stripeSubscripstionId= stripe_subscription_id, stripeCustomerId= stripe_customer_id)
db.session.add(user)
db.session.commit()
except:
print('FAILED TO WORK') #doesn't get returned
return {}
@blueprint.route("/config")
def get_publishable_key():
stripe_config = {"publicKey": stripe_keys["publishable_key"]}
return jsonify(stripe_config)
@blueprint.route("/success")
def success():
print('session id is')
print(session['id'])
user = User.query.filter_by(id=str(session['id'])).first()
if user.stripeSubscriptionId:
return render_template("/General/success.html")
return "didn't get stored"
@blueprint.route("/create-checkout-session")
def create_checkout_session():
domain_url = "http://localhost:5000/"
stripe.api_key = stripe_keys["secret_key"]
# User.query.filter_by(id=session['id']).first()
try:
checkout_session = stripe.checkout.Session.create(
client_reference_id=User.id,
success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
cancel_url=domain_url + "cancel",
payment_method_types=["card"],
mode="subscription",
line_items=[
{
"price": stripe_keys["price_id"],
"quantity": 1,
}
]
)
return jsonify({"sessionId": checkout_session["id"]})
except Exception as e:
return jsonify(error=str(e)), 403
Checkout page in 'subscription' mode saves payment methods even when payment is not sucessfull (declined card for example).
This may not be a bug in a strict sense but it is a major inconvience, because if user enters wrong card data by mistake it will be later seen in his payment methods without any indication that it is wrong card.
After inspecting events generated by stripe I can see that there is no setup_intent.*
events present in logs. So it seems like Checkout in subscription
mode doesn't use SetupIntent API to create payment methods as is recommended in docs (https://stripe.com/docs/api/payment_methods/attach). When I used SetupIntent API to manually add a payment method to a customer it is not created if card data is incorrect, which is desirable behaviour.
I understand that it may be a design decision to make it work that way, but it is unituitive from customer's view point.
subscription
mode with added customer
field and payment_method_types
set to ['card']
I expected that Checkout page wouldn't add payment methods to customer if first subscription payment has failed.
Is there a way to add more fields to the checkout page, such as organization, city, state? This will help us better identify the customer.
Hello. We need to handle these 2 scenarios:
Postdate a subscription: save the card info, charge the first payment instantly, but only start the subscription in the future.
E.g, user pays first payment on Dec 1 2020, but subscription only starts on Jan 1 2021, and the next payment will be on Jan 1 2022.
Backdate a subscription: save the card info, charge the first payment instantly, starts the subscription in the past.
E.g, user pays first payment on Feb 1 2020, but subscription actually started on Jan 1 2020, and the next payment will be on Jan 1 2021.
With the old legacy charge/subscription API, we were able to achieve these using one time charges and set future start date for subscription. These cases are very common in membership payments that do not offer proration (proration in full).
How can we address these cases? Thank you!
When using the stripe samples
command to clone the repository, it removes the python/
folder (and copies the content into 'server'). This then means that when you run the application, it is unable to find the client files because the static dir is set to ../../client
while it should be set to ../client
.
Potential solutions:
.env.example
file to set the static director appropriatelyThe stock command in the readme.md gives an error:
Missing required param: Currency
Steps to reproduce the behavior, please provide code snippets or a repository:
Follow the readme.md , Using Node although it would be the same for others.
When you are up to this part:
./stripe prices create
-d "product=ID_OF_BASIC_PRODUCT"
-d "unit_amount=1800"
-d "currency=usd"
-d "recurring[interval]=month"
After substituting in your ID, you'll get this error:
"error": {
"code": "parameter_missing",
"doc_url": "https://stripe.com/docs/error-codes/parameter-missing",
"message": "Missing required param: currency.",
"param": "currency",
"request_log_url": "https://dashboard.stripe.com/test/logs/req_DizJci51DGcUB5?t=1685198228",
"type": "invalid_request_error"
}
}
That the price is created, without you having to go in the GUI and do it.
I have successfully created checkout form in php. it 's not showing google pay or apple pay buttons on my checkout form. they both have been by default ON in my dashboard. so what could be the problem.
What i need :
https://i.imgur.com/a06wtgH.png?1
What i have :
https://i.imgur.com/FnXl4KA.png?1
Any Reply Would be Greatly Appreciated.
Thanks.
I have the same problem that @jackcrane had. He seems to have found the "shared.php" but now the file is no longer there.
Can anyone let me know where the file is?
Is it possible to specify the collection_method
for a subscription when checkout?
I see that it is charge_automatically
by default, is it possible to set collection_method
to send_invoice
when we create checkout session?
Thank you.
Many of the files in the server/PHP demonstration of this repo are including a "shared.php" file, but there is not one anywhere in the repo.
Some of the files that include it are:
server/php/public/config.php
server/php/public/get-checkout-session.php
server/php/public/webhook.php
server/php/public/create-checkout-session.php
Hi there,
My question is more business related than code:
In my app, I'd like to give an extra week of subscription if a user referred someone to the app.
My question:
I've added a pull request #218 to fix a few issues with the examples documentation, especially when using on Windows environments. The formatting was also fairly inconsistent, and I feel the updates I did makes it more legible.
Welcome any changes of course.
Hi,
I have been trying to implement this checkout-single-subscription
What is working :
Failure :
** Expected ** : retrieve the error message insufficient funds
** Explanation ** : The logs (see index) are able to throw the right error Your card has insufficient funds
. However the error catching in the creatsubscription function fails.
Catching 8 in createSubscription = TypeError: Cannot destructure property 'subscription' of 'undefined' as it is undefined.
at handlePaymentMethodRequired (Checkoutform.js:103)
I had to create a default error showCardError({ error: { message: 'Your card was declined.' } });
. because I wasn't able to use the thrown erros.
export default function CheckoutForm(props) {
const [priceId, setPriceId] = React.useState('price_1GqywBC2ahMs23DqyBiAZLZG');
const [openedSuccessModal, setOpenedSuccessModal] = React.useState(false)
const[openErrorSnack,setOpenSnack] = React.useState(false)
const[errorMessage,setErrorContent] = React.useState('')
const stripe = useStripe();
const elements = useElements();
function showCardError(event) {
console.log("showCardError")
if (event.error) {
setErrorContent(event.error.message)
setOpenSnack(true)
} else {
setErrorContent('event.error.message')
}
}
function retryInvoiceWithNewPaymentMethod(
customerId,
paymentMethodId,
invoiceId,
priceId
) {
console.log("retryInvoiceWithNewPaymentMethod")
return (
fetch('http://127.0.0.1:8000/api/user/retry_subscription/', {
method: 'post',
headers: {
'Content-type': 'application/json',
Authorization: `Bearer ${props.token}`
},
body: JSON.stringify({
customerId: customerId,
paymentMethodId: paymentMethodId,
invoiceId: invoiceId,
}),
})
.then((response) => {
return response.json();
})
// If the card is declined, display an error to the user.
.then((result) => {
if (result.error) {
console.log("1 throwing in retryInvoiceWithNewPaymentMethod")
console.log("1 result=",result)
// The card had an error when trying to attach it to a customer.
throw result;
}
return result;
})
// Normalize the result to contain the object returned by Stripe.
// Add the addional details we need.
.then((result) => {
return {
// Use the Stripe 'object' property on the
// returned result to understand what object is returned.
invoice: result,
paymentMethodId: paymentMethodId,
priceId: priceId,
isRetry: true,
};
})
// Some payment methods require a customer to be on session
// to complete the payment process. Check the status of the
// payment intent to handle these actions.
.then(handleCustomerActionRequired)
// No more actions required. Provision your service for the user.
.then(onSubscriptionComplete)
.catch((error) => {
console.log(" catching 2 in retryInvoiceWithNewPaymentMethod :",error)
// An error has happened. Display the failure to the user here.
// We utilize the HTML element we created.
showCardError(error);
})
);
}
function handlePaymentMethodRequired({
subscription,
paymentMethodId,
priceId,
}) {
console.log("handlePaymentMethodRequired")
if (subscription.status === 'active') {
console.log("considered active in handlePaymentMethodRequired")
// subscription is active, no customer actions required.
return { subscription, priceId, paymentMethodId };
} else if ( subscription.latest_invoice.payment_intent.status === 'requires_payment_method') {
console.log("subscription.latest_invoice.payment_intent.status=",subscription.latest_invoice.payment_intent.status)
// Using localStorage to manage the state of the retry here,
// feel free to replace with what you prefer.
// Store the latest invoice ID and status.
// faire des vraies variables de ça, car comme ça = de la merde
localStorage.setItem('latestInvoiceId', subscription.latest_invoice.id);
localStorage.setItem('latestInvoicePaymentIntentStatus', subscription.latest_invoice.payment_intent.status);
console.log("11 throwing in handlePaymentMethodRequired")
console.log("11 custom_throw=",{ error: { message: 'Your card was declined.' } })
throw { error: { message: 'Your card was declined.' } };
} else {
return { subscription, priceId, paymentMethodId };
}
}
function handleCustomerActionRequired({
subscription,
invoice,
priceId,
paymentMethodId,
isRetry,
}) {
console.log("handleCustomerActionRequired")
if (subscription && subscription.status === 'active') {
console.log("subscription exists and subscription.status ===active in handleCustomerActionRequired")
// Subscription is active, no customer actions required.
return { subscription, priceId, paymentMethodId };
}
// If it's a first payment attempt, the payment intent is on the subscription latest invoice.
// If it's a retry, the payment intent will be on the invoice itself.
let paymentIntent = invoice ? invoice.payment_intent : subscription.latest_invoice.payment_intent;
if (paymentIntent.status === 'requires_action' || (isRetry === true && paymentIntent.status === 'requires_payment_method')) {
console.log("paymentIntent.status:",paymentIntent.status)
console.log("paymentIntent.client_secret:",paymentIntent.client_secret)
return stripe
.confirmCardPayment(paymentIntent.client_secret, {
payment_method: paymentMethodId,
})
.then((result) => {
if (result.error) {
console.log("4")
console.log("Throwing 4 in handleCustomerActionRequired =",result)
// Start code flow to handle updating the payment details.
// Display error message in your UI.
// The card was declined (i.e. insufficient funds, card has expired, etc).
console.log("result ")
throw result;
} else {
if (result.paymentIntent.status === 'succeeded') {
console.log("result.paymentIntent.status:",result.paymentIntent.status)
console.log("in handleCustomerActionRequired")
// Show a success message to your customer.
setOpenedSuccessModal(true)
localStorage.removeItem('latestInvoiceId')
localStorage.removeItem('latestInvoicePaymentIntentStatus')
// There's a risk of the customer closing the window before the callback.
// We recommend setting up webhook endpoints later in this guide.
return {
priceId: priceId,
subscription: subscription,
invoice: invoice,
paymentMethodId: paymentMethodId,
};
}
}
})
.catch((error) => {
console.log("6")
console.log("Catching 6 in handleCustomerActionRequired=",error)
showCardError(error);
});
} else {
// No customer action needed.
return { subscription, priceId, paymentMethodId };
}
};
function onSubscriptionComplete(result) {
console.log("onSubscriptionComplete")
console.log("result.subscription.status=",result.subscription.status)
if (result.subscription.status === 'active') {
setOpenedSuccessModal(true)
localStorage.removeItem('latestInvoiceId')
localStorage.removeItem('latestInvoicePaymentIntentStatus')
}
}
function createSubscription({ customerId, paymentMethodId, priceId }) {
return (
fetch('http://127.0.0.1:8000/api/user/create_subscription/', {
method: 'post',
headers: {
'Content-type': 'application/json',
Authorization: `Bearer ${props.token}`
},
body: JSON.stringify({
customerId: customerId,
paymentMethodId: paymentMethodId,
priceId: priceId,
}),
})
.then((response) => {
return response.json();
})
// If the card is declined, display an error to the user.
.then((result) => {
if (result.error) {
console.log("7 throwing in createSubscription ")
console.log("7 result=",result)
// The card had an error when trying to attach it to a customer.
throw result;
}
return result;
})
// Normalize the result to contain the object returned by Stripe.
// Add the addional details we need.
.then((result) => {
//console.log("return=",result)
return {
paymentMethodId: paymentMethodId,
priceId: priceId,
subscription: result,
};
})
// Some payment methods require a customer to be on session
// to complete the payment process. Check the status of the
// payment intent to handle these actions.
// // If attaching this card to a Customer object succeeds,
// // but attempts to charge the customer fail, you
// // get a requires_payment_method error.
// // No more actions required. Provision your service for the user.
.then(handleCustomerActionRequired)
.then(handlePaymentMethodRequired)
.then(onSubscriptionComplete)
.catch((error) => {
// An error has happened. Display the failure to the user here.
// We utilize the HTML element we created.
console.log("8")
console.log("Catching 8 in createSubscription =",error)
showCardError({ error: { message: 'Your card was declined.' } });
})
);
}
function createPaymentMethod(cardElement, customerId, priceId) {
return stripe
.createPaymentMethod({
type: 'card',
card: cardElement,
})
.then((result) => {
if (result.error) {
console.log("9 in createPaymentMethod")
showCardError(result)
} else {
let latestInvoiceId = localStorage.getItem('latestInvoiceId');
let latestInvoicePaymentIntentStatus = localStorage.getItem('latestInvoicePaymentIntentStatus')
if (latestInvoiceId && latestInvoicePaymentIntentStatus==='requires_payment_method'){
retryInvoiceWithNewPaymentMethod(customerId, result.paymentMethod.id, latestInvoiceId,priceId)
}
else createSubscription({
customerId: customerId,
paymentMethodId: result.paymentMethod.id,
priceId: priceId,
});
}
});
}
const handleSubmit = async (event) => {
// We don't want to let default form submission happen here,
// which would refresh the page.
event.preventDefault();
console.log("priceId=",priceId)
console.log("customerId=",props.customerId)
if (!stripe || !elements) {
// Stripe.js has not yet loaded.
// Make sure to disable form submission until Stripe.js has loaded.
return;
}
await createPaymentMethod(elements.getElement(CardElement),props.customerId,priceId)
};
return (
<form onSubmit={handleSubmit}>
<FormControl component="fieldset">
<FormLabel component="legend">Plan</FormLabel>
<RadioGroup aria-label="product_id" name="product_id" value={priceId} onChange={(event)=>setPriceId(event.target.value)}>
<FormControlLabel value="price_1GvhOFC2ahMs23Dqf7E83kTk" control={<Radio />} label="EXPENSIVE Plan" />
<FormControlLabel value="price_1GqywBC2ahMs23DqyBiAZLZG" control={<Radio />} label="Veterinary Plan (15.99 / mois)" />
</RadioGroup>
</FormControl>
<CardSection/>
<button disabled={!stripe}>Save Card</button>
<ModalSuccessPayment is_open={openedSuccessModal} setIsOpen={setOpenedSuccessModal} text ={<Emoji text=" 🎉️ Your payment has been accepted and your subscription started" />} {...props}/>
{/*{snack}*/}
<Snackbar open={errorMessage.length > 0} autoHideDuration={600000} message={errorMessage}/>
</form>
);
}
README links to demo of project are broken.
Hi there,
Thanks for the demo. Do you have any demo with subscription management (and authentication ?) using the hosted checkout page?
When you create a checkout session without a PriceID, the server should respond with a 400 status code and
{
error: {
message: "some message"
}
}
Where "some message" is the result of e.getMessage()
The variable handleFetchResult (at https://github.com/stripe-samples/checkout-single-subscription/blob/master/client/script.js#L12) is not used anywhere.
Expect this variable to be used somewhere, perhaps in catch block for the fetch.
I am trying out Stripe checkout client-only integration. I am facing an issue which actually creates a new customer after checkout even though a customer with the same email id exists. Is there a way to handle it in a way that if a customer already exists with the same name add the subscription to the existing customer or create a new customer and add the subscription to the new customer
hey, I agree that it's an obvious mistake but is there any other valid reason or serious reason why the amount is different from the index page to the checkout page?
plan 5$ week become 5$ /month
plan 12$ week become 15$/month
I guess that this is just a parameter inside dashboard wrongly setup
I'm using the client-server code with python and added billing_address_collection="required",
to the server-side create checkout session code. With the payment form, the billing address is shown, but it is not stored with neither the customer nor the invoice.
.NET sample shows using env variables for retrieving secrets. There's actually a more secure way recommended by Microsoft.
The current example code shows the following in Startup.cs:
services.Configure<StripeOptions>(options =>
{
options.PublishableKey = Environment.GetEnvironmentVariable("STRIPE_PUBLISHABLE_KEY");
options.SecretKey = Environment.GetEnvironmentVariable("STRIPE_SECRET_KEY");
options.WebhookSecret = Environment.GetEnvironmentVariable("STRIPE_WEBHOOK_SECRET");
options.BasicPrice = Environment.GetEnvironmentVariable("BASIC_PRICE_ID");
options.ProPrice = Environment.GetEnvironmentVariable("PRO_PRICE_ID");
options.Domain = Environment.GetEnvironmentVariable("DOMAIN");
});
However, for greater security (outlined in the next link), individuals should use App Secrets to manage these things.
Example code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MyNamespace
{
/// <summary>
/// Holds our Stripe secrets.
/// </summary>
/// <remarks>
/// This is established for DI.
/// </remarks>
public interface IStripeApiSecrets
{
/// <summary>
/// The base URI for the API calls.
/// </summary>
/// <remarks>
/// This is so we can change between sandbox and live via config.
/// </remarks>
Uri ApiBaseUri
{
get;
}
/// <summary>
/// The publishable token.
/// </summary>
string PublishableKey
{
get;
}
/// <summary>
/// The secret token.
/// </summary>
string SecretKey
{
get;
}
}
/// <summary>
/// POCO object for retrieving our secret Stripe API data.
/// </summary>
/// <remarks>
/// See https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-3.1&tabs=linux#map-secrets-to-a-poco
/// </remarks>
public sealed class StripeApiSecrets : IStripeApiSecrets
{
/// <inheritdoc/>
public Uri ApiBaseUri
{
get;
set;
}
/// <inheritdoc/>
public string PublishableKey
{
get;
set;
}
/// <inheritdoc/>
public string SecretKey
{
get;
set;
}
}
}
Startup.cs
:// Init our Stripe secrets for taking payments.
var stripeSecrets = Configuration.GetSection(nameof(StripeApiSecrets)).Get<StripeApiSecrets>();
if (stripeSecrets == null)
{
throw new InvalidDataException($"{nameof(stripeSecrets)} is null.");
}
// For DI.
services.AddSingleton<IStripeApiSecrets>(stripeSecrets);
N/A
N/A
If applicable, add screenshots to help explain your problem.
Server Env: .NET
N/A
A clear and concise description of what the bug is.
Steps to reproduce the behavior, please provide code snippets or a repository:
A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
Add any other context about the problem here.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.