Coder Social home page Coder Social logo

fitanalytics-webwidget-ios's Introduction

FitAnalytics WebWidget Integration in iOS (Objective-C)

Overview

The WebWidget SDK allows integrating the Fit Analytics Size Advisor widget into your own iOS app.

As a first step, we suggest that you familiarize yourself with the Fit Analytics web-based Size Advisor service by:

  1. Reading through the Fit Analytics website and trying out a sample product - https://www.fitanalytics.com/

The integration method currently supported by this SDK is based on loading HTML/JS-based widget code in a separate WKWebView instance and establishing communication between the host app and the embedded web widget.

The SDK introduces a layer that imitates a web-based (JavaScript) integration of the Fit Analytics widget by:

  1. Exporting the FitAWebWidget class, which serves as a main web view widget controller.
  2. Creating and initializing the widget in a provided web view instance.
  3. Exposing several methods that allow controlling the widget.
  4. Defining the FITAWebWidgetHandler interface, which allows registering various callbacks (by implementing them as interface methods). These callbacks are invoked by the widget controller through various events (e.g. when a user closes the widget, when the widget displays a recommendation, etc.).

Preferably, you can also include the purchase reporting for the order confirmation page/view.


Installation (using Cocoapods)

Prerequisities:

  1. XCode 7 or higher
  2. iOS 9 or higher

Step 1. Make sure you already have cocoapods installed in your system. Otherwise you can follow the steps in cocoapods documentation to install https://cocoapods.org/

Step 2. To initialize the pod in your project use pod init command to create a Podfile with smart defaults.

Step 3. Now you can install the dependencies in your project by using pod install command.

Step 4. Make sure you always open Xcode Workspace instead of Xcode Project. You can do it by using open YourApp.xcworkspace command in terminal.

Installation (using the universal binary framework)

Alternatively you can use the pre-built universal binary framework. It's available for download with each release (since v0.4.2).

It comes in two build flavors:

  1. all - which includes binary code for both devices (arm7x, arm64) and simulators (i386, x86_64)
  2. device_only - which includes just device-specific binaries (arm7x, arm64). The device-specific flavor is meant for final builds that are meant to be released in App store (which disallows the simulator binary code in apps).

You can find the minimal example project that uses the binary framework in UniversalFramework/Framework Example subdirectory of the repository.

Step 1. Download the framework package from the (release page)[https://github.com/UPcload/FitAnalytics-WebWidget-iOS/releases]

Step 2. Unpack the binary framework and add to the XCode project via "Add files ..." context menu.

Step 3. When including header files (see below) add them in the form

#import <FitAnalytics_WebWidget/FITAWebWidget.h>
...

instead of

#import "FITAWebWidget.h"
...

Widget integration Procedure

We're presuming a simple app with the single main ViewController class.

Import the FitAWebWidget.h and the FITAWebWidgetHandler.h header file into your ViewController.m

#import "FITAWebWidget.h"
#import “FITAWebWidgetHandler.h"

The view controller class implements the FITAWebWidgetHandler interface, so that the widget callback can be implemented directly on it.

@interface ViewController ()<FITAWebWidgetHandler>

Add a property for storing the reference to the widget.

@property (nonatomic, strong) FITAWebWidget *widget;

Add a property for the WKWebView reference. This is the WKWebView instance that will contain the load widget container page.

@property (weak, nonatomic) IBOutlet WKWebView *wkWebView;

Initialize the widget controller with the WKWebView instance and assign self as the callback handler.

self.widget = [[FITAWebWidget alloc] initWithWKWebView:self.wkWebView handler:self];

WARNING

Since iOS 12.x update, when using WKWebView, all widget interactions are reliable only when the widget container WebView is connected to the view hierarchy. When the WebView is in a disconnected state (i.e. it has no superview) all HTTP requests inside it begin to fail/timeout silently. To avoid this issue, always keep the WebView connected in the hierarchy and hide it visually (by setting the hidden property and the frame dimensions to zero). We are still looking into a better solution and/or workaround.

Methods

- (BOOL)load

Begin loading the HTML widget container page.

[self.widget load];

 

- (BOOL)create:productSerial options:NSDictionary

Create and (optionally) initialize it with product serial and other options.

Create a widget instance inside the container page by passing the productSerial and options. Options can be nil or a dictionary of various options arguments. Important supported options are listed here.

This method should be called only after the WebWidgetDidBecomeReady callback has been called (or inside the callback) and will return a true when the widget was successfully created.

[self.widget create:@"example-123456" options:@{ "sizes": @[ @"S", @"XL" ] }];

 

- (void)open

Show the actual widget. It may trigger loading additional resources over network, and will show the widget only after all assets have been loaded. When the opening is finished, the WebWidgetDidOpen callback will be called on the callback handler.

[self.widget open];

 

- (void)openWithOptions:productSerial options:NSDictionary

Configure the widget with new productSerial and/or options and show it. See open above for more details.

[self.widget open:@"example-123456" options:@{ "sizes": @[ @"S", @"XL" ] ];

 

- (void) close

Close the widget and remove the widget markup. Will trigger the webWidgetDidClose callback when it finishes.

[self.widget close];

 

- (void) recommend

Request a recommendation. The recommended size and additional details will be returned as arguments to the WebWidgetDidRecommend callback.

[self.widget recommend];

 

- (void) recommendWithOptions:productSerial options:NSDictionary

Configure the widget with the new productSerial and/or widget options and request a recommendation. See recommend above for more details.

[self.widget recommend:@"example-123456" options:nil];

 

- (void)reconfigure:productSerial options:NSDictionary

Configure the widget with the new productSerial and/or widget options. If the productSerial argument is non-nil and is different from the last provided product serial, this will trigger a request for the product information. After the new product info is loaded, the webWidgetDidLoadProduct will be called. If the product serial is invalid or the product isn't supported by Fit Analytics, the webWidgetDidFailLoadingProduct will be called.

[self.widget reconfigure:@"example-123456" options:nil];

// OR

[self.widget reconfigure:nil options:@{ "sizes": @[ @"XL" ] ];

Reconfigure the widget with a new product serial and/or widget options object.

Callbacks

- (void)webWidgetDidBecomeReady:(FITAWebWidget *)widget;

This method will be called when widget container inside the WebView has successfully loaded and is ready to accept commands.

 

- (void)webWidgetInitialized:(FITAWebWidget *)widget;

This method will be called when widget container inside the WebView has successfully loaded.

  • widget .. The widget controller instance

 

- (void)webWidgetDidFailLoading:(FITAWebWidget *)widget withError:(NSError *)error;

This method will be called when widget inside the WebView has failed to load or initialize for some reason.

  • widget .. The widget controller instance
  • error .. The error instance
- (void)webWidgetDidLoadProduct:(FITAWebWidget *)widget productId:(NSString *)productId details:(NSDictionary *)details;

This method will be called when the widget has successfully loaded the product info. A successful load means that the product is supported by Fit Analytics and the widget should be able to provide a size recommendation for it.

  • widget .. The widget controller instance
  • productId .. The ID of the product
  • details .. The details object.

 

- (void)webWidgetDidFailLoadingProduct:(FITAWebWidget *)widget productId:(NSString *)productId details:(NSDictionary *)details;

This method will be called when widget failed to load the product info or the product is not supported.

  • widget .. The widget controller instance
  • productId .. The ID of the product
  • details .. The details object.

 

- (void)webWidgetDidOpen:(FITAWebWidget *)widget productId:(NSString *)productId;

This method will be called when the widget has successfully opened after the open method call.

  • widget .. The widget controller instance
  • productId .. The ID of the product

 

- (void)webWidgetDidClose:(FITAWebWidget *)widget productId:(NSString *)productId size:(nullable NSString *)size details:(nullable NSDictionary *)details;

This method will be called when user of the widget has specifically requested closing of the widget by clicking on the close button.

  • widget .. The widget controller instance
  • productId .. The ID of the product
  • size .. The last recommended size of the product, if there was a recommendation. null if there wasn't any recommendation.
  • details .. The details object.

 

- (void)webWidgetDidAddToCart:(FITAWebWidget *)widget productId:(NSString *)productId size:(nullable NSString *)size details:(nullable NSDictionary *)details;

This method will be called when user of the widget has specifically clicked on the add-to-cart inside the widget.

  • widget .. The widget controller instance
  • productId .. The ID of the product
  • size .. The size of the product that should be added to cart.
  • details .. The details object.

 

- (void)webWidgetDidRecommend:(FITAWebWidget *)widget productId:(NSString *)productId size:(nullable NSString *)size details:(nullable NSDictionary *)details;

This method will be called after the getRecommendation call on the FITAWebWidget controller, when the widget has received and processed the size recommendation.

  • productId .. The ID of the product
  • size .. The recommended size of the product.
  • details .. The details object.

Configurable widget options

@interface FitAnalyticsWidgetOptions : NSObject

/**
 *  (Shop Session ID) .. a first-party client generated session ID (can be a cookie): we use it to track purchases and keep our data more consistent (we **do NOT** use it to track or identify users)
 */
@property (nonatomic, strong) NSString *shopSessionId;

/**
 * The shop prefix, this is a value that we set internally so we can identify your shop with the product.
 */
@property (nonatomic, strong) NSString *shopPrefix;

/**
 * The product serial number, which is used to identify the product in the Fit Analytics database.
 * If `shopPrefix` is not set, we are going to infer the shop prefix based on the product serial number prefix. E.G. `shopprefix-abcd1234`
 */
@property (nonatomic, strong) NSString *productSerial;

/**
 * Product thumbnail image URL.
 */
@property (nonatomic, strong) NSString *thumb;

/**
 * All the sizes of your product, each size should be a key in the object, and the value should be a boolean indicating if the size is available or not.
 * They keys should match with the keys in the products' feed.
 * E.G. @{
 *     @"M": @YES,
 *     @"L": @NO
 *  };
 *  means that the product is available in size M but not in size L.
 */
@property (nonatomic, strong) NSDictionary<NSString *, BOOL *> *manufacturedSizes;


/**
 * In stock sizes for the current product.
 * E.G. @[ @"S", @"XL" ]
 */
@property (nonatomic, strong) NSArray<NSString *> *sizes;

/**
 * The user identifier based on the shop's user id, for example in case the user is logged in.
 */
@property (nonatomic, strong) NSString *userId;

/**
 * ISO 639-1
 * E.G. "en"
 */
@property (nonatomic, strong) NSString *language;

/**
 * ISO 3166-1
 * E.G. "GB"
 */
@property (nonatomic, strong) NSString *shopCountry;

/**
 * Metric system
 * 0: imperial
 * 1: metric
 * 2: british
 * If it is not set it will be inferred from the shop country.
 */
@property (nonatomic) NSInteger metric;

- (void)close:(NSString *)productSerial size:(NSString *)size;
- (void)error:(NSString *)productSerial;
- (void)cart:(NSString *)productSerial size:(NSString *)size;
- (void)recommend:(NSString *)productSerial size:(NSString *)size;
- (void)load:(NSString *)productSerial;

@property (nonatomic, strong) NSString *userAge;

/**
 * m: man
 * w: women
 */
@property (nonatomic, strong) NSString *userGender;

/**
 * Even if the `metric` property is set to a different numerical system, the user's weight and height should be in kilograms and centimeters respectively.
 */
@property (nonatomic, strong) NSString *userWeight;
@property (nonatomic, strong) NSString *userHeight;

/**
 * Women bra measurements
 * The values described bellow can be obtained from the user's profile after they have filled in their bra measurements.
 * It is a large subset of possible bra measurements to be described, usually you feed the widget with the measurements available from the profile
 */
@property (nonatomic, strong) NSString *userBraBust;
@property (nonatomic, strong) NSString *userBraCup;
@property (nonatomic, strong) NSString *userBraSystem;

/**
 * Enforces the mobile view, so in tablets you will see the phone widget layout.
 */
@property (nonatomic, assign) BOOL showMobile;

@end

Purchase reporting

Purchase reporting usually means that when the user receives a confirmation of a successful purchases, namely, the user sees the Order Confirmation Page (a.k.a OCP or checkout page), the app will report all items in the order to Fit Analytics. The reporting is done by sending a simple HTTP request.

The usual report is a collection of attributes such as the order ID, the product serial for each purchased item, purchased size, price, currency, etc.

The most common attributes are:

@interface FitAnalyticsPurchaseOptions : NSObject

/**
 *  (Shop Session ID) .. a first-party client generated session ID (can be a cookie): we use it to track purchases and keep our data more consistent (we **do NOT** use it to track or identify users)
 * (value **MUST** conform with the one passed in the PDP for the same shopping session)
 */
@property (nonatomic, strong) NSString *shopSessionId;

/**
 * The product serial number, which is used to identify the product in the Fit Analytics database.
 * If `shopPrefix` is not set, we are going to infer the shop prefix based on the product serial number prefix. E.G. `shopprefix-abcd1234`
 */
@property (nonatomic, strong) NSString *productSerial;

/**
 * (optional) the size-specific identifier
 */
@property (nonatomic, strong) NSString *shopArticleCode;

/**
 * Acts as a size code identifier that we can use when gathering data per size
 */
@property (nonatomic, strong) NSString *ean;

/**
 * Shops' internal order identifier.
 */
@property (nonatomic, strong) NSString *orderId;

/**
 * It should match the size that is available in the product's feed.
 */
@property (nonatomic, strong) NSString *purchasedSize;

/**
 * The user identifier based on the shop's user id, for example in case the user is logged in.
 */
@property (nonatomic, strong) NSString *userId;

/**
 * ISO 639-1
 * E.G. "en"
 */
@property (nonatomic, strong) NSString *language;

/**
 * ISO 3166-1
 * E.G. "GB"
 */
@property (nonatomic, strong) NSString *shopCountry;

@property (nonatomic, assign) NSNumber *price;

@property (nonatomic, assign) NSNumber *quantity;

/**
 * E.G. "EUR" | "USD" | "GBP"
 */
@property (nonatomic, strong) NSString *currency;

@end

Usage

Import the FITAPurchaseReport.h and the FITAPurchaseReporter.h header file.

#import "FITAPurchaseReport.h"
#import "FITAPurchaseReporter.h"

Create a new instance of the purchase reporter (FITAPurchaseReporter).

FITAPurchaseReporter *reporter = [[FITAPurchaseReporter alloc] init];

For each line item present in the customer's order, create a new instance of FITAPurchaseReport and send it via reporter.

FITAPurchaseReport *report = [[FITAPurchaseReport alloc] init];

report.orderId = @"0034";
report.userId = @"003242A32A";
report.productSerial = @"test-55322214";
report.purchasedSize = @"XXL";

report.shopSessionId = @"0a1b2c3d"
// add additional attributes, such as shopCountry, lanugage etc. here.

[reporter sendReport:report];

Alternatively, you can initialize the report instance with a dictionary. That can be useful for setting shared defaults, for example.

NSDictionary *reportDefaults = @{
  @"orderId": @"0034",
  @"userId": @"003242A32A"
};

FITAPurchaseReport *report = [[FITAPurchaseReport alloc] initWithDictionary:reportDefaults];

report.productSerial = @"test-55322214";
report.purchasedSize = @"XXL";

[reporter sendReport:report];

If you wish to wait until reporting has finished, you can pass a callback function.

[reporter sendReport:report done:^(NSError *error) {
  if (error != nil)
    NSLog("ERROR: %@", error);
  else
    NSLog("SUCCESS");
}]

Privacy Information

The Info.plist file includes usage descriptions for the following data collected by the SDK:

  • User input data such as height, weight, body shapes and fit preference
  • User Id for Fit Finder profile creation and serving immediate recomendations
  • Session identifiers for tracking usage within the app to connect Fit Finder usage with purchases and returns for better ML model performance

fitanalytics-webwidget-ios's People

Contributors

tjelen avatar nicolasjuarezn avatar tomshenhav avatar ayoubbenaissafita avatar zahramammadli avatar

Stargazers

Carlos Ricardo Santos avatar Rafael Gabriel avatar アンドレ avatar Vitor Leonardi avatar

Watchers

Marcio Lorran avatar Vitor Leonardi avatar Miguel Rubinos Rodríguez avatar James Cloos avatar Carla avatar Terese Haimberger avatar  avatar sasahara.mieko avatar  avatar 杨雄杰 avatar Henri Beck avatar Andrei Mihai Mereuta avatar Eric Nordebäck avatar  avatar  avatar Jorge Morales avatar MGRe-takano avatar Rama avatar  avatar  avatar Linus Hellberg avatar AM avatar Patrícia Gabriele Neri avatar  avatar Andrijana Arsovska avatar  avatar David Miguel Martínez avatar Olivier Bonal (Asos) avatar christian hr avatar  avatar Pedro Ramirez avatar

fitanalytics-webwidget-ios's Issues

Universal framework with a defined iOS deployment target

Rather than the app incorporating the FITAWebWidget source code into it's build target, there should be a xcode project that builds a FitAnalytics universal framework.
Why? - When we update the iOS deployment target to iOS 10.0 for our app, we get deprecation warnings for some methods in FITAWebWidget. We don't want to fix those warnings by maintaining our own branch of this repository.
A FitAnalytics framework with the deployment target set to the appropriate version (iOS 9?) would solve that, and naturally we'd prefer that such a build target be maintained in this repository.

Xcode 13 b4 - SwiftPackageManager App Extensions

Using latest version of FitAnalytics-WebWidget-iOS and Xcode 13.0 beta 4 the App Extensions targets don't compile due:

'sharedApplication' is unavailable: not available on iOS (App Extension) - Use view controller based solutions where appropriate instead.

and

'openURL:' is unavailable: not available on iOS (App Extension)

WebView initialization gets waiting forever

We can't initialize properly our FitAnalytics interface in our iOS app. It gets stuck after receiving the "__init" message from the embedded JS widget, so our app keeps waiting for the "load" message forever and our FitAnalytics user interface gets hung in a "loading" state (see attached screenshot).

Our logic follow these steps:
1 - We instantiate a FITAWebWidget by passing a WKWebView/UIWebView component as a parameter, we call [FITAWebWidget load], and then wait to receive a "webWidgetDidBecomeReady" callback from it.
2 - When "webWidgetDidBecomeReady" is called, we respond by calling [FITAWebWidget create:zara-3519100800-I2018 options:]. Then, we receive the "__init" signal from the widget via "webWidgetInitialized" handler/delegate method, but our side keeps waiting for the "webWidgetDidLoadProduct" callback to arrive to take further actions, and this never happens.

The issue seems to be related to iOS 12 and its WKWebView component. We are detecting this issue in some devices updated to iOS 12. If it happens in one device, it happens always in this device. We couldn't reproduce this issue in MacOS device Simulator.
If we instantiate a deprecated UIWebView instead and run our App in the same devices, the problem disappears: we receive this "webWidgetDidLoadProduct" callback from the FITAWebWidget and we can now take further actions like "open" or "recommend".

We've attached traces inserted in FITAWebWidget.m file that picture a "working" case (FitAnalyticsTraces_working.txt) and a non-working one (FitAnalyticsTraces_failing).

** Options sent to [FITAWebWidget create:options:] method:

▿ 5 elements
▿ 0 : 2 elements
- key : "manufacturedSizes"
▿ value : 5 elements
▿ 0 : 2 elements
- key : "S"
- value : true
▿ 1 : 2 elements
- key : "M"
- value : true
▿ 2 : 2 elements
- key : "XS"
- value : true
▿ 3 : 2 elements
- key : "L"
- value : true
▿ 4 : 2 elements
- key : "XL"
- value : true
▿ 1 : 2 elements
- key : "cart"
- value : true
▿ 2 : 2 elements
- key : "userId"
- value : "4cAcqAbJN79urYseIHKwMzp/wfC454NowjYvY8t2AtY="
▿ 3 : 2 elements
- key : "language"
- value : "en"
▿ 4 : 2 elements
- key : "shopCountry"
-

Useful information:

fitanalytics_interface_gets_hung_loadingstate
FitAnalyticsTraces_failing.txt
FitAnalyticsTraces_working.txt

Apple Privacy Manifest File

Third-party software development kits (SDKs) can provide great functionality for apps; they can also have the potential to impact user privacy in ways that aren’t obvious to developers and users. As a reminder, when you use a third-party SDK with your app, you are responsible for all the code the SDK includes in your app, and need to be aware of its data collection and use practices [...]

Privacy manifest files outline the privacy practices of the third-party code in an app, in a single standard format. When you prepare to distribute your app, Xcode will combine the privacy manifests across all the third-party SDKs used by your app into a single, easy-to-use report. With one comprehensive report that summarizes all the third-party SDKs found in an app, it will be even easier for you to create more accurate Privacy Nutrition Labels.

Starting in spring 2024, you must include the privacy manifest for any SDK listed below when you submit new apps in App Store Connect that include those SDKs, or when you submit an app update that adds one of the listed SDKs as part of the update.
Describe the solution you'd like 🤔
A clear and concise description of what you want to happen.

Any update about the privacy manifest file?

missing information about Installation (using Cocoapods)

Hi,

it seems that there are some information about the cocoapod integration are missing.
Text in the Readme:
"Prerequisities:

XCode 7 or higher
iOS 8 or higher
Step 1. Make sure you already have cocoapods installed in your system. Otherwise you can follow the steps in cocoapods documentation to install https://cocoapods.org/

Step 2. To initialize the pod in your project use pod init command to create a Podfile with smart defaults.

Step 3. Now you can install the dependencies in your project by using pod install command.

Step 4. Make sure you always open Xcode Workspace instead of Xcode Project. You can do it by using open YourApp.xcworkspace command in terminal."

the name of the pod is missing and the source if it ist nor in cocoapods

Retain cycle due to FITAWebWidget handler strong reference

In FITAWebWidget.m, the handler property is declared as strong:

@property (nonatomic, strong) id<FITAWebWidgetHandler> handler;

This can easily cause a retain loop, because the object implemented FITAWebWidgetHandler often retains the FITAWebWidget. In our app that's the case, and also the demo view controller in FitAnalytics-Demo/ViewController.m has exactly this issue. If you would present and dismiss this ViewController from another view controller you would get a memory leak.

Suggested resolution: declare handler as weak:

@property (nonatomic, weak) id<FITAWebWidgetHandler> handler;

Dry-run functionality

When testing our app, we perform orders against a pre-production system, but also against the production system. We don't disable any functionality for our test builds, since we want to test exactly the same code path that we subsequently release.
You probably don't want our many test runs to end up as records in your purchase reporting database, perhaps not even in the recommendation statistics, so I suggest that you add some kind of dry-run functionality. For example a dryRun=YES property on the recommendations and purchase report requests, which we will set on our test builds.

WKWebView on iOS12 does not call didLoad delegate method

We found that after running the app on iOS 12 the delegate method webWidgetDidLoadProduct wasn't called anymore using the WKWebView. It bizarrely still worked on the simulator but not anymore on actual devices.

Our workaround was to replace the WKWebView with a UIWebView, which resulted in the delegate method webWidgetDidFailLoadingProduct being fired because of NSURLErrorDomain error -999.

After adjusting the UIWebViewDelegate in the framework, everything worked as expected again. See https://discussions.apple.com/thread/1727260?answerId=8877452022#8877452022 for reference. This was the adjustment we had to make to the UIWebView method didFailLoadWithError:

- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
    if (error.code == NSURLErrorCancelled) {
        return;
    }
    [self onLoadError:error];
}

Expected language code for Simplified vs Traditional chinese

Documentation: https://developers.fitanalytics.com/documentation#list-callbacks-parameters says "in the specified 2-letter language (ISO 639-1)".
But there is no standard 2-letter code for simplified and traditional chinese. Only "zh" for the "Chinese language group" which is a mostly useless code.

We have users in Taiwan, Hong Kong, Macao and most of those expect Traditional Chinese script. The rest of our app outside the fit analytics widget is localised accordingly.

Suggestion, support BCP47 identifiers:
"zh-Hant"
"zh-Hans"
or even better also the local regional variants:
"zh-Hant-TW"
"zh-Hant-HK"

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.