Expo, React Native, Stripe: Setting up future payment methods not working - ios

I’m in desperate need for help.
So I have a side project for an iOS app using Expo / React Native. And I'm having issues with setting up future payment methods using Stripe & Expo’s stripe library.
Our back-ender set up a graphql back-end, and provides me with all the variables I need. I’m trying to set up future payments to charge clients later, but I’m having trouble having with the paymentIntentSheet not showing up after creating an intent and fetching the clientSecret, ephemeralKey and customerId from our back-end. Now i don’t know where the issue is.. Is it because of me using the wrong versions? Maybe incorrect installation? Are the variables I’m using right..?
I used the following documentation page(s) as a guide:
https://stripe.com/docs/payments/save-and-reuse?platform=react-native
https://github.com/stripe/stripe-react-native#expo
These are the version numbers of the libraries I’m using, relevant to this topic/issue:
"expo": "~41.0.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz",
"#stripe/stripe-react-native": "0.1.1"
These are the steps I took:
Install stripe-react-native, and add it to my app.json as a plugin:
"plugins": [
[
"#stripe/stripe-react-native",
{
"merchantIdentifier": "",
"enableGooglePay": false
}
]
],
On global level, I import the StripeProvider component and pass down the given publishable key:
pk_live_51[.....]
On global level it’ll look like this:
<StripeProvider
publishableKey="pk_live_51[...]"
>
<AuthProvider>
<ApolloProvider client={client}>
<InnerApp />
</ApolloProvider>
</AuthProvider>
</StripeProvider>
Then according to the stripe docs, at the component where I'll be setting up future payments, I am supposed to fetch the setupIntent, ephemeralKey, and the customer from the back-end. In this case, it's done in the useEffect of my component. I was provided with a graphql mutation to obtain these values:
mutation (
$createUserPaymentMethodSetupIntentInput: CreateUserPaymentMethodSetupIntentInput!
) {
createUserPaymentMethodSetupIntent(
input: $createUserPaymentMethodSetupIntentInput
) {
setupIntentId
clientSecret
customerId
ephemeralKeySecret
}
}
I then call the function that will eventually provide me with all the necessary variables:
createIntent({
variables: {
createUserPaymentMethodSetupIntentInput: {
userUid: userUid,
},
},
})
.then((res) => {
const clientSecret =
res.data.createUserPaymentMethodSetupIntent.clientSecret
const setupIntentId =
res.data.createUserPaymentMethodSetupIntent.setupIntentId
const ephemeralKeySecret =
res.data.createUserPaymentMethodSetupIntent.ephemeralKeySecret
const customerId =
res.data.createUserPaymentMethodSetupIntent.customerId
// IGNORE THIS FOR NOW
initializePaymentSheet(
clientSecret,
setupIntentId,
ephemeralKeySecret,
customerId
)
})
.catch((err) => console.log({ graphqlError: err }))
The function gives me the following response:
Object {
"data": Object {
"createUserPaymentMethodSetupIntent": Object {
"__typename": "CreatedUserPaymentMethodSetupIntent",
"clientSecret": "seti_1K[....]",
"customerId": "cus_[...]",
"ephemeralKeySecret": "ek_live_[...]",
"setupIntentId": "seti_[...]",
},
},
According to the docs, I should use the setupIntent, ephemeralKey, and customer values as variables in one of their given functions/hooks called “initPaymentSheet” which should initialize the paymentsheet on their end.
These functions are imported like this:
const { initPaymentSheet, presentPaymentSheet } = useStripe();
In step 3, you see that I call a function that then calls the initPaymentSheet after successfully fetching the values from the server.
initializePaymentSheet(
clientSecret,
setupIntentId,
ephemeralKeySecret,
customerId
)
The initializePaymentSheet function looks like this:
const initializePaymentSheet = (
clientSecret,
setupIntentId,
ephemeralKeySecret,
customerId
) => {
initPaymentSheet({
customerId: customerId,
customerEphemeralKeySecret: ephemeralKeySecret,
setupIntentClientSecret: setupIntentId,
})
.then((res) => {
console.log(res)
setDisabledButton(false)
})
.catch((err) => console.log("error.."))
}
As you can see, I call the initPaymentSheet hook there, exactly like shown on the docs, and pass in the values i received from the back-end. However, after doing this i get the following error in the console:
Object {
"error": Object {
"code": "Failed",
"message": "You must provide the paymentIntentClientSecret",
},
}
This didn’t seem like a huge error, so I went ahead and changed the initPaymentSheet parameters by adding the paymentIntentClientSecret field and passed in the clientSecret value which wasn’t previously used:
initPaymentSheet({
customerId: customerId,
customerEphemeralKeySecret: ephemeralKeySecret,
setupIntentClientSecret: setupIntentId,
paymentIntentClientSecret: clientSecret
})
.then((res) => {
console.log(res)
setDisabledButton(false)
})
.catch((err) => console.log("little error.."))
After calling the function and seeing the error disappear, and the console.log shown above logs the following in the console:
Object {
"paymentOption": null,
}
I didn’t think too much of this, and thought it says null just because I have no previously set paymentOptions. I was just happy there were no more errors.
In the .then chain, you see that i enable a button that basically allows a user to call a function that would present a payment sheet where users can submit their paymentMethod. This button is disabled, because I think you should initialize the paymentSheet first before enabling it?
<WideButton
disabled={disabledButton}
text="Add New Payment Method"
clicked={openPaymentSheet}
/>
Anyways, now that the button is finally enabled, the user can click on it and it'll call the following function:
const openPaymentSheet = async () => {
setDisabledButton(true)
const { error, paymentOption } = await presentPaymentSheet()
if (error) {
console.log(error)
setDisabledButton(false)
Alert.alert(`Error code: ${error.code}`, error.message)
}
if (paymentOption) {
setDisabledButton(false)
Alert.alert(
"Success",
"Your payment method is successfully set up for future payments!"
)
console.log(paymentOption)
}
}
Now to quote the stripe docs:
When your customer taps the Set up button, call presentPaymentSheet() to open the sheet. After the customer completes setting up their payment method for future use, the sheet is dismissed and the promise resolves with an optional StripeError.
So, that's exactly what I did: Call the presentPaymentSheet, but then i get the following error:
Object {
"code": "Failed",
"message": "There was an unexpected error -- try again in a few seconds",
}
Now this is where I’m stuck, because it doesn’t provide me with any more information than given above. I’ve tried looking everywhere, and some resources tell me that I should update my stripe, some say i should add stripe to my plugins in app.json. I’ve done all of that and I can’t still figure it out.
Here is a video showing you the behavior in action:
https://user-images.githubusercontent.com/29804130/146274443-82c581ba-8913-4c87-ad2e-5b8719680fed.mov
Here is the code of the entire component:
// steps
// 1. call graphql query to set up intent, retrieve the clientsecret and setupintentid
// 2. call stripes initPaymentSheet's function and pass in useruid, clientsecret and setupintentid
// 3. when initpaymentsheet is ready, enable button for user to add payment information
// 4. Retrieve the payment information and call the createpaymentmethod mutation
// 5. disable button again, and refresh page
export default function PaymentMethods({ userUid }) {
const { initPaymentSheet, presentPaymentSheet } = useStripe()
const [disabledButton, setDisabledButton] = useState(false)
const [createIntent, { data, loading, error }] = useMutation(
ADD_PAYMENT_METHOD_INTENT
)
useEffect(() => {
createUserPaymentMethodIntent()
}, [])
const createUserPaymentMethodIntent = () => {
setDisabledButton(true)
createIntent({
variables: {
createUserPaymentMethodSetupIntentInput: {
userUid: userUid,
},
},
})
.then((res) => {
console.log(res)
const clientSecret =
res.data.createUserPaymentMethodSetupIntent.clientSecret
const setupIntentId =
res.data.createUserPaymentMethodSetupIntent.setupIntentId
const ephemeralKeySecret =
res.data.createUserPaymentMethodSetupIntent.ephemeralKeySecret
const customerId =
res.data.createUserPaymentMethodSetupIntent.customerId
initializePaymentSheet(
clientSecret,
setupIntentId,
ephemeralKeySecret,
customerId
)
})
.catch((err) => console.log({ graphqlError: err }))
}
const initializePaymentSheet = (
clientSecret,
setupIntentId,
ephemeralKeySecret,
customerId
) => {
initPaymentSheet({
customerId: customerId,
customerEphemeralKeySecret: ephemeralKeySecret,
setupIntentClientSecret: setupIntentId,
paymentIntentClientSecret: clientSecret,
})
.then((res) => {
console.log(res)
setDisabledButton(false)
})
.catch((err) => console.log("little error.."))
}
const openPaymentSheet = async () => {
setDisabledButton(true)
const { error } = await presentPaymentSheet()
if (error) {
Alert.alert(`Error code: ${error.code}`, error.message)
} else {
Alert.alert(
"Success",
"Your payment method is successfully set up for future payments!"
)
}
}
return (
<ScrollView>
<PaymentMethodList userUid={userUid} />
<WideButton
disabled={disabledButton}
text="Add New Payment Method"
clicked={openPaymentSheet}
/>
</ScrollView>
)
}
someone plz help :(

you might want to check the logs in your Stripe Dashboard (Dashboard -> Developers -> Logs). From there you'll be able to see more info about this error,

Related

How to add to WebSocket response some additional information?

I am using Dart Alfred framework. And learning websockets.
Here is implamentation from example:
var users = <WebSocket>[];
app.get('/ws', (req, res) {
return WebSocketSession(
onOpen: (ws) {
users.add(ws);
users
.where((user) => user != ws)
.forEach((user) => user.send('A new user joined the chat.'));
},
onClose: (ws) {
users.remove(ws);
users.forEach((user) => user.send('A user has left.'));
},
onMessage: (ws, dynamic data) async {
users.forEach((user) => user.send(data));
},
);
});
https://github.com/rknell/alfred#websockets
I can't figure out how to return some additional data for every user to client.
For example (let's simplify) it's country from server. For example I have next map:
Map cuntries = {
'Mike': 'USA',
'Piter': 'Holland',
'Jow': 'Italy'
};
I did not worked with WebSocket before. Could anybody provide example how to do it?

TWILIO : Filtering TaskQueues with "evaluateWorkerAttributes" resulting in Time Out error

I'm facing a "runtime application timed out" error whenever I try to filter TaskQueues. Here is the complete scenario.
On the twilio flex contact pad, I have to show cumulative count of pending tasks from all the task queues, that the agent belongs to. For this, I have written a twilio function which takes the worker skills as input and filters TaskQueues based on skills supplied, and count the pending tasks. I followed the twilio documentation provided here.
I tried different ways to write the expression, but all are eventually resulting in time out error.
The worker attributes are defined as below
{
"routing": {
"skills": [
"German",
"English",
"French"
],
"domains": [
"ABC"
],
"categories": [
"XYZ"
],
"levels": {
"XYZ": 34
},
"platforms": [
"Platform1"
],
"provider": "Provider_1"
},
"full_name": "XXXXXXX",
"image_url": "https:\/\/www.avatar.co\/avatar\/91f0e496?d=mp",
"roles": [
"admin",
"wfo.full_access"
],
"contact_uri": "client:XXX.YYYY",
"disabled_skills": {
"skills": [],
"levels": {}
},
"email": "test#email.com"
}
Below the code snippet to filter the Queues.
const TokenValidator = require('twilio-flex-token-validator').functionValidator;
const WORKSPACE_ID = 'WSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
const workerSkills = ['English','French'];
console.log('SKILLS IN FUNC : ' + `routing.skills IN [${workerSkills}]`);
const getQueues = (client) => {
return new Promise ((resolve, reject) => {
client.taskrouter.v1.workspaces(WORKSPACE_ID)
.taskQueues
.list({evaluateWorkerAttributes:'{"routing.skills" : "${workerSkills}" }',
//.list({evaluateWorkerAttributes:"routing.skills HAS 'French'",
limit: 1000
})
.then(taskQueues => resolve(taskQueues))
});
}
const getQueueRealTimeStatistics = (client, queueID) => {
return new Promise ((resolve, reject) => {
client.taskrouter.v1.workspaces(WORKSPACE_ID)
.taskQueues(queueID)
.realTimeStatistics()
.fetch()
.then(realTimeStatistics => resolve(realTimeStatistics))
});
}
exports.handler = function(context, event, callback) {
const client = context.getTwilioClient();
let pendingTasks = 0;
const promises = [];
const response = new Twilio.Response();
response.appendHeader('Access-Control-Allow-Origin', '*');
response.appendHeader('Access-Control-Allow-Methods', 'OPTIONS POST GET');
response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');
response.appendHeader('Content-Type', 'application/json');
getQueues(client).then(res => res.forEach(x => {
promises.push(getQueueRealTimeStatistics(client, x.sid));
console.log('queueID : ' + x.sid);
})).then(x => {
Promise.all(promises).then(res => {
res.forEach(y => {
pendingTasks = pendingTasks+ parseInt(y.tasksByStatus.pending,10);
})
//response.setBody(pendingTasks);
response.setBody(pendingTasks + parseInt(res, 10));
})
.then(x => callback(null, response))
.catch(err => {
console.log(err.message);
response.appendHeader('Content-Type', 'plain/text');
response.setBody(err.message);
response.setStatusCode(500);
// If there's an error, send an error response
// Keep using the response object for CORS purposes
callback(null, response);
});
})
};
Can someone help to resolve this issue. Thanks in advance.
Twilio developer evangelist here.
It looks like your function is written mostly correctly and should return. There's two things I can think of that might be going wrong.
It really is timing out
I'm not sure how many queues you have, but getting all the queues and then getting all the statistics for each queue could just be pushing your Function over the 10 second limit for Function execution. If that is the case, then may have to break up the Function so that it can run within the time. Are you able to run this locally (using the Twilio Serverless Toolkit) and it succeed?
There's an error that is not being caught and the promises don't resolve
Your code looks right to me, but the way you wrap the results of API calls in a new promise means that if there is an error the wrapper promise never resolves or rejects. This would mean that the Function hangs waiting for a result until the timeout occurs.
Thing is, you don't need to wrap the API calls in a new Promise, they already return Promises.
I would try rewriting it like this, avoiding new Promise wrappers and then seeing if there is an error that you aren't catching. You can also avoid pushing promises into an array and jump straight into the Promise.all too:
const TokenValidator = require("twilio-flex-token-validator").functionValidator;
const WORKSPACE_ID = "WSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const workerSkills = ["English", "French"];
console.log("SKILLS IN FUNC : " + `routing.skills IN [${workerSkills}]`);
const getQueues = (client) => {
return client.taskrouter.v1.workspaces(WORKSPACE_ID).taskQueues.list({
evaluateWorkerAttributes: '{"routing.skills" : "${workerSkills}" }',
//.list({evaluateWorkerAttributes:"routing.skills HAS 'French'",
limit: 1000,
});
};
const getQueueRealTimeStatistics = (client, queueID) => {
return client.taskrouter.v1
.workspaces(WORKSPACE_ID)
.taskQueues(queueID)
.realTimeStatistics()
.fetch();
};
exports.handler = function (context, event, callback) {
const client = context.getTwilioClient();
let pendingTasks = 0;
const response = new Twilio.Response();
response.appendHeader("Access-Control-Allow-Origin", "*");
response.appendHeader("Access-Control-Allow-Methods", "OPTIONS POST GET");
response.appendHeader("Access-Control-Allow-Headers", "Content-Type");
response.appendHeader("Content-Type", "application/json");
getQueues(client)
.then((queues) => {
return Promise.all(
queues.map((queue) => {
console.log("queueID : " + queue.sid);
return getQueueRealTimeStatistics(client, queue.sid);
})
);
})
.then((queueStats) => {
queueStats.forEach((queueStat) => {
pendingTasks = pendingTasks + parseInt(queueStat.tasksByStatus.pending, 10);
});
response.setBody({ pendingTasks });
})
.then(() => callback(null, response))
.catch((err) => {
console.log(err.message);
response.appendHeader("Content-Type", "plain/text");
response.setBody(err.message);
response.setStatusCode(500);
// If there's an error, send an error response
// Keep using the response object for CORS purposes
callback(null, response);
});
};
Doing it this way cuts down some of the nesting too. Let me know if it helps.

How edit asset name?

How i can edit asset name? its doesnt work. Thanks
let assetService = $injector.get(self.ctx.servicesMap.get('assetService'));
let activeID = self.ctx.data[0].datasource.entityId
let tenantId = self.ctx.dashboard.authUser.tenantId
let asset = {
additionalInfo: null,
createdTime: 1599121131415, // временно
customerId: {
entityType: "CUSTOMER",
id: self.ctx.dashboard.authUser.customerId
},
id: {
entityType: "ASSET",
id: activeID
},
label: null,
name: "kuku", // временно
tenantId: {
entityType: "TENANT",
id: tenantId
},
type: "справочник"
}
assetService.saveAsset(asset)
Thingsboard is built using Angular 10 currently See releases. You correctly injected the Angular service 'assetService'. You need to follow the Angular method of subscribing to the observable from assetService.
Calling
assetService.saveAsset(asset)
without subscribing means nothing happens. From the Angular University Blog
The multiple versions of the Angular HTTP module all have an RxJS Observable-based API. This means that the multiple calls to the HTTP module will all return an observable, that we need to subscribe to one way or the other.
So here's the code to 'subscribe' to the observable described above
assetService.saveAsset(asset).subscribe(
(response) => {
console.log(
"saveAsset call Success:",
response);
},
response => {
console.log(
"saveAsset call Error:",
response);
},
() => {
console.log(
"saveAsset observable Complete"
);
});
Let me know if there's a mistake in the code above, I didn't test it. And thanks for your question Anzor - it led me to a solution to make a custom Thingsboard widget along with the Widgets Development Guide.

Stripe React Native Subscriptions

I'm stuck on this issue:
When I try to submit this subscription, I got an error of "subscription_payment_intent_requires_action", should I handle it on client-side or there is another way to do it?
For information: I already created setupIntent (verify 3D Secure) and return payment_method
Steps:
1 - create setupIntent
2 - generate payment_method ( verify 3D Secure)
3 - create Customer and attach payment_method to it
4 - create Subscription (on back_end) => error: "subscription_payment_intent_requires_action" shows up!
Thank you all!
// Create Subscription With Payment Method + Customer ID
router.post('/createSubscription', function (req, res){
const {payment_method, customerId} = req.body;
var stripe = require("stripe")(stripe_sk);
try{
stripe.subscriptions
.create({
customer: customerId,
items: [{
plan: 'plan_HTGCI8ljPYFTHQ'
}],
default_payment_method: payment_method,
expand: ["latest_invoice.payment_intent"],
// enable_incomplete_payments: true
}).then(subscription => {
res.send({
subscription : subscription
})
}).catch(err => {
res.send({
err
})
})
} catch (error) {
res.send("Error : ", error);
}
});```

Apollo Subscription doesn't seem to get called on Mutation

New to Apollo, so I decided to take the most simple example I found and try to work it in a slightly different way. My code can be found here.
The problem I am having is that the Subscription doesn't seem to get called when I call the Mutation createTask(). The Mutation and Subscription are defined in schema.graphql as:
type Mutation {
createTask(
text: String!
): Task
}
type Subscription {
taskCreated: Task
}
And in resolvers.js as:
Mutation: {
createTask(_, { text }) {
const task = { id: nextTaskId(), text, isComplete: false };
tasks.push(task);
pubsub.publish('taskCreated', task);
return task;
},
},
Subscription: {
taskCreated(task) {
console.log(`Subscript called for new task ID ${task.id}`);
return task;
},
},
What I am expecting to happen is that I would get a console.log in the server every time I run the following in the client:
mutation Mutation($text: String!) {
createTask(text:$text) {
id
text
isComplete
}
}
But nothing happens. What am I missing?
The subscription resolver function is called when there is actually a subscription to the GraphQL Subscription.
As you did not add a client which uses subscriptions-transport-ws and the SubscriptionClient for subscribing to your websocket and the subscription it will not work.
What you could do is add the subscription Channel to the setupFunctions of the SubscriptionManager and therein you get the value that the pubsub.publish function delivers.
Could look like this:
...
const WS_PORT = 8080;
const websocketServer = createServer((request, response) => {
response.writeHead(404);
response.end();
});
websocketServer.listen(WS_PORT, () => console.log( // eslint-disable-line no-console
`Websocket Server is now running on http://localhost:${WS_PORT}`
));
const subscriptionManager = new SubscriptionManager({
schema: executableSchema,
pubsub: pubsub,
setupFunctions: testRunChanged: (options, args) => {
return {
taskCreated: {
filter: (task) => {
console.log(task); // sould be log when the pubsub is called
return true;
}
},
};
},
,
});
subscriptionServer = new SubscriptionServer({
subscriptionManager: subscriptionManager
}, {
server: websocketServer,
path: '/',
});
...

Resources