Related
I have integrated flutter_stripe 2.0.1 plugins in my flutter application using Firebase CLI. getting a successful response of paymentIntent from firebase provided URL. But in the Stripe dashboard payment status is Incomplete.
Below is my node.js code
index.js file
const functions = require('firebase-functions');
const stripe = require('stripe')(functions.config().stripe.testkey);
exports.stripePayment = functions.https.onRequest(async (req, res) => {
const paymentIntent = await stripe.paymentIntents.create({
amount: 1,
currency: 'usd'
},
function(err, paymentIntent) {
if(err != null){
console.log(err);
} else {
res.json({
paymentIntent: paymentIntent.client_secret,
})
}
}
)
});
Below is my flutter code
void main() async {
WidgetsFlutterBinding.ensureInitialized();
Stripe.publishableKey = stripePublishableKey;
Stripe.merchantIdentifier = 'merchant.flutter.stripe.test'; //any string works
await Stripe.instance.applySettings();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: DashboradPage(),
);
}
}
class DashboradPage extends StatefulWidget {
const DashboradPage({Key? key}) : super(key: key);
#override
_DashboradPageState createState() => _DashboradPageState();
}
class _DashboradPageState extends State<DashboradPage> {
Map<String, dynamic>? paymentIntentData;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Stripe Examples'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
makePayment();
},
child: Text('Pay Amount'),
),
),
);
}
Future<void> makePayment() async {
final url = Uri.parse(
'https://us-central1-stripeflutterdemo.cloudfunctions.net/stripePayment');
final header = {'Content-Type': 'application/json'};
try {
final response = await http.get(url, headers: header);
paymentIntentData = json.decode(response.body);
await Stripe.instance.initPaymentSheet(
paymentSheetParameters: SetupPaymentSheetParameters(
paymentIntentClientSecret: paymentIntentData?['paymentIntent'] ?? '',
applePay: true,
googlePay: true,
style: ThemeMode.light,
merchantCountryCode: 'US',
merchantDisplayName: 'Flutter Stripe Store Demo',
));
setState(() {});
displayPaymentSheet();
} catch (e) {
print(e);
displaySnackbar(e.toString());
}
}
Future<void> displayPaymentSheet() async {
try {
await Stripe.instance.presentPaymentSheet();
// await Stripe.instance.presentPaymentSheet(
// parameters: PresentPaymentSheetParameters(
// clientSecret: paymentIntentData?['paymentIntent'] ?? '',
// confirmPayment: true,
// ),
// );
setState(() {
paymentIntentData = null;
});
displaySnackbar('Payment succesfully completed');
} catch (e) {
print(e);
displaySnackbar(e.toString());
}
}
void displaySnackbar(String msg) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(msg),
),
);
}
}
Below is the postman response of URL mentioned in makePayment()
I have used payment sheet, when I clicked on the Pay button it is redirecting to webview (Authentication completed) but in the Stripe dashboard payment status is Incomplete.
Stripe Dashboard Payment Event data
PaymentIntent status:requires_payment_method,
PaymentIntent status:requires_action
From Stripe
payment_intent.payment_failed
View event detail
Event data
{
"id": "pi_3JpZh7SIKwX0CSUQ0sUoM9wK",
"object": "payment_intent",
"last_payment_error": {
"message": "As per Indian regulations, export transactions require a description. More info here: https://stripe.com/docs/india-exports",
"param": "description",
"payment_method": {
"id": "pm_1JpZijSIKwX0CSUQ7u91wths",
"object": "payment_method",
"billing_details": {
"address": {
"city": null,
"country": "US",
"line1": null,
"line2": null,
"postal_code": "45612",
"state": null
},
"email": null,
"name": null,
"phone": null
},
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": "unchecked",
"cvc_check": "unchecked"
},
"country": "US",
"exp_month": 4,
"exp_year": 2044,
"fingerprint": "EK9VAoccqqCLBqQk",
"funding": "credit",
"generated_from": null,
"last4": "4242",
"networks": {
"available": [
"visa"
],
"preferred": null
},
"three_d_secure_usage": {
"supported": true
},
"wallet": null
},
"created": 1635431806,
"customer": null,
"livemode": false,
"metadata": {
},
"type": "card"
},
"type": "invalid_request_error"
},
"livemode": false,
"next_action": null,
"status": "requires_payment_method",
"amount": 1,
"amount_capturable": 0,
"amount_received": 0,
"application": null,
"application_fee_amount": null,
"canceled_at": null,
"cancellation_reason": null,
"capture_method": "automatic",
"charges": {
"object": "list",
"data": [
],
"has_more": false,
"total_count": 0,
"url": "/v1/charges?payment_intent=pi_3JpZh7SIKwX0CSUQ0sUoM9wK"
},
"client_secret": "pi_3JpZh7SIKwX0CSUQ0sUoM9wK_secret_oVhK5CmrcHjImvFMyoo51GCXy",
"confirmation_method": "automatic",
"created": 1635431705,
"currency": "usd",
"customer": null,
"description": null,
"invoice": null,
"metadata": {
},
"on_behalf_of": null,
"payment_method": null,
"payment_method_options": {
"card": {
"installments": null,
"network": null,
"request_three_d_secure": "automatic"
}
},
"payment_method_types": [
"card"
],
"receipt_email": null,
"review": null,
"setup_future_usage": "off_session",
"shipping": null,
"source": null,
"statement_descriptor": null,
"statement_descriptor_suffix": null,
"transfer_data": null,
"transfer_group": null
}
Below is the iPhone screenshot
According to the Stripe documentation, the required_payment_method message as a status means you have not attached a payment method to your payment intent:
When the PaymentIntent is created, it has a status of requires_payment_method until a payment method is attached.
There is an official document explaining how to integrate Stripe payments with your Firebase backend. It provides all the required code and steps to properly set up payments with Stripe. In the sample code inside the index.js file, they include additional objects when creating the payment intent with stripe.paymentIntents.create()
const payment = await stripe.paymentIntents.create(
{
amount,
currency,
customer,
Payment_method, //payment method when creating the payment intent
off_session: false,
confirm: true,
confirmation_method: 'manual',
},
{ idempotencyKey }
);
Since your sample is missing the payment method, it could explain the error you are receiving, as the PaymentIntent is being created without a payment method. I recommend following the guide to cover all the required steps to avoid further errors that might be present. You can also see the full sample code, which is documented and can be customized for your use case and the flutter_stripe library.
I am trying to create a Tinder-like swipe functionality with the Youtube videos. I'll provide a detailed description of what I am trying to achieve.
Step by Step breakdown:
Fetch the Youtube videos using the Youtube Data API v3.
youtube _model.dart
// To parse this JSON data, do
//
// final youtubeSearchVideos = youtubeSearchVideosFromJson(jsonString);
import 'dart:convert';
YoutubeSearchVideos youtubeSearchVideosFromJson(String str) =>
YoutubeSearchVideos.fromJson(json.decode(str));
String youtubeSearchVideosToJson(YoutubeSearchVideos data) =>
json.encode(data.toJson());
class YoutubeSearchVideos {
YoutubeSearchVideos({
required this.kind,
required this.etag,
this.nextPageToken,
this.prevPageToken,
required this.regionCode,
required this.pageInfo,
required this.items,
});
String kind;
String etag;
String? nextPageToken;
String? prevPageToken;
String regionCode;
PageInfo pageInfo;
List<Item> items;
factory YoutubeSearchVideos.fromJson(Map<String, dynamic> json) =>
YoutubeSearchVideos(
kind: json["kind"],
etag: json["etag"],
nextPageToken: json["nextPageToken"],
prevPageToken: json["prevPageToken"],
regionCode: json["regionCode"],
pageInfo: PageInfo.fromJson(json["pageInfo"]),
items: List<Item>.from(json["items"].map((x) => Item.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"kind": kind,
"etag": etag,
"nextPageToken": nextPageToken,
"prevPageToken": prevPageToken,
"regionCode": regionCode,
"pageInfo": pageInfo.toJson(),
"items": List<dynamic>.from(items.map((x) => x.toJson())),
};
}
class Item {
Item({
required this.kind,
required this.etag,
required this.id,
required this.snippet,
});
String kind;
String etag;
Id id;
Snippet snippet;
factory Item.fromJson(Map<String, dynamic> json) => Item(
kind: json["kind"],
etag: json["etag"],
id: Id.fromJson(json["id"]),
snippet: Snippet.fromJson(json["snippet"]),
);
Map<String, dynamic> toJson() => {
"kind": kind,
"etag": etag,
"id": id.toJson(),
"snippet": snippet.toJson(),
};
}
class Id {
Id({
required this.kind,
required this.videoId,
});
String kind;
String videoId;
factory Id.fromJson(Map<String, dynamic> json) => Id(
kind: json["kind"],
videoId: json["videoId"],
);
Map<String, dynamic> toJson() => {
"kind": kind,
"videoId": videoId,
};
}
class Snippet {
Snippet({
required this.publishedAt,
required this.channelId,
required this.title,
required this.description,
required this.thumbnails,
required this.channelTitle,
required this.liveBroadcastContent,
required this.publishTime,
});
DateTime publishedAt;
String channelId;
String title;
String description;
Thumbnails thumbnails;
String channelTitle;
String liveBroadcastContent;
DateTime publishTime;
factory Snippet.fromJson(Map<String, dynamic> json) => Snippet(
publishedAt: DateTime.parse(json["publishedAt"]),
channelId: json["channelId"],
title: json["title"],
description: json["description"],
thumbnails: Thumbnails.fromJson(json["thumbnails"]),
channelTitle: json["channelTitle"],
liveBroadcastContent: json["liveBroadcastContent"],
publishTime: DateTime.parse(json["publishTime"]),
);
Map<String, dynamic> toJson() => {
"publishedAt": publishedAt.toIso8601String(),
"channelId": channelId,
"title": title,
"description": description,
"thumbnails": thumbnails.toJson(),
"channelTitle": channelTitle,
"liveBroadcastContent": liveBroadcastContent,
"publishTime": publishTime.toIso8601String(),
};
}
class Thumbnails {
Thumbnails({
required this.thumbnailsDefault,
required this.medium,
required this.high,
});
Default thumbnailsDefault;
Default medium;
Default high;
factory Thumbnails.fromJson(Map<String, dynamic> json) => Thumbnails(
thumbnailsDefault: Default.fromJson(json["default"]),
medium: Default.fromJson(json["medium"]),
high: Default.fromJson(json["high"]),
);
Map<String, dynamic> toJson() => {
"default": thumbnailsDefault.toJson(),
"medium": medium.toJson(),
"high": high.toJson(),
};
}
class Default {
Default({
required this.url,
required this.width,
required this.height,
});
String url;
int width;
int height;
factory Default.fromJson(Map<String, dynamic> json) => Default(
url: json["url"],
width: json["width"],
height: json["height"],
);
Map<String, dynamic> toJson() => {
"url": url,
"width": width,
"height": height,
};
}
class PageInfo {
PageInfo({
required this.totalResults,
required this.resultsPerPage,
});
int totalResults;
int resultsPerPage;
factory PageInfo.fromJson(Map<String, dynamic> json) => PageInfo(
totalResults: json["totalResults"],
resultsPerPage: json["resultsPerPage"],
);
Map<String, dynamic> toJson() => {
"totalResults": totalResults,
"resultsPerPage": resultsPerPage,
};
}
youtube_api_service.dart
import 'package:http/http.dart' as http;
import 'package:starcast_intros/models/youtube_search.dart';
import 'package:starcast_intros/private_keys.dart';
class YoutubeApi {
static const String youtubeAPI =
'https://youtube.googleapis.com/youtube/v3/search?part=snippet&maxResults=5&q=surfing&type=video&videoDefinition=standard&videoDimension=2d&videoDuration=short&videoEmbeddable=true&key=$YOUTUBE_DATA_API_KEY';
Future<YoutubeSearchVideos> fetchVideos() async {
try {
final response = await http.get(Uri.parse(youtubeAPI));
if (response.statusCode == 200) {
return youtubeSearchVideosFromJson(response.body);
}
throw Exception('Failed to fetch videos ${response.body}');
} catch (e) {
print(e);
throw Exception('Failed to fetch videos $e');
}
}
}
2. After retrieving the list of youtube video IDs from the API, render the Youtube videos like Tinder cards which can be swiped left or right.
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:starcast_intros/models/youtube_search.dart';
import 'package:starcast_intros/services/youtube_api.dart';
import 'package:tcard/tcard.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
static const HOME = 'Home';
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
late Future<YoutubeSearchVideos> futureVideos;
#override
void initState() {
super.initState();
final youtubeAPI = YoutubeApi();
futureVideos = youtubeAPI.fetchVideos();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<YoutubeSearchVideos>(
future: futureVideos,
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Widget> cards = List.generate(
snapshot.data!.items.length,
(int index) {
YoutubePlayerController _controller = YoutubePlayerController(
initialVideoId: snapshot.data!.items[index].id.videoId,
flags: YoutubePlayerFlags(
autoPlay: false,
mute: true,
isLive: false,
disableDragSeek: true,
loop: false,
forceHD: false,
),
);
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.0),
boxShadow: [
BoxShadow(
offset: Offset(0, 17),
blurRadius: 23.0,
spreadRadius: -13.0,
color: Colors.black54,
)
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: YoutubePlayer(
controller: _controller,
),
),
);
},
);
return TCard(
size: Size(
MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height,
),
cards: cards,
);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return SpinKitDoubleBounce(
color: Theme.of(context).accentColor,
size: 75.0,
);
},
);
}
}
Please note that you would need a Youtube API key (create it using Google Console) to retrieve the list of videos. I am using the Youtube Search API. Probably, you can use the below-given JSON if you do not wanna make a request or create an API key:
{ "kind": "youtube#searchListResponse", "etag":
"E2FpjhO0gVzn8gmf9Q1VSJ72Rwk", "nextPageToken": "CAUQAA",
"regionCode": "IN", "pageInfo": {
"totalResults": 1000000,
"resultsPerPage": 5 }, "items": [
{
"kind": "youtube#searchResult",
"etag": "HmJbuO71viHMk8216TydPkfPIAg",
"id": {
"kind": "youtube#video",
"videoId": "xIspFfN3vfs"
},
"snippet": {
"publishedAt": "2020-03-06T22:13:55Z",
"channelId": "UCR03gYk1xLMV4ko8ljxTeIA",
"title": "Nick O'Bea : How to Wing surf",
"description": "Wing Surfing How to: Skills and Drills on the wing with Nick O'Bea 5'1"x26" 90 liters from supsurfmachines.com 6
meter F1 Swing in 10-12 mph 1020 AXIS ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/xIspFfN3vfs/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/xIspFfN3vfs/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/xIspFfN3vfs/hqdefault.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "Rush Mark Rush",
"liveBroadcastContent": "none",
"publishTime": "2020-03-06T22:13:55Z"
}
},
{
"kind": "youtube#searchResult",
"etag": "PbsGbvz61v4Fpq4DwtLexeIB708",
"id": {
"kind": "youtube#video",
"videoId": "0zO26j6vNGg"
},
"snippet": {
"publishedAt": "2016-08-10T08:07:07Z",
"channelId": "UCjYiM-YLuOQN-4S0I7PfgVg",
"title": "Alison Teal: Surfing Hawaii Volcano Eruption",
"description": "Kilauea Volcano is erupting on the Big Island of Hawaii and flowing into the ocean for the first time since 2011.
Alison, a surfer and film maker, travels the world ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/0zO26j6vNGg/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/0zO26j6vNGg/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/0zO26j6vNGg/hqdefault.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "Alison's Adventures",
"liveBroadcastContent": "none",
"publishTime": "2016-08-10T08:07:07Z"
}
},
{
"kind": "youtube#searchResult",
"etag": "Ab0zXs-KAbFMVeH4jBajBEj60yU",
"id": {
"kind": "youtube#video",
"videoId": "pPGaGZTMc_4"
},
"snippet": {
"publishedAt": "2011-12-05T17:08:27Z",
"channelId": "UChug4c-a2tUGgZ-XeEKdxZQ",
"title": "Strongbow Neon Night Surfing Bondi",
"description": "To mark the start of summer, Strongbow joined forces with legendary surfing filmmaker Jack McCoy (Endless Summer
II), Bali Strickland and Eugene Tan ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/pPGaGZTMc_4/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/pPGaGZTMc_4/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/pPGaGZTMc_4/hqdefault.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "Surfers Village TV",
"liveBroadcastContent": "none",
"publishTime": "2011-12-05T17:08:27Z"
}
},
{
"kind": "youtube#searchResult",
"etag": "XjmemrMgMcym-3mlYs53ie_w3t4",
"id": {
"kind": "youtube#video",
"videoId": "4uwtqRBE4Kk"
},
"snippet": {
"publishedAt": "2010-08-13T02:10:28Z",
"channelId": "UCTYHNSWYy4jCSCj1Q1Fq0ew",
"title": "Andy Irons - i surf because short film",
"description": "Andy Irons is one of the world's greatest ever surfers. A 3 times world champion made famous by his epic battles with
Kelly Slater. But outside all the victories ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/4uwtqRBE4Kk/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/4uwtqRBE4Kk/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/4uwtqRBE4Kk/hqdefault.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "Billabong",
"liveBroadcastContent": "none",
"publishTime": "2010-08-13T02:10:28Z"
}
},
{
"kind": "youtube#searchResult",
"etag": "FZQyT7mq6dN1LX4M5RjXzVJxrtQ",
"id": {
"kind": "youtube#video",
"videoId": "kGvs0Nv5zJo"
},
"snippet": {
"publishedAt": "2013-11-15T09:31:05Z",
"channelId": "UCNSfJB-VQeHpv5ThtV1VtBA",
"title": "Wave cinematographer captures surfer's last wave",
"description": "On Wednesday morning, well known Wave Cinematographer Larry Haynes was filming those big sets from the
shore, and was rolling on Kirk Passmore as the ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/kGvs0Nv5zJo/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/kGvs0Nv5zJo/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/kGvs0Nv5zJo/hqdefault.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "KITV",
"liveBroadcastContent": "none",
"publishTime": "2013-11-15T09:31:05Z"
}
} ] }
Once the card is stacked with different videos, I swipe the card at the top. As soon as I swipe, the video in the 1st card appears in the card beneath (2nd card). I expected the 2nd video to play in the 2nd card as all the video IDs are different.
If I just drag a little and hold it, I can see the thumbnail of the 2nd video in the 2nd card. But, as soon as I swipe right, the video in the 2nd card (2nd video) gets replaced with the video in the 1st card (1st video).
This repeats until the last card.
Any help to crack this would be much appreciated. Thanks in anticipation.
Cheers.
Add a unique key to each of the YouTube cards, You could use the YouTube ID as a key
When to Use Keys
I want to display a chart (sap.viz.ui5.controls.VizFrame) that visualizes data from an OData Service. However, the data of the service has to be manipulated. See the example below:
Main.view.xml
<mvc:View controllerName="demo.chart.controller.Main" xmlns:mvc="sap.ui.core.mvc" displayBlock="true" xmlns="sap.m">
<Shell id="shell">
<App id="app">
<pages>
<Page id="page" title="Chart Demo">
<content></content>
</Page>
</pages>
</App>
</Shell>
</mvc:View>
Chart.fragment.xml
<c:FragmentDefinition xmlns:c="sap.ui.core" xmlns:viz="sap.viz.ui5.controls" xmlns:viz.data="sap.viz.ui5.data"
xmlns:viz.feeds="sap.viz.ui5.controls.common.feeds">
<viz:VizFrame uiConfig="{applicationSet:'fiori'}" vizType='donut'>
<viz:dataset>
<viz.data:FlattenedDataset data="{manipulatedData>/}">
<viz.data:dimensions>
<viz.data:DimensionDefinition name="Gender" value="{manipulatedData>gender}"/>
</viz.data:dimensions>
<viz.data:measures>
<viz.data:MeasureDefinition name="Amount" value="{manipulatedData>amount}"/>
</viz.data:measures>
</viz.data:FlattenedDataset>
</viz:dataset>
<viz:feeds>
<viz.feeds:FeedItem uid="color" type="Dimension" values="Gender"/>
<viz.feeds:FeedItem uid="size" type="Measure" values="Amount"/>
</viz:feeds>
</viz:VizFrame>
</c:FragmentDefinition>
Main.controller.js
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/json/JSONModel",
"sap/ui/core/Fragment"
], function (Controller, JSONModel, Fragment) {
"use strict";
return Controller.extend("demo.chart.controller.Main", {
onInit: function () {
const mDefault = this.getOwnerComponent().getModel();
const mManipulatedData = new JSONModel([{
gender: "F",
amount: 0
}, {
gender: "M",
amount: 0
}, {
gender: "X",
amount: 12
}]);
this.getView().setModel(mManipulatedData, "manipulatedData");
console.log(this.getView().getModel("manipulatedData"));
mDefault.read("/ContactSet", {
success: oData => {
const aManipulatedData = mManipulatedData.getData();
oData.results.forEach(entry => {
aManipulatedData.forEach(type => {
if (entry.Sex === type.gender) {
type.amount++
}
})
});
Fragment.load({
name: "demo.chart.view.Chart",
controller: this
}).then(oFragment => {
this.getView().addDependent(oFragment);
this.byId("page").addContent(oFragment);
});
}
})
}
});
});
Structure of the service (GWSAMPLE_BASIC/ContactSet)
{
"d": {
"results": [
{
"__metadata": {
"id": "https://sapes5.sapdevcenter.com/sap/opu/odata/IWBEP/GWSAMPLE_BASIC/ContactSet(guid'0050568c-901d-1eda-bcae-e8394de7e116')",
"uri": "https://sapes5.sapdevcenter.com/sap/opu/odata/IWBEP/GWSAMPLE_BASIC/ContactSet(guid'0050568c-901d-1eda-bcae-e8394de7e116')",
"type": "GWSAMPLE_BASIC.Contact"
},
"Address": {
"__metadata": {
"type": "GWSAMPLE_BASIC.CT_Address"
},
"City": "Walldorf",
"PostalCode": "69190",
"Street": "Robert-Koch-Straße",
"Building": "1",
"Country": "DE",
"AddressType": "02"
},
"ContactGuid": "0050568c-901d-1eda-bcae-e8394de7e116",
"BusinessPartnerID": "0100000000",
"Title": "",
"FirstName": "Karl",
"MiddleName": "",
"LastName": "Müller",
"Nickname": "",
"Initials": "",
"Sex": "M",
"PhoneNumber": "0622734567",
"FaxNumber": "0622734004",
"EmailAddress": "do.not.reply#sap.com",
"Language": "EN",
"DateOfBirth": null,
"ToBusinessPartner": {
"__deferred": {
"uri": "https://sapes5.sapdevcenter.com/sap/opu/odata/IWBEP/GWSAMPLE_BASIC/ContactSet(guid'0050568c-901d-1eda-bcae-e8394de7e116')/ToBusinessPartner"
}
}
}
]
}
}
So, as you can see, I'm only interested in the aggregated values of the Sex property. My current solution is to loop through all the entries of the entity set and increase the property in a custom JSON model. This is not only very inperformant because you do the heavy-lifting on the client side, it also requires you to know all the possible values for the data displayed in the chart right away. Is there a way to do this with OData queries or do I have to create a new entity set with the comulated data in my SAP system?
OData should be able to count and group, as described in this quesiton: OData v4 groupby with $count
But I doubt your SAP OData Service will, most of the time, those services do not implement the full OData specification.
You can improve your js by not using two nested for loops but something like this:
mGender = {};
oData.results.forEach(entry => {
if (!mGender[entry.Sex])
{mGender[entry.Sex] = 0
}
mGender[entry.Sex]++
});
I have a registration model in loopback over mongodb with fallowing properties:
"properties": {
"Fname": {
"type": "string",
"required": true
},
"Lname": {
"type": "string",
"required": true
},
"phone": {
"type": "string",
"required": true
},
"date": {
"type": "string",
"required": true
},
"time": {
"type": "string",
"required": true
}
}
in application I post some additional data with model required data, for controlling and processing in server side:
submitForm() {
let headers = new Headers(
{
'Content-Type': 'application/json'
});
let options = new RequestOptions({ headers: headers });
let data = JSON.stringify({
Fname: this.form.Fname,
Lname: this.form.Lname,
phone: this.form.phone,
time: this.form.time,
date: this.form.date,
uid: this.form.uid
});
//console.log(data);
let url = 'http://localhost:3000/api/registrations';
return new Promise((resolve, reject) => {
this.http.post(url, data, options)
.toPromise()
.then((response) => {
console.log('API Response : ', response.status);
resolve(response.json());
})
.catch((error) => {
console.error('API Error : ', error.status);
console.error('API Error : ', JSON.stringify(error));
reject(error.json());
});
});
}
In server side I have this code:
Registration.observe('before save', function (ctx, next) {
if (ctx.instance) {
// When Create (POST)
// ctx.instance have the json properties
console.log("Triggers when create");
if (checkUID(ctx.instance) ==200 ){
console.log('ok');
}
} else {
// When Update (UPDATE)
// ctx.data have the json properties
console.log("Triggers when update");
}
next();
});
but after successfully registration I saw that uid that was added into document and regardless to model's properties, document contains addition properties.
{
"Fname": "Eram",
"Lname": "SA",
"phone": "1234567890",
"date": "2017/10/06",
"time": "17:37:46",
"id": "59d78e3f5e5e6704205038aa",
"uid": "38bc3241a43073a7b40d186f24923cc5"
},
What is the correct way to manage the state of radio and checkboxes using React?
In some instances a form would be rendered partially completed so some radio and checkboxes would be pre selected on first load.
I have the following code snippet and i cannot get it to work as expected.
var formData = {
"id": 13951,
"webform_id": 1070,
"page": 0,
"type": "radios",
"name": "What industry are you in?",
"tooltip": "",
"weight": 0,
"is_required": 1,
"default_value": "",
"validation": "",
"allow_other_option": 0,
"other_option_text": "",
"mapped_question_id": "a295189e-d8b4-11e6-b2c5-022a69d30eef",
"created_at": "2017-04-07 18:40:39",
"updated_at": "2017-04-07 18:40:39",
"option_conditional_from": null,
"default_value_querystring_key": "",
"deleted_at": null,
"is_auto_save": 0,
"is_component_number_hidden": 0,
"is_component_inline": 0,
"enable_confirm_validation": 0,
"confirm_validation_text": null,
"additional_options": "",
"url_mapping": "",
"webformcomponentoptions": [
{
"id": 13888,
"webform_component_id": 13951,
"key": "Hospitality",
"value": "Hospitality",
"created_at": "2017-04-07 18:40:39",
"updated_at": "2017-04-07 18:40:39",
"group": "",
"selected" : false
},
{
"id": 13889,
"webform_component_id": 13951,
"key": "Retail",
"value": "Retail",
"created_at": "2017-04-07 18:40:39",
"updated_at": "2017-04-07 18:40:39",
"group": "",
"selected" : false
},
{
"id": 13890,
"webform_component_id": 13951,
"key": "Other",
"value": "Other",
"created_at": "2017-04-07 18:40:39",
"updated_at": "2017-04-07 18:40:39",
"group": "",
"selected" : false
}
]
}
class WebformApp extends React.Component {
render() {
return (
<form>
<label>{this.props.webform.name}</label>
<div className="group-wrapper">
<Radio radio={this.props.webform.webformcomponentoptions} />
</div>
</form>
)
}
}
class Radio extends React.Component {
render() {
var options = [];
this.props.radio.forEach(function(radio, i) {
options.push(<Option option={radio} key={radio.id} index={i} />);
})
return (
<div>{options}</div>
)
}
}
class Option extends React.Component {
constructor(props) {
super(props);
this.handleOptionChange = this.handleOptionChange.bind(this);
this.state = {selectedIndex: null};
}
handleOptionChange(e) {
this.setState({selectedIndex: this.props.index}, function() {
});
}
render() {
const selectedIndex = this.state.selectedIndex;
return (
<div>
<input type="radio"
value={this.props.option.value}
name={this.props.option.webform_component_id}
id={this.props.option.id}
checked={selectedIndex === this.props.index}
onChange={this.handleOptionChange} />
<label htmlFor={this.props.option.id}>{this.props.option.key}</label>
</div>
)
}
}
ReactDOM.render(
<WebformApp webform={formData} />,
document.getElementById('app')
);
https://codepen.io/jabreezy/pen/KWOyMb
The most important thing would be to have the Radio component handle the state, and keeping track of the selected option.
In addition, I would simplify by using map instead of forEach, and foregoing the Option component for a class method returning an <input type='radio'>. For simplicity's sake, using the option value for keeping track of the selected state instead of the index, and mimicking React's select component allowing a default value prop instead of setting each option's selected prop (which you don't seem to be using).
Finally, for order's sake, renaming the Radio:s radio prop to the (IMO) more correct options. Ergo (caveat, I haven't tested this):
class WebformApp extends React.Component {
render() {
return (
<form>
<label>{this.props.webform.name}</label>
<div className="group-wrapper">
<Radio options={this.props.webform.webformcomponentoptions} value={this.props.webform.value} />
</div>
</form>
)
}
}
class Radio extends React.Component {
constructor (props) {
super(props)
this.handleOptionChange = this.handleOptionChange.bind(this)
this.state = {value: this.props.value}
}
render() {
return this.props.options.map(this.getOption)
}
handleOptionChange (e) {
this.setState({value: e.target.value})
}
getOption (option) {
return (
<div>
<input type='radio'
value={option.value}
name={option.webform_component_id}
id={option.id}
key={option.id}
checked={this.state.value === option.value}
onChange={this.handleOptionChange} />
<label htmlFor={option.id}>{option.key}</label>
</div>
)
}
}
ReactDOM.render(
<WebformApp webform={formData} />,
document.getElementById('app')
);
Thank you so much for your input Linus. You set me along the correct path and i've solved my problem the following way:
var formData = {
"id": 13951,
"webform_id": 1070,
"page": 0,
"type": "radios",
"name": "What industry are you in?",
"tooltip": "",
"weight": 0,
"is_required": 1,
"default_value": "",
"validation": "",
"allow_other_option": 0,
"other_option_text": "",
"mapped_question_id": "a295189e-d8b4-11e6-b2c5-022a69d30eef",
"created_at": "2017-04-07 18:40:39",
"updated_at": "2017-04-07 18:40:39",
"option_conditional_from": null,
"default_value_querystring_key": "",
"deleted_at": null,
"is_auto_save": 0,
"is_component_number_hidden": 0,
"is_component_inline": 0,
"enable_confirm_validation": 0,
"confirm_validation_text": null,
"additional_options": "",
"url_mapping": "",
"webformcomponentoptions": [
{
"id": 13888,
"webform_component_id": 13951,
"key": "Hospitality",
"value": "Hospitality",
"created_at": "2017-04-07 18:40:39",
"updated_at": "2017-04-07 18:40:39",
"group": "",
"selected" : false
},
{
"id": 13889,
"webform_component_id": 13951,
"key": "Retail",
"value": "Retail",
"created_at": "2017-04-07 18:40:39",
"updated_at": "2017-04-07 18:40:39",
"group": "",
"selected" : false
},
{
"id": 13890,
"webform_component_id": 13951,
"key": "Other",
"value": "Other",
"created_at": "2017-04-07 18:40:39",
"updated_at": "2017-04-07 18:40:39",
"group": "",
"selected" : false
}
]
}
class WebformApp extends React.Component {
render() {
return (
<form>
<label>{this.props.webform.name}</label>
<div className="group-wrapper">
<Radio radio={this.props.webform.webformcomponentoptions} />
</div>
</form>
)
}
};
class Radio extends React.Component {
constructor(props) {
super(props);
this.state = {selectedOption: 'Other'};
}
handleOptionChange(changeEvent) {
this.setState({
selectedOption: changeEvent.target.value
})
};
renderOption(props) {
return (
<div>
<h3>{props.index}</h3>
<input type="radio"
value={props.option.value}
name={props.option.webform_component_id}
id={props.option.id}
checked={props.status}
onChange={props.clickeme} />
<label htmlFor={props.option.id}>{props.option.key}</label>
</div>
)
};
render() {
return (
<div>
{this.props.radio.map(function(radio) {
var selected = this.state.selectedOption === radio.value;
return <this.renderOption option={radio} key={radio.value} status={selected} clickeme={(e)=> this.handleOptionChange(e)} />;
}, this)}
</div>
)
};
};
ReactDOM.render(
<WebformApp webform={formData} />,
document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>